Это - перевод соответствующей статьи из блога TON. Я читал статьи, пытаясь понять, какие идеи стоят за TON, и чем он отличается от других блокчейнов. Мне показалась интересной идея таких распределённых смарт-контрактов, и я решил поделиться ей с русскоязычной аудиторией. А заодним и попробовать себя в роли переводчика :)С этого момента начинается чистый перевод, без каких-либо моих комментариев. Главная идея TON - принести блокчейн массовому пользователю. Сколько людей в мире уже попробовали блокчейны на данный момент? Статистика Ethereum говорит о нескольких миллионах. Даже если мы предположим, что это 50 миллионов пользователей — это будет довольно щедрой цифрой. Как увеличить это число до 1 миллиарда? Текущая версия Ethereum выполняет около 1 миллиона транзакций в день в пике своей активности. Ещё в 2016 году Telegram доставлял 15 миллиардов сообщений в день. Такой объем данных привёл к архитектурным решениям, которые отразились на дизайн-решениях блокчейна TON. Увеличение масштабирования системы в 10000 раз в норме недостижимо простыми улучшениями протокола. Для этого нужны радикальные изменения в подходе. Шардинг — вполне зрелая концепция, которая возникла в мире баз данных. Грубо говоря, шардинг позволяет горизонтально масштабировать данные, деля их на независимые куски и распределяя по “шардам”. Это — ключ к трансформации мира от простых данных к большим данным. Когда данные становятся слишком большими чтобы с ними работать стандартными методами, не существует другого метода работы с ними, кроме как разбить их на части. TON не первым использовал шардинг в блокчейнах. Ethereum 2.0 поддерживает фиксированное количество шардов — до 64. Подход TON радикально другой не просто потому, что число шардов больше. Он другой концептуально: Число шардов не фиксировано — TON поддерживает добавление такого количества шардов, которое необходимо в данный момент, с верхней границей в 2^60 (на каждый воркчейн). Это число на практике безгранично. Его хватит, чтобы у каждого человека на земле было по ~100 миллионов шардов. Число шардов динамично. TON поддерживает автоматическое деление шардчейна на два, когда нагрузка слишком высока, и слияние их в один, когда она падает. Это единственный способ работать с динамически меняющимися требованиями сети, когда невозможно предсказать нагрузку наперёд. Почитать больше об этих идеях можно в whitepaper TON’а. Попытки фундаментально изменить мир редко даются даром. Чтобы этот радикальный подход сработал, разработчики смарт контрактов должны “дизайнить” смарт контракты несколько иначе. Если у вас есть опыт работы со смарт контрактами в других блокчейнах, архитектура TON может показаться довольно чуждой. В таком случае рекомендую прочитать статью, чтобы облегчить переход. Базовая атомарная единица в блокчейне TON — инстанс смарт контракта. У инстанса смарт контракта есть адрес, код, и ячейки данных (cells) (постоянное хранилище или состояние). Мы называем его атомарной единицей потому, что у него есть синхронный атомарный доступ к его состоянию. А вот коммуникация между инстансами смарт-контрактов в TON не синхронная и не атомарная. Лучше всего представлять смарт контракты в TON как микросервисы. У каждого микросервиса есть доступ ТОЛЬКО к его локальным данным. Коммуникация между микросервисами включает в себя отправку асинхронных собщений по сети. Как знает каждый архитектор, большие системы начинают требовать перехода от монолитной архитектуры к микросервисной. Использование такого распределённого подхода требует определённых усилий, но взамен открывает ряд возможностей. Современный подход к масштабированию опирается на оркестратор (например, Kubernetes) чтобы группировать контейнеризованные микросервисы, и запускать новые инстансы автоматически (автоскейлинг), эффективно распределяя их по доступным машинам. Мне нравится аналогия с Kubernetes, потому что TON делает то же самое. Как только увеличивается нагрузка на конкретный шард-чейн, он делится на два. Но поскольку сами инстансы смарт контрактов атомарны, сами они никогда не делятся. Это означает, что смарт-контракты, которые когда-то находились на одном шард-чейне, в один момент могут оказаться на разных шард-чейнах. Грубо говоря, виртуальная машина TON (TVM), применяет концепцию распределённых микросервисов к монолиту Ethereum (EVM). Популярный вопрос у начинающих системных архитекторов: “насколько большими должны быть микросервисы?” — или другими словами: “когда микросервис слишком “монолитный”, что его пора делить на два?”. На этот вопрос нет чёткого ответа, это искусство. Идея в том, чтобы ассистировать Kubernetes в его задаче. Чем меньше микросервис, тем легче для Kubernetes оптимизировать систему путём инициализации новых инстансов и перемещения ресурсов по мере возникновения потребности. С другой стороны, чем они меньше, тем сложнее для разработчика реализовать сложные механизмы, т.к. взаимодействие становятся слишком асинхронным. Я заметил, что всё то же самое работает для шардирования смарт контрактов в TON. Идея в том, чтобы ассистировать авто-шардинг TON’а в его задаче — делить данные по множеству смарт-контрактов, так, чтобы когда нагрузка возрастает, их можно было переместить на другие шард-чейны эффективно. Но, если шардировать слишком активно, то придётся уже иметь дело с со сложностью, вызванной возросшей асинхронностью. До этого момента статья была довольно теоретической. Теперь хотелось обратиться к практике. Давайте проанализируем пример из реального мира, чтобы понять эту архитектуру. Для примера, мы возьмём смарт-контракт “Жетона” (Jetton). Жетон — это смарт-контракт, который реализует взаимозаменяемые токены. Это TON’овская версия ERC-20 токенов в Ethereum и BEP-20 токенов в Binance Smart Chain. Реализовать токены довольно просто. Нам всего-лишь нужно одно базовое действие — отправка токена (transfer) — которое позволяет владельцу отправлять какое-то количество токена кому-то другому. Также нам потребуется действие эмиссии токенов (mint) — возможность выпустить новые токены в обращение, и его противоположное действие для вывода из обращения (burn). Что насчёт хранилища? Нам также нужно хранить балансы всех пользователей. В Ethereum мы бы использовали mapping, где ключ — это адрес кошелька пользователя, а значение — это баланс. Как архитекторам этого смарт-контракта в сети TON, нам нужно решить, нужно ли, и если нужно, то как, делить данный смарт-контракт на меньшие части, чтобы авто-шардинг работал более эффективно. Что произойдёт, если у нашего Жетона будет 1 миллиард пользователей? Выдержит ли архитектура этот кейс? Давайте попробуем применить подход, описанный выше, чтобы найти “корректное” число шардов для “Жетонов”. Я понимаю, что это слишком теоретизированно. К счастью, существует практический тест, который, как мне кажется, довольно неплохо работает: Если вы заметили, что в вашем смарт-контракте существует любая безграничная структура данных — скорее всего, пора поделить этот смарт-контракт на множество инстансов. Безграничные структуры данных — это массивы и map’ы, которые потенциально могут расти безгранично. В Ethereum нашему смарт-контракту потребовался бы map, содержащий балансы всех пользователей. Данный map может безгранично расти постольку, поскольку число держателей нашего токена также безгранично. Пора применить наше практическое правило. Если мы будем хранить все балансы в одном TON'овском смарт-контракте, у нас будет безграничная структура данных. Значит, у нас появился идеальный кандидат для шардирования! Итак, как будем шардировать? Здесь всё вполне прямолинейно. Если мы не хотим, чтобы все балансы хранились в одном инстансе смарт-контракта, почему бы нам не поделить его так, чтобы баланс каждого пользователя хранился в его собственном инстансе смарт-контракта? Давайте предположим, что наш Жетон — это токен под названием Shiba-Inu, или SHIB. У двух пользователей есть SHIB - Алисон и Бэки. Мы уже говорили о том, что балансы каждого пользователя хранятся в их собственных инстансах контрактов, что означает, что у нас есть два инстанса (дочерних). Нам также необходим ещё один инстанс смарт-контракта (родительский), который бы хранил глобальную информацию о SHIB. Это приводит нас к такой архитектуре: Давайте почитаем код Jetton’а! У команды разработчиков TON’а есть официальная реализация данного стандарта, которую вы можете найти здесь. В репозитории содержится 2 главных смарт-контракта: jetton-minter.fc — Это родительский контракт, который содержит в себе общую информацию о данном токене, такую, как его название. Для каждого Жетона существует только один инстанс данного контракта. Я не вполне понимаю, почему команда разработчиков выбрала название jetton-minter. Я бы предпочёл название jetton-parent. Данный контракт действительно занимается эмиссией (minting), но даже если эмиссия новых токенов в контракте отключена, вам всё ещё нужен данный инстанс, что несколько запутывает. jetton-wallet.fc — Это родительский контракт, который содержит баланс токена для одного конкретного пользователя. Данный смарт-контракт существует в множестве экземпляров, один экземпляр на каждого пользователя. Команда выбрала название jetton-wallet, но я бы предпочёл jetton-child. Если у нашего токена 1.000.000 держателей, это означает, что будет развёрнут ровно 1.000.001 контракт. На этом моменте начинает происходить магия авто-шардинга. Изначально, все контракты будут расположены на одном шард-чейне. Но если пользователи начнут совершать слишком много транзакций, и данный шард-чейн окажется под сильной нагрузкой, TON автоматически разделит их на несколько шард-чейнов. Теоретически, система может продолжать делиться до тех пор, пока каждый контракт не окажется на своём шард-чейне. Это — тот самый секрет, который позволяет блокчейну TON масштабироваться под миллиарды пользователей. Теперь мы понимаем базовую архитектуру. Давайте посмотрим на несколько конкретных сценариев использования. Например, давайте выясним, что происходит, когда один пользователь отправляет токены другому. В сети TON, все взаимодействующие сущности — это всегда инстансы смарт-контрактов. Какие контракты нам нужны? Мы уже встречались с первыми тремя. Их исходный код можно найти в этом репозитории. Что насчёт трёх контрактов справа? Наши юзер-стори будут включать в себя 3 действующих лица: Алисон и Бэки - держатели нашего токена SHIB. И Админ. У Админа особая роль, потому что он — единственный пользователь, который может эмитировать (mint) новые токены SHIB (это то, как новые токены SHIB создаются). Админ — доверенная роль, которая в номе должная быть отозвана (заменена на нулевой адрес) как только токен начнёт торговаться. Для того, чтобы держать общее количество токена в обращении в  строгих рамках. Пользователи в сети TON также представлены как смарт-контракты. Это смарт-контракты кошельков, которые в норме развёрнуты для пользователей такими приложениями, как TonKeeper. Если вы ещё не знакомы с тем, как смарт-контракты кошельков работают в TON, рекомендую почитать пост How TON wallets work and how to access them from JavaScript. Алисон, Бэки, и Админ хранят свои балансы TON’ов на этих кошельках. Эти кошельки никак не завязаны на коде Жетонов. Вот пример реализации подобного кошелька контракта в официальном репозитории TON. Наши юзер-стори будут начинаться с кого-то из наших пользователей (Алисон, в данном случае), который хочет совершить какое-то действие с токеном SHIB. В данном кейсе, Алисон решает отправить какое-то количество токенов Бэки. Алисон открывает её любимое приложение кошелька (например, TonKeeper), и подтверждает данное действие. Как только подтверждение происходит, приложение кошелька отправляет транзакцию контракту кошелька Алисон. Транзакция содержит сообщение (message), предназначенное для контракта-получателя. Сообщения — способ коммуникации между контрактами в TON. Сообщения закодированы как bag of cells, что по своей сути является бинарным форматом упаковки данных. Одно из главных полей сообщения — это 32-х битный integer под названием op, который описывает тип операции данного сообщения. В нашем примере, поскольку Алисон хочет отправить токены, она отправляет своему смарт-контракту SHIB сообщение с op типом transfer. Сообщение закодировано в транзакции, которую Алисон отправляет смарт-контракту её кошелька. Когда смарт-контракт её кошелька верифицирует подпись данной транзакции [код], он пересылает сообщение адресу-получателю, который Алисон указала [код]. Когда то самое сообщение с op типом transfer добирается до своего места назначения [код] — контракта SHIB Алисон, который хранит её баланс — данный контракт обрабатывает сообщение и изменяет стейт в своём хранилище (уменьшает баланс Алисон на указанную сумму [код]). Если контракту нужно взаимодействовать с другими контрактами, он может отправить дополнительные сообщения. В нашем случае, контракт отправит сообщение с op типом internal transfer контракту, содержащему баланс SHIB у Бэки [код]. Когда сообщение с op типом internal transfer достигает своего места назначения — контракта SHIB Бэки, который хранит его баланс — [код], данный контракт обрабатывает сообщение и изменяет свой стейт в хранилище (увеличивает баланс Бэки на сумму, указанную в сообщении [код]). Данный контракт обычно отправляет одно последнее сообщение с op типом excesses обратно смарт-контракту кошелька Алисон, чтобы вернуть весь оставшийся газ [комиссию за перевод] обратно, и уведомить об успешном завершении транзакции [код]. Вот полная последовательность сообщений: Сообщения в TON асинхронны. Мы не знаем, когда они будут обработаны. Существует как шанс, что сообщения будут обработаны в одном блоке, так и шанс, что они будут обработаны в разных блоках. Это означает, что операция перевода может занять некоторое время. Даже если первая транзакция завершится успешно, всё ещё существует шанс, что целый перевод завершится с ошибкой. Что, если получатель SHIB — не обычный человек, а смарт-контракт онлайн-магазина, который должен выполнить какое-то действие, когда он получает оплату? Например, изменить записи DNS так, чтобы они начали указывать на нового владельца. Было бы неплохо, если бы мы могли вызывать данный смарт-контракт с конкретным сообщением. К нашему везению, у сообщения transfer существует поддержка подобного поведения. Изначальный отправитель может указать payload для уведомления, которое будет отправлено на смарт-контракт кошелька получателя токена SHIB. Последовательность действий в данном случае идентична предыдущему кейсу, за исключением последнего шага. Перед отправкой сообщения с op типом excesses, смарт-контракт - держатель баланса SHIB Бэки сначала отправит сообщение с op типом transfer notification контракту кошелька Бэки (фактически, владельцу контракта SHIB Бэки). Эта история звучала бы куда логичнее, если бы мы заменили “Бэки” на онлай-магазин типа “DNS-Superstore”. В таком случае, контракт “DNS-Superstore” получит уведомление, поскольку он — владелец смарт-контракта - держателя SHIB. Данный контракт, при получении сообщения, инициирует смену DNS записей в соответствии с данными, которые были предоставлены в сообщении. Последовательность сообщений: Как нам узнать, какие ещё фичи есть у сообщения transfer? Обычно, сообщения закодированы языком под названием TL-B. Хорошая практика заключается в том, что создатель смарт-контракта должен опубликовать TL-B спецификацию для всех сообщений, с которыми его контракт имеет дело. Вот спецификация сообщения transfer [код]: amount - количество SHIB токенов, которые мы хотим перевести destination - адрес смарт-контракта кошелька Бэки response_destination - адрес получателя excesses (в норме, адрес контракта-кошелька Алисон) forward_payload - payload для “DNS-Superstore” кейса В этом кейсе, Алисон решает сжечь какое-то количество SHIB, которые у неё есть. Сжигание SHIB выводит их из оборота. Для чего это делать? Сжигание снижает общее количество токена в обороте (total supply). Общее количество токена важно, потому что оно помогает вычислить общую капитализацию токена. Сжигание токенов — это как обратный выкуп (байбэк) в мире акций, это повышает их стоимость. Где хранится число с общим количеством токена? Как вы, возможно, догадались, т.к. данное значение - глобальное и общеизвестное, имеет смысл хранить его в нашем родительском смарт-контракте jetton-minter. Чтобы инициализировать сжигание, Алисон отправляет сообщение с op типом burn своему смарт-контракту баланса SHIB. Как всегда, данное сообщение закодировано в транзакции, которую Алисон отправляет своему контракту-кошельку. В свою очередь, контракт-кошелёк проверяет подпись Алисон, и, если подпись корректна, пересылает данное сообщение указанному получателю. Как только сообщение burn достигает своего получателя [код] (смарт-контракт SHIB с балансом Алисон), данный контракт обрабатывает сообщение и модифицирует стейт в своём хранилище (уменьшает баланс Алисон на указанное в сообщении количество amount[код]). Затем контракт отправляет сообщение с op типом burn notification родительскому контракту jetton-minter. Когда сообщение burn notification достигает места назначения [код], контракт jetton-minter его обрабатывает и меняет стейт в своём хранилище (уменьшает значение общего количества токена (total supply) на указанное в сообщении значение). Данный контракт в норме отправляет ещё одно сообщение excesses контракту-кошельку Алисон, чтобы уведомить об успешной операции, и вернуть оставшийся газ [комиссию за транзакцию] [код]. Последовательность сообщений: Родительский контракт позволяет пользователям запрашивать общее количество токена в обращении с помощью соответствующего геттер-метода [код]. Изначально, когда контракт SHIB разворачивается, общее количество токена SHIB в обращении — ноль, соответственно, их ни у кого нет. Как токены создаются? Процесс создания новых токенов называется эмиссией (minting). Эмитировать новые токены может только одна специальная роль - Админ. Админ также может передать свои полномочия любому другому адресу. Хорошая практика заключается в том, чтобы до момента начала торгов передать полномочия Админа на нулевой адрес. Это гарантирует, что никто не сможет выпускать новые токены и раздувать общее количество токена (провоцировать инфляцию). Чтобы инициировать эмиссию, Админ отправляет сообщение с op типом mint на родительский смарт-контракт jetton-minter. Как всегда, через свой контракт-кошелёк. После проверки подписи Админа, контракт-кошелёк пересылает сообщение указанному получателю. Как только сообщение mint достигает своего места назначения [код], родительский контракт jetton-minter, данный контракт обрабатывает сообщение и удостоверяется в том, что сообщение действительно было отправлено Админом [код]. Затем, данный контракт изменяет свой стейт в хранилище (увеличивает общее количество токена в обращении на указанное в сообщении количество [код]). После чего контракт отправляет сообщение с op типом internal transfer контракту, который хранит SHIB баланс Бэки [код]. Когда сообщение internal transfer достигает своего места назначения [код] (контракт SHIB с балансом Бэки), контракт обрабатывает сообщение и изменяет стейт в своём хранилище (увеличит SHIB баланс Бэки на эмитируемое количество токена [код]). Данный контракт в норме отправляет одно последнее сообщение excesses контракту-кошельку Админа, чтобы уведомить об успешной операции mint, и вернуть оставшийся газ [код]. Вот последовательность сообщений: По аналогии с операцией перевода, здесь точно так же можно уведомить получателя SHIB отдельным сообщением, чтобы он мог обработать поступление токенов на счёт. Помните пример с “DNS-Supershop”? Вот последовательность сообщений в данном кейсе: Напомню архитектуру контрактов SHIB: есть один инстанс родительского контракта jetton-minter, и по одному инстансу дочернего контракта jetton-wallet на каждого держателя токена: Понятно, что родительский контракт jetton-minter развёрнут создателем токена SHIB. Но вот что насчёт дочерних контрактов? Кто их разворачивает? Здесь дизайн очень эффективный — дочерний контракт разворачивается только тогда, когда его владелец получает токен SHIB в первый раз. Это может звучать довольно запутанно — ведь получатель может даже не знать, что ему отправили токены SHIB. Если помните описанный выше кейс с отправкой токена, получение токена SHIB инициируется сообщением internal transfer. Если же контракт-получатель данного сообщения ещё не развёрнут, отправитель сообщения должен его развернуть! Вы можете видеть, как это работает в данном коде. Часть сообщения под названием state_init ответственна за разворачивание. Здесь вы можете видеть, как данное значение вычисляется из ячейки (cell) кода дочернего контракта (скомпилированного байт-кода реализации контракта), и изначального стейта ячейки (cell) данных. Т.к. отправитель сообщения internal transfer не знает, развёрнут получатель или нет, он всегда включает часть с разворачиванием в сообщение. TON достаточно умён, чтобы игнорировать часть с разворачиванием, если смарт-контракт уже развёрнут. В кейсах выше мы видели, что весь процесс поделен на множество сообщений. Например, сообщение internal transfer заставляет получателя увеличить его внутренний баланс SHIB. Но что произойдёт, если хакер попробует подделать это сообщение и отправит его на контракт, который хранит его собственный баланс SHIB? Если мы подойдём к разработке недостаточно внимательно, подобная подделка даст хакеру возможность создавать новые токены для себя из чистого воздуха! Чтобы защитить наши контракты от подобных подделок, нам необходимо убедиться, что подобные критически важные сообщения действительно принадлежат валидным отправителям. Вы можете посмотреть на код проверки здесь. Контракт обработает сообщение только в том случае, если оно было отправлено либо родительским контрактом (адрес которого по каким-то причинам назван jetton_master_addresss), либо одним из валидных дочерних контрактов. Это приводит нас к очень интересному вопросу: откуда мы знаем, что очередной рандомный адрес — это адрес валидного дочернего контракта? Одну минуту… А когда Алисон отправляла сообщение контракту Бэки, откуда она брала адрес контракта Бэки? Это очередное элегантное дизайнерское решение в TON: адреса смарт контрактов вычисляются из ячейки кода инициализации смарт-контракта (скомпилированный байт-код реализации смарт-контракта для TVM), и изначального стейта ячейки данных (изначальный стейт его хранилища). Если нам известны эти два значения, мы можем вычислить адрес смарт-контракта ещё до того, как он будет развёрнут. То есть, вычисление адреса детерминировано. Код Jetton содержит функцию, которая умеет вычислять адреса дочерних контрактов из адресов держателей токена.  Вы можете посмотреть на эту функцию здесь. Как вы можете видеть, функция действительно опирается на ячейку кода инициализации дочернего контракта, и его инициализирующую ячейку данных. Довольно непросто понять, почему такой механизм безопасен. Что помешает хакеру развернуть свой вредоносный контракт по адресу одного из “легальных” дочерних контрактов? Чтобы оказаться на “легальном” адресе, вредоносному контракту необходимо содержать ячейку кода инициализации “официального” дочернего контракта — что само по себе ограничивает возможность хакера как-либо добавить вредоносный код к коду контракта. В дополнение, инициализирующая ячейка данных гарантирует, что дочерний контракт будет подчиняться только валидному родительскому контракту jetton-minter, т.к. изначальная ячейка данных содержит его адрес. Как мы помним, процесс перевода разделен на несколько асинхронных частей. Первое сообщение уменьшает баланс SHIB отправителя, а второе сообщение увеличивает баланс SHIB получателя. Всё хорошо, когда всё идёт по плану. Но что будет, если обработка второго сообщения каким-то образом завершится с ошибкой? Большинство машин смарт-контрактов (к примеру, EVM у Ethereum), выполняют транзакции полностью атомарно и синхронно — так что если один из этапов завершается с ошибкой, вся транзакция также откатывается, и стейт не меняется вообще. Такой механизм и в самом деле очень прост для понимания. Но т.к. сообщения в TON не атомарные и не синхронные — у нас нет функции откатывания к изначальному состоянию из коробки. Как же быть? Нам придётся работать с механизмом откатывания самостоятельно. Это делает разработку смарт-контрактов в TON несколько сложнее чем в других средах. Когда обработка сообщения в TON завершается с ошибкой из-за выброшенного исключения, и при этом у сообщения установлен флаг bounce, система автоматически возвращает сообщение обратно отправителю с флагом bounced. Можете почитать спецификацию данного механизма здесь. Давайте вернёмся к примеру выше: обработка сообщения завершается с ошибкой уже после того, как отправитель SHIB  уменьшил свой баланс на заданное число. Чтобы сохранить систему в консистентном состоянии, нам необходимо каким-то образом откатить уменьшение баланса, когда подобная ошибка возникает. Как это должно работать? Если предположить, что второе сообщение было отправлено с флагом bounce, мы можем откатить уменьшение баланса отправителя, когда соответствующее сообщение bounced возвращается обратно. Пример того, как код контракта Jetton проверяет флаг bounced можно посмотреть здесь, а код отмены уменьшения баланса здесь.  Backend developer | Project manager