Перейти к основному содержимому

Руководство Airdrop Claiming

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

Claim Machine

к сведению

Как работает практически любое решение для клейма? Давайте подумаем.

Пользователь отправляет некое доказательство, пруф того, что он имеет право на клейм. Разработанный алгоритм решения осуществляет проверку доказательства и при ее успешности отправляет жетоны. В данном случае используется доказательство Меркла, но это вполне могут быть просто подписанные данные или любой другой метод авторизации. Отправка жетонов осуществляется с помощью Jetton wallet и Jetton minter. Также, нужно убедиться, что хитрые пользователи не смогут клеймить дважды – для этого необходим контракт с защитой от двойного списания. И, наверное, заработать немного денег, не так ли? Значит потребуется, по меньшей мере, один кошелек для клейма. Подведем итог:

Дистрибьютор

Принимает доказательство от пользователя, проверяет его, выпускает жетоны. State init: (merkle_root, admin, fee_wallet_address).

Двойное списание

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

Jetton wallet

Jetton wallet, с которого токены будут отправлены дистрибьютором. Jetton minter выходит за рамки этой статьи.

Кошелек для комиссии

Любой тип контракта кошелька.

Архитектура

V1

Один из возможных вариантов реализации:

  • Пользователь отправляет доказательство дистрибьютору
  • Дистрибьютор проверяет доказательство и разворачивает смарт-контракт двойного списания
  • Дистрибьютор передает сообщение контракту двойного списания
  • Контракт двойного списания отправляет claim_ok дистрибьютору, если он не был развернут ранее
  • Дистрибьютор отправляет комиссию за клейм на кошелек для оплаты комиссии
  • Дистрибьютор отпускает жетоны пользователю

Что здесь не так? Похоже, что цикл избыточен.

V2

Линейная структура намного лучше:

  • Пользователь разворачивает контракт двойного списания, который в свою очередь передает доказательство дистрибьютору
  • Дистрибьютор проверяет адрес отправки смарт-контракта двойного списания по state init (distributor_address, user_address?)
  • Дистрибьютор проверяет доказательство и выпускает жетоны. В данном случае индекс пользователя должен быть частью доказательства.
  • Дистрибьютор отправляет комиссию на кошелек для оплаты комиссии

Оптимизация шардов

Хорошо, у нас что-то получается, однако что насчет оптимизации шардов?

Что это такое?

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

Идентификация адресов, которые мы можем контролировать

Адрес дистрибьютора

Мы полностью контролируем данные дистрибьютора, поэтому должны иметь возможность поместить их в любой шард. Как это сделать? Помните, что адрес смарт-контракта определяется его состоянием. Нужно использовать некоторые поля данных контракта в качестве nonce и продолжать попытки до тех пор, пока не получим желаемый результат. Примером хорошего nonce в реальных контрактах может быть (subwalletId/publicKey) для смарт-контракта кошелька. Любое поле, которое можно изменить после развертывания или которое не влияет на логику контракта (как subwalletId), подойдет для этого. Можно даже создать неиспользуемое поле специально для этой цели, как это делает vanity-contract.

Адрес Jetton wallet

Мы не можем контролировать адрес полученного jetton wallet напрямую. Однако, если мы контролируем адрес дистрибьютора, то можем подобрать его таким образом, чтобы jetton wallet для дистрибьютора оказался в том же шарде. Но как это сделать? Для этого существует данная библиотека! В настоящее время она поддерживает только кошельки, но добавить поддержку произвольных контрактов достаточно просто. Посмотрите, как это сделано для Highload-кошелька V3.

Смарт-контракт двойное списание

Контракт двойного списания должен быть уникальным для каждого доказательства, поэтому сможем ли мы настроить его на шарде? Если подумать, то это зависит от структуры доказательства. Первое, что приходит на ум, это та же структура, что и у mintless jetton:

_ amount:Coins start_from:uint48 expired_at:uint48 = AirdropItem;

_ _(HashMap 267 AirdropItem) = Airdrop;

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

_ amount:Coins start_from:uint48 expired_at:uint48 nonce:uint64 = AirdropItem;

_ _(HashMap 267 AirdropItem) = Airdrop;

или даже:

_ amount:Coins start_from:uint48 expired_at:uint48 addr_hash: uint256 = AirdropItem;

_ _(HashMap 64 AirdropItem) = Airdrop;

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

Адрес пользователя

Очевидно, что мы не контролируем адреса пользователей, не так ли? Да, НО мы можем сгруппировать их таким образом, чтобы шард пользовательских адресов совпадал с шардом дистрибьюторов. В таком случае каждый дистрибьютор будет обрабатывать merkle root, состоящий исключительно из пользователей его шарда.

Резюме

Мы можем поместить double_spend -> dist -> dist_jetton часть цепочки в один шард. Для других шардов останется только dist_jetton -> user_jetton -> user_wallet.

Как же развернуть такую установку

Одно из требований заключается в том, чтобы контракт дистрибьютора имел обновляемый merkle root. Давайте выполним по шагам:

  • Разверните смарт-контракт дистрибьютора в каждом шарде (0-15) – в пределах тех же шардов, что и их jetton wallet, используя начальный merkle_root в качестве nonce
  • Сгруппируйте пользователей по шардам
  • Для каждого пользователя найдите такой индекс, чтобы контракт двойного списания (distributor, index) оказался в том же шарде, что и адрес пользователя
  • Сгенерируйте merkle roots с индексами из шага выше
  • Обновите дистрибьюторов в соответствии с merkle roots

Теперь все должно быть в порядке!

V3

  • Пользователь разворачивает контракт двойного списания в одном шарде, используя настройку индекса
  • Дистрибьютор в шарде пользователя проверяет адрес отправки двойного списания по state init (distributor_address, index)
  • Дистрибьютор отправляет комиссию на кошелек для оплаты комиссии
  • Дистрибьютор проверяет доказательство и выпускает жетоны через jetton wallet на том же шарде. В данном случае индекс пользователя должен быть частью доказательства

Какой недостаток у данной структуры? Давайте посмотрим внимательно. Правильно! Существует только один кошелек для комиссии – таким образом сборы комиссий скапливаются в очередь на одном шарде. Это могло бы стать катастрофой! (Интересно, случалось ли такое в реальности?)

V4

  • То же самое, что и в V3, но теперь 16 кошельков для получения комиссии, каждый из которых находится в том же шарде, что и его дистрибьютор
  • Придется сделать адрес кошелька для комиссии обновляемым

Как вам теперь? Выглядит хорошо.

Что дальше?

Мы всегда можем пойти еще дальше. Ознакомьтесь с кастомным смарт-контрактом jetton wallet, который имеет встроенную оптимизацию шардов. В результате пользовательский jetton wallet оказывается на том же шарде, что и пользователь, с вероятностью 87%. Но это все еще довольно-таки неизведанная территория, так что вам придется действовать самостоятельно. Удачи с TGE!