Оптимизация шардов в TON
Основы архитектуры
TON разработан для параллельной обработки множества транзакций. Эта возможность основана на парадигме бесконечного шардинга. Это означает, что как только нагрузка на группу валидаторов приближается к пределу их пропускной способности, они разделяются, шардируются. В последствии две группы валидаторов будут обрабатывать общую нагрузку независимо и параллельно. Это разделение происходит детерминированно, и то, к какой группе будут относится обрабатываемые транзакции, зависит от адреса контракта, связанного с этой транзакцией. Адреса, которые находятся близко друг к другу (имеют одинаковый префикс), будут обрабатываться в одной группе, шарде.
Когда сообщение отправляется из одного контракта в другой, есть два возможных сценария: либо оба контракта будут находиться в одном шарде, либо в разных. В первом случае текущая группа уже имеет все необходимые данные и может немедленно приступить к обработке сообщения. В ином случае сообщение должно быть направлено из одной группы в другую. Чтобы избежать потери сообщений или двойной обработки, необходим корректный учет. Это делается путем регистрации очереди исходящих сообщений из шарда отправителя в блоке мастерчейна, а затем шард получателя должен явно подтвердить, что он обработал эту очередь. Такие накладные расходы замедляют доставку сообщений между шардами, так как между блоком, в который было отправлено сообщение, и блоком, в котором оно было получено, должен быть как минимум один блок мастерчейна. Эта задержка обычно составляет около 12-13 секунд.
Поскольку транзакции на одном счете всегда обрабатываются в одном шарде, скорость транзакций в секунду (TPS) для одного счета ограничена. Это означает, что при разработке архитектуры для нового протокола массового масштабирования следует избегать центральных точек. Кроме того, если цепочка транзакций следует по одному и тому же маршруту, то шардирование ее не ускорит, так как ограничение TPS для каждого контракта в цепочке будет одинаковым. Однако из-за отсутствия задержки доставки общее время обработки цепочки будет выше.
В системе массового масштабирования компромисс между задержкой и пропускной способностью становится тем моментом, который отличает хорошие протоколы от отличных.
Шардировать или не шардировать
Чтобы улучшить пользовательский опыт и время обработки, разработчику протоколов необходимо понимать, какие части системы могут обрабатываться параллельно и, следовательно, для повышения пропускной способности должны быть шардированы. А также какие части должны быть строго последовательны и, следовательно, при помещении их в один шард будут работать с наименьшей задержкой.
Отличным примером оптимизации пропускной способности являются жетоны. Поскольку переходы из A в B и из C в D не зависят друг от друга, их можно обрабатывать параллельно. Распределив все jetton-кошельки равномерно и случайно по адресному пространству, мы сможем добиться идеального баланса нагрузки по блокчейну. Что в свою очередь позволит достичь пропускной способности в сотни переводов в секунду (тысячи в будущем) с целевой задержкой.
Верно и обратное – если другой смарт-контракт А, который имеет дело с жетоном Х, совершает произвольную операцию, когда получает этот жетон (а контракт jetton-кошелька A — это B), то тогда размещение контракта A и его кошелька B в разных шардах не увеличит пропускную способность. В самом деле, каждый входящий перевод будет проходить через одну и ту же цепочку адресов, и каждый адрес будет узким местом. В этом случае целесообразно переместить контракты A и B в один шард, тем самым уменьшив общее время цепочки.
Практические выводы для разработчиков смарт-контрактов
Если у вас есть один смарт-контракт, отвечающий за бизнес-логику, попробуйте развернуть несколько подобных контрактов, чтобы воспользоваться средствами параллелизации TON. Если это невозможно сделать и ваш смарт-контракт взаимодействует с предопределенным набором других смарт-контрактов (скажем, jetton-кошельков), то подумайте о том, чтобы поместить их всех в один шард. Часто это можно сделать вне сети (путем ввода конкретных адресов контрактов, чтобы все нужные jetton-кошельки имели соседние адреса), иногда такой подход допускается и внутри блокчейна.
Ожидается, что подобные улучшения производительности узлов и сети увеличат пропускную способность одного шарда, а также сократят задержку доставки. Вы так же должны их учитывать, если вы планируете делать приложение массовым. По мере присоединения большего количества пользователей, оптимизация шардов будет приобретать все большее значение. В конечном итоге это станет решающим фактором – пользователи всегда будут выбирать наиболее удобное для них приложение, то есть приложение с меньшей задержкой. Поэтому не откладывайте оптимизацию шардов вашего приложения, рассчитывая на глобальное улучшение сети. Сделайте это сейчас! Во многих случаях это может быть даже важнее оптимизации газа.
Практические выводы для сервисов
Депозиты
Если вы ожидаете, что скорость пополнения счета превысит, скажем, 30 переводов в секунду, желательно иметь несколько адресов, которые позволят принимать пополнения параллельно, существенно увеличивая пропускную способность. Если вам известен адрес, с которого пользователь будет вносить депозит, например, через транзакцию в TON Connect, тогда вам стоит выбрать адрес, наиболее близкий к адресу кошелька пользователя. Готовый к использованию код на Typescript для выбора ближайшего адреса может выглядеть так:
import { Address } from '@ton/ton';
function findMatchingBits (a: number, b: number, start_from: number) {
let bitPos = start_from;
let keepGoing = true;
do {
const bitCount = bitPos + 1;
const mask = (1 << (bitCount)) - 1;
const shift = 8 - bitCount;
if(((a >> shift) & mask) == ((b >> shift) & mask)) {
bitPos++;
}
else {
keepGoing = false;
}
} while(keepGoing && bitPos < 7);
return bitPos;
}
function chooseAddress(user: Address, contracts: Address[]) {
const maxBytes = 32;
let byteIdx = 0;
let bitIdx = 0;
let bestMatch: Address | undefined;
if(user.workChain !== 0) {
throw new TypeError(`Only basechain user address allowed:${user}`);
}
for(let testContract of contracts) {
if(testContract.workChain !== 0) {
throw new TypeError(`Only basechain deposit address allowed:${testContract}`);
}
if(byteIdx >= maxBytes) {
break;
}
if(byteIdx == 0 || testContract.hash.subarray(0, byteIdx).equals(user.hash.subarray(0, byteIdx))) {
let keepGoing = true;
do {
if(keepGoing && testContract.hash[byteIdx] == user.hash[byteIdx]) {
bestMatch = testContract;
byteIdx++;
bitIdx = 0;
if(byteIdx == maxBytes) {
break;
}
}
else {
keepGoing = false;
if(bitIdx < 7) {
const resIdx = findMatchingBits(user.hash[byteIdx], testContract.hash[byteIdx], bitIdx);
if(resIdx > bitIdx) {
bitIdx = resIdx;
bestMatch = testContract;
}
}
}
} while(keepGoing);
}
}
return {
match: bestMatch,
prefixLength: byteIdx * 8 + bitIdx
}
}
Если вы ожидаете депозиты жетонов, в дополнение к созданию нескольких адресов депозита, желательно оптимизировать эти адреса по шардам: выбирать такие адреса, чтобы каждый адрес депозита находился в том же шарде, что и его jetton-wallet. Генератор для таких адресов можно найти здесь. Выбор ближайшего адреса к пользователю также будет целесообразным.
Вывод средств
То же самое относится и к выводу средств; если вам нужно отправлять большое количество переводов в секунду, желательно иметь несколько адресов отправки и оптимизировать их с помощью jetton-wallets, если необходимо.
Оптимизация шардов 101
Описание шардов в терминологии Web 2
Блокчейн TON, как и любой другой блокчейн, является сетью, поэтому имеет смысл попытаться объяснить это в терминах сети web2 (ipv4).
Endpoint
В общей сети endpoint — это физическое устройство, в блокчейне endpoint — это смарт-контракт.
Шарды
В логике web2, шард — это не более чем подсеть. Единственное отличие с этой точки зрения в том, что у IPv4 32-битная схема адресации, а у TON — 256-битная. Таким образом, префикс шарда в адресе контракта — это часть адреса контракта, которая идентифицирует группу валидаторов, которые будут вычислять результат входящего сообщения. С точки зрения сети, очевидно, что запрос в том же шарде сети будет обрабатываться быстрее, чем тот, который маршрутизируется в другое месте, верно? Это аналогично использованию CDN для размещения контента ближе к конечным пользователям, в свою очередь в TON мы развертываем контракт ближе к конечным пользователям.
Если нагрузка на шард превышает определенный уровень, шард разделяется. Цель состоит в том, чтобы предоставить выделенные вычислительные ресурсы загружаемому контракту и изолировать его влияние на всю сеть. Максимальная длина префикса шарда составляет всего 4 бита, поэтому блокчейн может быть разделен максимум на 16 шардов с префиксом 0 до 15.
Проблемы при оптимизации шардов
Давайте перейдем к практике
Проверка принадлежности двух адресов одному шарду
Поскольку мы знаем, что префикс шарда равен максимум 4 битам, фрагмент кода для проверки может выглядеть следующим образом:
import { Address } from '@ton/core';
const addressA = Address.parse(...);
const addressB = Address.parse(...);
if((addressA.hash[0] >> 4) == (addressb.hash[0] >> 4)) {
console.log("Same shard");
} else {
console.log("Nope");
}
Самый простой способ проверить шард адреса — посмотреть на необработанную форму.
Для этого можно использовать страницу адреса.
Давайте проверим адрес USDT, например: EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs
.
Вы увидите 0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe
как необработанное представление, а первые 4 бита по сути я вляются первым шестнадцатеричным символом - b
.
Теперь мы знаем, что минтер USDT находится в шарде b
(шестнадцатеричном) или 11
(десятичном), если хотите.
Как развернуть контракт на определенном шарде
Чтобы понять, как это работает, нужно понять, как адрес контракта зависит от его кода и данных.
По сути, это SHA256 кода и данных во время развёртывания.
Зная это, единственный способ развернуть контракт с тем же кодом в другом шарде — это корректировка начальных данных. Поле данных, которое используется для изменения результирующего адреса контракта, называется nonce.Для таких целей можно использовать любое поле, которое: доступно для безопасного обновления после развертывания или не оказывает прямого влияния на выполнение контракта.
Одним из первых известных контрактов, в которых использовался этот принцип, является vanity contract.
В нём есть поле данных salt
, единственное назначение которого — подбор собственного значения, приводящее в итоге к желаемому шаблону адреса.
Размещение контракта в определенном шарде выполняется точно так же, за исключением того, что префикс, который нужно сопоставить, намного короче.
Одним из самых простых примеров, с которого можно начать, будет контракт кошелька.
- Создание кошелька для другого шарда, статья описывает сценарий, когда публичный ключ используется как nonce для помещения кошелька в определенный шард.
- Другие примеры - turbo-wallet использующий subwalletId для тех же целей. Вы можете довольно быстро расширить интерфейс ShardedContract с помощь ю вашего конструктора контрактов, чтобы сделать его шардированным.
Решения для массового распределения жетонов
Если вам нужно распределить жетоны среди десятков/сотней тысяч или миллионов пользователей, прочитайте эту статью. Мы предлагаем вам присмотреться к существующим, проверенным в бою сервисам. Некоторые из них имеют глубокую оптимизацию, что делает их не только преднастроенными под шардирование, но и существенно более дешевыми относительно самописных решений:
- Mintless Jettons: Когда вам нужно распределить жетоны во время события генерации токенов (TGE), вы можете разрешить пользователям запрашивать предопределенный эирдроп непосредственно из контракта jetton-wallets. Он дешевый, не требует дополнительных транзакций и доступен по запросу (только пользователи, которым нужно потратить жетоны сейчас, смогут их получить). [ССЫЛКА]
- Решение от Tonapi для массовой отправки жетонов: позволяет распределять существующие жетоны путем прямой отправки в кошельки пользователей. Проверено в бою Notcoin и DOGS (несколько миллионов переводов каждый), оптимизировано для уменьшения задержки, пропускной способности и затрат. Массовая отправка жетонов
- Решение TokenTable для децентрализованного выпуска: позволяет пользователям запрашивать жетоны из определенных транзакций требования (пользователи платят за комиссию). Проверено в бою Avacoin и DOGS (несколько миллионов переводов), оптимизировано для увеличения пропускной способности и затрат. Введение