Оптимизация шардов в 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.