July 5, 2023

Об эффективных адресах Ethereum

Когда речь идет о публичных блокчейнах, ничего не достается бесплатно. Именно поэтому постепенное повышение эффективности может привести к значительному снижению затрат, особенно в долгосрочной перспективе. В Ethereum и других блокчейнах, использующих EVM, это часто означает поиск способов сэкономить на стоимости газа, либо полностью избегая блокчейна, когда это возможно (например, контрфактическое инстантирование и обобщенные каналы состояний, красноречиво объясненные здесь Риком Санчесом), либо тщательно оптимизируя транзакции на цепочке, когда это необходимо

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

Я уже догадываюсь, о чем вы думаете:

Подождите минутку - каждая ванильная транзакция стоит 21 000 газа, независимо от того, о каких адресах идет речь. Кроме того, мой милый адрес 0xdecafdeadbeef пришлось искать целую вечность. Мне понадобятся доказательства, подтверждающие ваше язвительное утверждение, что выбор адреса имеет значение

Как обычно, ответ можно найти в "Желтой газете". (Ого, смелое открытие...)

Постарайтесь не слишком переживать из-за всех этих пикантных подробностей

Нас интересуют несколько таких операций (и их аналоги), в частности: gtxdatazero (против gtxdatanonzero) и gsload + gsset (против неиспользования gsset — вся это штука с sstore звучит чертовски дорого)

Данные о звонках, малые данные

Сначала рассмотрим gtxdatazero: он стоит 4 газа на каждый нулевой байт данных транзакции. Сравните это со стоимостью gtxdatanonzero: 68 газа, или в 17 раз дороже. Как следствие, каждый раз, когда в msg.data используется нулевой байт вместо ненулевого, экономится 64 газа. Несколько моментов, которые следует запомнить в связи с этим наблюдением:

  • При просмотре байтовых строк в шестнадцатеричной форме каждая пара цифр (где каждый символ представляет одну из 16 возможных цифр) образует байт (16² = 2⁸ = 256 бит). Одиночные шестнадцатеричные нули или соседние нули, распределенные по двум разным байтам, не уменьшают вес строки байтов
  • Порядок нулевых байтов не влияет на выгоду - по крайней мере, от этой конкретной оптимизации - только общее количество нулевых байтов, используемых для замены соответствующего ненулевого байта

Адреса - одна из областей, где это свойство может пригодиться. Рассмотрим случай с передачей ERC20 transfer(). Вес хэмминга msg.sender здесь не имеет значения, но, как оказалось, параметр _to переданный в msg.data как часть функции transfer имеет значение (кстати, как и селектор функции)

Используя OpenZeppelin’s StandardToken в качестве эталонной реализации, стандартная transfer по адресу без нулевых байтов стоит 51 486 газа. Однако передача по адресу с восемью нулевыми байтами стоит всего 50 974 газа, разница составляет51486 — 50974 = 512 газа. Это также можно выразить как 64 * 8: как раз то, что мы ожидали от вышеприведенных расчетов. Это все равно что иметь карточку "купи сто, получи один бесплатно" для всех жетонов, переведенных на адрес. И, если вы задавались вопросом о transferFrom (а вы, конечно, задавались, хитрый пес), угадайте что: если оба адреса имеют восемь нулевых байтов, экономия газа на транзакцию будет вдвое больше!

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

(As an aside, when using this method you want to be extra careful to protect against the Short Address attack, explained in more depth here. In brief, if an address has trailing zero bytes, you can truncate them when requesting that a third party construct some token transfer for you, and if they don’t validate the input properly it will “steal” the extra bytes from the next parameter, amount. Then, whaddaya know: we now get back 256 times as many tokens!)

(В качестве дополнения, при использовании этого метода вы должны быть особенно осторожны, чтобы защититься от атаки короткого адреса, о которой более подробно рассказывается здесь. Вкратце, если адрес имеет нулевые байты в конце, вы можете усечь их, когда просите третью сторону построить для вас передачу токенов, и если они не подтвердят ввод должным образом, она "украдет" лишние байты из следующего параметра, суммы. И что же: теперь мы получаем обратно в 256 раз больше токенов)

Далее рассмотрим gsset: эта и gsreset обе работают с опкодом sstrore и устанавливают 32-байтовые слова в состояние, ну а хранение любых ненулевых данных в первый раз стоит 20 000 газа (да... если мы сможем избежать вызова sstore, That_Would_Be_Great.gif). После того, как слово будет сохранено, оно будет стоить еще 200 газов при каждом извлечении через sload, что тоже не очень здорово

When it comes to addresses in particular, we can pull off some nice gas savings as follows: if the address has at least four leading zero bytes (or eight leading zeroes in hex-encoded format), then each address will only need to take up 16 bytes. If you do the math, taking covariance into account when integrating over the partial differential equations and remembering to carry the seven, we find that two addresses can be packed into one sweet 32-byte package. (Word.)

Когда речь идет, в частности, об адресах, мы можем добиться неплохой экономии газа следующим образом: если адрес имеет хотя бы четыре ведущих нулевых байта (или восемь ведущих нулей в шестнадцатеричном кодировании), то каждый адрес должен занимать всего 16 байт. Если вы посчитаете, учтете ковариацию при интегрировании по дифференциальным уравнениям и не забудете перенести семерку, то выяснится, что два адреса можно упаковать в один сладкий 32-байтовый пакет. (Слово)

Эта оптимизация интересна тем, что вы не только используете экономию от улучшений gtxdatazero но и экономите еще больше газа, поскольку вам не нужно читать и записывать столько слов из данных вызова. Другая интересная особенность этой оптимизации заключается в том, что уже существует пробный токен, позволяющий воспользоваться ее преимуществами - подробности позже, но суть в том, что (в зависимости от метода передачи) мы можем сэкономить до 5% от общей стоимости передачи токена

Вы также можете увидеть смежное преимущество этой оптимизации в действии, проверив печально известный GasToken, именно то, как он обрабатывает GST2. Адрес GST2, 0x0000000000b3F879cb30FE243b4Dfee438691c04, использует только 15 байт памяти. Таким образом, развернутые нежелательные контракты могут быть на 5 байт короче и все равно проверять, что msg.sender соответствует контракту GST2, чтобы selfdestruct и получить скидки на газ, экономя дополнительные 5 байт по 200 газа за байт при их развертывании. Если вы собираетесь играть с газовой системой и заполнять место в хранилище ненужными контрактами, вы можете сделать это правильно! (Эта оптимизация вокруг фабричных контрактов также обсуждается в конце EIP-1167.)

ВААААААААААААУ… запишите меня!

Теперь вы можете сказать:

Хорошо, 0age - ты, чудак, любящий ноль, - думаю, я убежден. Но как я могу получить такой адрес?

Есть несколько способов. Самый простой вариант: по сути, вы "добываете" один адрес.


Адреса генерируются детерминированно, либо из закрытого ключа (см. здесь отличную статью, в которой подробно рассказывается об этом) в случае аккаунтов, принадлежащих внешним пользователям, либо из адреса вызывающего и nonce (счетчик при вызове CREATE, или произвольно предоставленный nonce при вызове CREATE2, одного из опкодов, включенных в Constantinople). В любом случае, все это поступает в хэш keccak256 что означает, что при изменении входных данных результат каждого байта будет не хуже случайного

Нулевые байты встречаются в случайных адресах с частотой, описываемой биноминальным распределением:

Массовая функция вероятности для обобщенного биномиального распределения (бла-бла-бла).

с биномальным коэффициентом выраженным как:

Биномиальный коэффициент. Факториал здесь сильный

В нашем случае вероятность нулевого байта составляет 1 к 256, а в адресе 20 байт. Используя p = (1/256) и k = 20:

То же биномиальное распределение, примененное к адресу с двадцатью байтами

Посмотрите на распределение (каждая строка имеет три порядка величины - один к одному, один к тысяче, один к миллиону и т.д.):

Обратите внимание на логарифмический масштаб оси y.

Вы смотрите на:

  • 92,47% вероятность найти ноль нулевых байтов в адресе
  • 7,25% вероятность найти один нулевой байт.
  • 0,27% вероятность найти два нулевых байта.
  • 0,00635% вероятность найти три нулевых байта.
  • 0,00000106% шанс найти четыре, или примерно один на миллион.

Такие адреса легко достижимы без особых усилий. Однако для поиска адреса с семью нулевыми байтами требуется в среднем 978 054 817 444 попытки - чуть меньше триллиона хэшей - с вероятностью 0,00000000010224% найти все семь в одном месте.

Один взгляд на распределение показывает, насколько сложнее становится дальше. Если вы найдете один с десятью нулевыми байтами, я как-нибудь угощу вас пивом; если вы найдете один с четырнадцатью, NSA угостит вас

Давайте также рассмотрим адреса с ведущими нулевыми байтами. Формула для нахождения адреса с n ведущими нулевыми байтами намного проще:

Вот это уже больше похоже на это - математика с размахом ELI5

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

Довольно жестоко по сравнению с этим, да?

Вероятность найти адрес с четырьмя ведущими нулевыми байтами при одной попытке составляет один к 4 294 967 296. Однако это не означает, что мы можем просто обработать 4,3 миллиарда хэшей и на этом закончить. Вероятность найти адрес с четырьмя ведущими нулевыми байтами в определенном количестве хэшей может быть получена из геометрического распределения, где H обозначает количество хэшей:

Я думал, мы договорились, что покончили с длинными формулами...

Проблема такого распределения, особенно когда у вас достаточно хэш-мощности, чтобы его найти, заключается в его положительном перекосе. Если вам не повезет, вам придется очень долго ждать, пока появится адрес с достаточным количеством нулей. Майнинговые пулы для механизмов консенсуса Proof-of-work являются распространенным ответом на это явление и служат основной целью "сглаживания" длинного хвоста распределения

"Другой путь"

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

Однако есть и другой способ - который, однако, не связан с преимуществами обмена закрытыми ключами - который стал возможен благодаря хардфорку Constantinople. Опкод CREATE2 позволяет нам развертывать адреса контрактов, полученные из sha3(msg.sender ++ salt ++ init_code)[12:] а не из обычного sha3(rlp.encode([msg.sender, nonce])) используемого уже знакомым и любимым нами опкодом CREATE Теперь добавьте к этому фабрику контрактов, создающую обновляемые прокси, например, AdminUpgradeabilityProxy из ZeppelinOS, и мы неожиданно создали механизм, с помощью которого эффективные адреса могут создаваться десятками. Я сказал десятками!

(Стоит помнить, что накладные расходы на использование прокси составят около 1600 газа. Учитывая это, если вам все равно нужен обновляемый прокси, почему бы не выбрать более эффективный?)

Все - даже ничто - имеет свою цену

Давайте проследим за развитием этой мысли. Мы создаем смарт-контракт под названием Pr000xy который позволяет пользователям либо создавать, либо требовать прозрачные прокси-контракты на эффективных адресах. Для того чтобы люди действительно начали передавать хэши смарт-контракту, они должны получить что-то за эти хлопоты. Это означает, что для каждого адреса должна быть установлена справедливая цена - в идеале такая, которая будет соответствовать фактической вероятности нахождения адреса. Мы также хотим принять во внимание общее количество нулевых байтов и общее количество ведущих нулевых байтов (но давайте оставим ведущие нули в стороне)

Оказывается, справедливую цену лучше всего определять не по шансам найти определенное количество нулей. Лучше назначать цену за адрес, исходя из вероятности того, что в нем найдется хотя бы определенное количество нулей. Чтобы определить это, сначала вычислите кумулятивную функцию распределения (или сумму, для тех, кто предпочитает говорить о таких вещах без пафоса) всех шансов найти определенное количество нулей до одного меньше, чем нужное нам число. Затем вы просто вычитаете это число из единицы и получаете результат. Это также известно как функция выживания данного биномиального распределения

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

Объединение двух распределений таким образом дает вероятность нахождения z или более нулевых байтов и s начальных байтов по заданному случайному адресу, выраженную следующим образом:

Теперь это становится просто смешно.

Перевод этой цифры в соответствующее количество токенов, которые фабрика прокси должна минтить при создании прокси и сжигать при заявлении прокси, занимает еще два шага: нахождение обратной величины комбинированного коэффициента и корректировка на соответствующий масштабный коэффициент (поскольку большинство адресов слишком обычны, чтобы заслуживать оплаты), который округляет вознаграждение до целочисленных значений. Хорошим масштабным коэффициентом является 256³ (или 2²⁴ для бит-фриков), что делает минимальное вознаграждение широко достижимым - генерация суетного адреса с тремя ведущими нулевыми байтами (т.е. шестью ведущими нулями) займет в среднем 16 777 216 попыток, но это вполне выполнимо на большинстве аппаратных средств, если у вас хватит терпения. Серьезно, попробуйте сами, если не верите мне

Это оставляет нам функцию вознаграждения R, которая дает точную стоимость создания прокси с адресом, содержащим z нулевых байтов и s начальных байтов, вычисляемую следующим образом:

Это красиво... если правильно прищуриться...

Я остановлю вас сейчас, пока вы не сказали:

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

Знаете почему? Потому что вам не придется вычислять его на цепочке. К счастью для всех нас, для этого подойдет таблица поиска, поскольку существует только 200 допустимых входов в функцию. Я избавил вас от головной боли, связанной с устранением ошибок точности плавающей точки при работе с шансами в триллионные доли триллионных долей процента. Пожалуйста (На самом деле все не так уж плохо, вы можете посмотреть мою работу.)

В поисках адресов прокси, которые генерируют ненулевое вознаграждение при создании Pr000xy, вы можете использовать это удобное регулярное выражение, которое даст вам знать, стоит ли адрес каких-либо токенов:

^0{6}|^0{4}((.{2})*(00)){2}|^((.{2})*(00)){5}

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

Синие ячейки - это игра для простых смертных. Белые клетки либо: а) ничего не стоят, либо б) буквально невозможны. Клетки в глубине зеленого лайма: а) стоят безумных денег и б) в буквальном смысле невозможны.
Увеличение до синей области выше, плюс новый цветовой градиент под названием "плазма", который является забавным.
Относительное сравнение размеров начального вознаграждения

Но подождите, это еще не все!

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

Кроме того, использование фабричного шаблона имеет еще одно интересное преимущество: адреса vanity можно безопасно добывать в ненадежной среде. Здесь нет приватных ключей, которые можно украсть, так как адрес, по которому будет передан nonce, предоставляется в качестве аргумента, а связанный с ним приватный ключ никогда не будет доступен машине, которая добывает vanity-адрес. Поздравляем - теперь у вашего теневого сайта есть новый способ тайно добывать криптовалюту в фоновом режиме! (Шутка - не будьте этим парнем).

Если вы построите его (и установите водные горки), они придут.

Assuming there’s a demand for proxies with efficient addresses, the trick to making a factory contract for them really hum lies in incentivizing enough miners to get hashing. An accurate reward function goes a long way, and the ability to mine addresses faster and in untrusted environments helps. However, there are already a lot of highly-optimized tools for finding vanity addresses using traditional means, and going through the factory contract will require paying some extra gas — a touch ironic, considering the ultimate aim. But there’s one more ace in the hole — even if they’re just looking for a vanity address for themselves, they can amortize the cost of finding it, or even make money, by capitalizing on all the addresses they find that would otherwise go to waste.

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

В качестве примера, допустим, новый горячий проект blockchainBuzzwordSoup хочет получить прокси-контракт с адресом, который содержит как минимум восемь нулевых байтов. У них также есть средства, чтобы добыть его самостоятельно, а токены Pr000xy сейчас слишком дороги на их вкус, тем более что для получения нужного адреса требуется 9 миллионов токенов. С имеющейся в их распоряжении хэш-мощностью им потребуется около двух недель™, чтобы найти его - что означает, что это может занять целую вечность, - но, к счастью, они пьют Kool-aid и решают использовать Pr000xy.

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

тобы действительно усилить этот эффект, пользователи, Pr000xy делают предложения о покупке пользовательских vanity адресов. Для этого они вкладывают эфир в контракт фабрики. Затем, когда фабрика создает прокси, соответствующий запросу, любой может вызвать метод для сопоставления действительного предложения с прокси, передавая средства, внесенные в качестве ставки, создателю прокси (с комиссией, предложенной запрашивающим за поиск соответствия) и право собственности на прокси пользователю, сделавшему предложение (сжигая в процессе все необходимые токены, если адрес имеет достаточно нулевых байтов). Чем больше пользователей ищут пользовательские vanity адреса, тем сильнее стимулы для майнеров использовать прокси, и тем быстрее будут найдены пользовательские адреса - добродетельный, но в основном тщетный цикл.

Токеномика

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

Представьте себе токен - назовем его для прикола T000ken, - в котором баланс разрешено хранить только на адресах с четырьмя ведущими нулями. Предположим, что у вас есть адрес, отвечающий этим требованиям, вы можете минтить T000kens, заблокировав Dai или что-то подобное, затем вы можете использовать их, разослать всем своим приятелям с адресами с нулевыми байтами, а в конце концов сжечь их и получить обратно свои Dai, если захотите. Чтобы получить большую часть выгоды, вы лишь немного отклоняетесь от стандартных методов ERC20, упаковывая аргументы, где это возможно, и используя селекторы функций с большим количеством нулевых байтов. (Ха! Вы знали, что этот маленький лакомый кусочек из предыдущей статьи снова всплывет). Приведем несколько конкретных примеров:

  • Наличие четырех нулевых байтов в адресе экономит 64 * 4 = 256 газа, и даже больше, если есть дополнительные нули
  • Упаковка адресов к и от в одно слово позволяет избавиться от 32 пустых байт данных вызова, что дает еще 32 * 4 = 128 экономии газа
  • Замена разрешенного, по умолчанию mapping (address => mapping (address => uint256)), на более эффективноеmapping(<packed addresses> => uint256) экономит целых 66 газа (7/11 часть из которых приходится на дополнительный sha3 необходимый для получения значения из вложенной хэш-таблицы).
  • Использование селекторов функций с тремя нулевыми байтами (можно даже с четырьмя!) экономит 64 * 3 = 192 газа
  • тщательное упорядочивание селекторов функций - они отсортированы в алфавитно-цифровом порядке по селектору - экономит 12 газа за раз


В сравнении со свежей реализацией OpenZeppelin ERC20 implementation, концепция T000ken демонстрирует неплохие результаты: более 3% экономии газа transfer и более 5% при transferFrom. Даже когда эталонная реализация использует те же адреса, T000ken все равно обеспечивает 2,5% скидки при transfer и 4% при transferFrom.

----------------------- Gas Savings Analysis -----------------------
cost to lock: 41499 (in-only: 56499)   free: 65260    total: 1067591.

 1. T000ken using standard ERC20 methods
                               Transfer    TransferFrom    Approve
   Example (regular address)   36518          44113         30308
   T000ken (ERC20 methods)     36027          42777         30045
   Gas savings:                  491           1336           263
   Percentage savings:         1.34%          3.03%         0.87%
   Example (compact address)   36262          43601         30052
   T000ken (ERC20 methods)     36027          42777         30045
   Gas savings:                  235            824             7
   Percentage savings:         0.65%          1.89%         0.02%
   Breakeven txs: (in only)      241             69
   Breakeven txs: (in and out)   455            130
   
 2. T000ken using extra-efficient "sort of like ERC20" methods
                               Transfer     TransferFrom    Approve
    Example (regular address)   36518          44113         30308
    T000ken (efficient)         35308          41829         29566
    Gas savings:                 1210           2284           742
    Percentage savings:         3.31%          5.18%         2.45%
    
 Example (compact address)      36262          43601         30052
    T000ken (efficient)         35308          41829         29566
    Gas savings:                  954           1772           486
    Percentage savings:         2.63%          4.06%         1.62%
    Breakeven txs: (in only)      241             32
    Breakeven txs: (in and out)   455             60