DHT - Расшифровывается, как Distributed Hash Table, и по сути представляет из себя распределенную базу данных ключ-значение, где каждый участник сети может что-то сохранить, например, информацию о себе.
Реализация DHT в TON по своей сути похожа на реализацию Kademlia, который используется в IPFS. Любой участник сети может запустить DHT ноду, сгенерировать ключи и хранить данные. Для этого ему нужно сгенерировать случайный айди и сообщить о себе другим нодам.
Для определения, на какой ноде сохранить данные, используется алгоритм определения "расстояния" между нодой и ключом. Алгоритм прост: берем айди ноды и айди ключа, производим операцию XOR . Чем меньше получилось значение, тем ближе нода. Задача в том, чтобы сохранить ключ на максимально близким к ключу нодам, чтобы другие участники сети могли, используя тот же алгоритм, найти ноду, которая сможет отдать данные по этому ключу.
Разберем пример с поиском ключа, подключимся к любой DHT ноде и установим соединение по ADNL UDP.
Например, мы хотим найти адрес и данные для подключения к RLDP ноде ТОН сайта foundation.ton. Допустим, мы уже получили ADNL адрес этого сайта, выполнив Get метод DNS контракта. ADNL адресом в hex представлении будет 516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174
. Теперь наша задача найти ip, порт и публичный ключ ноды, имеющей этот адрес.
Чтобы это сделать, нам нужно получить ID DHT ключа, сначала соберем сам DHT ключ:
dht.key id:int256 name:bytes idx:int = dht.Key
name
- это тип ключа, для ADNL адресов используется слово address
, а, например, для поиска нод шардчеина - nodes
. Но типом ключа может быть любой массив байтов, зависит от искомого значения.
Заполнив эту схему, получим:
8fde67f6 -- TL ID dht.key
516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174 -- наш искомый ADNL адрес
07 61646472657373 -- тип ключа, слово "address" в виде массива bytes
00000000 -- индекс 0 т.к ключ всего 1
Далее - получим айди ключа, sha256 хеш от сериализованных выше байтов. Это будет b30af0538916421b46df4ce580bf3a29316831e0c3323a7f156df0236c5b2f75
Теперь мы можем приступить к поиску. Для этого нам нужно выполнить запрос, имеющий схему:
dht.findValue key:int256 k:int = dht.ValueResult
key
- это айди нашего DHT ключа, а k
- это "ширина" поиска, чем она меньше тем точнее, но меньше потенциальных нод для опроса. Максимальное k для нод в тоне равно 10, мы можем использовать 6.
Заполним эту структуру, сериализуем и отправим запрос, используя схему adnl.message.query
. Подробнее об этом можно узнать в другой статье.
В ответ мы можем получить:
dht.valueNotFound
- если значение не найдено.dht.valueFound
- если значение найдено на этой ноде.
Если мы получили dht.valueNotFound
, ответ будет содержать список нод, которые известны запрошенной нами ноде и находятся максимально близко запрошенному нами ключу из списка известных ей нод. В этом случае нам нужно подключиться и добавить полученные ноды к списку известных нам. После этого, из списка всех известных нам нод выбрать самую близкую, доступную и еще не опрошенную, и сделать так же же запрос к ней. И так пока мы не попробуем все ноды в выбранном нами диапозоне или пока мы не перестанем получать новые ноды.
Разберем поля ответа подробнее, используемые схемы:
adnl.address.udp ip:int port:int = adnl.Address;
adnl.addressList addrs:(vector adnl.Address) version:int reinit_date:int priority:int expire_at:int = adnl.AddressList;
dht.node id:PublicKey addr_list:adnl.addressList version:int signature:bytes = dht.Node;
dht.nodes nodes:(vector dht.node) = dht.Nodes;
dht.valueNotFound nodes:dht.nodes = dht.ValueResult;
dht.nodes -> nodes
- список DHT нод (массив).
У каждой ноды есть id
, который является ее публичным ключом, обычно pub.ed25519, используется, как ключ сервера для подключения к ноде по ADNL. Также, каждая нода имеет список адресов addr_list:adnl.addressList
, версию и подпись.
Нам нужно обязательно проверять подпись каждой ноды, для этого читаем значение signature
и обнуляем поле (делаем пустым массивом bytes). После - сериализуем TL структуру dht.node
с обнуленной подписью и проверяем signature
, которая была до обнуления. Проверяем на полученных сериализованых байтах, используя ключ из id
. [Пример реализации]
Из списка addrs:(vector adnl.Address)
берем адрес и пробуем установить ADNL UDP соединение, в качестве ключа сервера используем id
, который является публичным ключом.
Чтобы узнать "расстояние" до этой ноды - нам нужно взять айди ключа от ключа из поля id
и проверить расстояние операцией XOR от айди ключа ноды и искомого ключа. Если расстояние достаточно небольшое - мы можем делать тот же запрос на эту ноду. И так далее, пока не найдем значение или не будет больше новых нод.
Ответ будет содержать само значение, полную информацию по ключу и, опционально, подпись.
Разберем поля ответа подробнее, используемые схемы:
adnl.address.udp ip:int port:int = adnl.Address;
adnl.addressList addrs:(vector adnl.Address) version:int reinit_date:int priority:int expire_at:int = adnl.AddressList;
dht.key id:int256 name:bytes idx:int = dht.Key;
dht.updateRule.signature = dht.UpdateRule;
dht.updateRule.anybody = dht.UpdateRule;
dht.updateRule.overlayNodes = dht.UpdateRule;
dht.keyDescription key:dht.key id:PublicKey update_rule:dht.UpdateRule signature:bytes = dht.KeyDescription;
dht.value key:dht.keyDescription value:bytes ttl:int signature:bytes = dht.Value;
dht.valueFound value:dht.Value = dht.ValueResult;
Для начала разберем key:dht.keyDescription
, он представляет из себя полное описание ключа, сам ключ, информацию о том, кто и как может обновлять значение.
key:dht.key
- ключ, должен полностью совпадать с тем, от чего мы брали айди ключа для поиска.id:PublicKey
- публичный ключ владельца записи.update_rule:dht.UpdateRule
- правило обновления записи-
dht.updateRule.signature
- обновлять запись может только владелец приватного ключа,signature
как ключа, так и значения должна быть валидной
-
dht.updateRule.anybody
- обновлять запись могут все,signature
пуста и не проверяется
-
dht.updateRule.overlayNodes
- обновлять ключ могут только ноды из одного оверлея (шарды воркчеина)
После чтения описания ключа, мы действуем в зависимости от updateRule
, для кейса с поиском ADNL адреса тип всегда - dht.updateRule.signature
. Проверяем подпись ключа тем же способом, что и в прошлый раз, делаем подпись пустым массивом байтов, сериализуем и проверяем. После - повторяем то же самое для значения, т.е для всего объекта dht.value
(подпись ключа при этом возвращаем на место).
Используется для ключей, содержащих информацию о других нодах-шардах воркчеина в сети, в значении всегда имеет TL структуру overlay.nodes
. Подпись значения должна быть пустой.
overlay.node id:PublicKey overlay:int256 version:int signature:bytes = overlay.Node;
overlay.nodes nodes:(vector overlay.node) = overlay.Nodes;
Для проверки валидности мы должны проверить все nodes
и для каждой проверить signature
на соответствие ее id
, сериализовав TL структуру:
overlay.node.toSign id:adnl.id.short overlay:int256 version:int = overlay.node.ToSign;
Как мы видим, id надо заменить на adnl.id.short, который является айди ключа id
из оригинальной структуры. После сериализации - сверяем подпись с данными.
В результате мы получаем валидный список нод, которые способны отдать нам информацию о нужной нам шарде воркчеина.
Подписей нет, обновлять может любой, но реального использования я не видел.
Когда все верифицировано и время жизни значения ttl:int
не просрочено, мы можем начинать работать с самим значением, т.е value:bytes
. Для ADNL адреса там внутри должна быть структура adnl.addressList
. В ней будут ip адреса и порты серверов, соответствующих запрошенному ADNL адресу. В нашем случае там скорее всего будет 1 адрес RLDP-HTTP сервиса foundation.ton
. В качестве ключа сервера мы будем использовать публичный ключ id:PublicKey
из информации о ключе DHT.
После установки соединения - мы можем запрашивать страницы сайта, используя протокол RLDP. Задача DHT на этом этапе выполнена.
DHT так же используется для поиска информации о нодах, хранящих данные воркчеинов и их шардов. Процесс такой же, как и при поиске любого ключа, разница лишь в самой сериализации ключа и валидации ответа, эти моменты мы разберем в этом разделе.
Для того, чтобы получить данные, например, мастерчеина и его шарды, нам нужно заполнить TL структуру:
tonNode.shardPublicOverlayId workchain:int shard:long zero_state_file_hash:int256 = tonNode.ShardPublicOverlayId;
Где workchain
в случае мастерчеина будет равен -1, его shard будет равен -922337203685477580, и zero state - хэш нулевого стейта чеина (file_hash), его, как и другие данные, можно взять в глобальном конфиге сети, в поле "validator"
"zero_state": {
"workchain": -1,
"shard": -9223372036854775808,
"seqno": 0,
"root_hash": "F6OpKZKqvqeFp6CQmFomXNMfMj2EnaUSOXN+Mh+wVWk=",
"file_hash": "XplPz01CXAps5qeSWUtxcyBfdAo5zVb1N979KLSKD24="
}
После того, как мы заполнили tonNode.shardPublicOverlayId
, мы сериализуем его и получаем из него айди ключа, путем хеширования (как и всегда).
Полученный айди ключа нам нужно использовать, как name
для заполнения структуры pub.overlay name:bytes = PublicKey
, завернув его в bytes
. Далее сериализуем его, и получаем айди ключа теперь уже из него.
Полученный айди будет ключом для использования в dht.findValue
, а в качестве name
будет слово nodes
. Повторяем основной процесс из предыдущего раздела, все как и в прошлый раз, но updateRule
будет dht.updateRule.overlayNodes.
После валидации - мы получим публичные ключи (id
) нод имеющих информацию о нашем воркчеине и шарде. Чтобы получить ADNL адреса нод - нам нужно сделать из ключей айди (методом хеширования) и для каждого из ADNL адресов повторить процедуру описанную выше, как и с ADNL адресом домена foundation.ton
.
В результате мы получим адреса нод, у которых, если захотим, сможем узнать адреса других нод этого чеина c помощью overlay.getRandomPeers. Также мы сможем получать от этих нод всю информацию о блоках.