Это протокол нижнего уровня, на котором построено все взаимодействие в сети TON, он может работать поверх любого протокола, но чаще всего применяется поверх TCP и UDP. UDP применяется для общения между нодами, а TCP для коммуникации с lite-серверами.
Сейчас мы разберем ADNL работающий поверх TCP и научимся взаимодействовать с лайт-серверами напрямую.
В TCP версии ADNL, в качестве адресов, узлы сети используют публичные ключи ed25519 и устанавливают соединение, используя общий ключ, полученный с помощью процедуры Диффи-Хелмана для эллиптических кривых - ECDH.
Каждый ADNL TCP пакет, кроме хэндшейка, имеет структуру:
- 4 байта размера пакета в little endian (N)
- 32 байта nonce [?]
- (N - 64) байт полезных данных
- 32 байта чексумма SHA256 от nonce и полезных данных
Весь пакет, включая размер, зашифрован AES-CTR. После расшифровки - нужно обязательно проверить, сходится ли чексумма с данными, для проверки нужно просто посчитать чексумму самостоятельно и сравнить результат с тем, что у нас в пакете.
Хэндшейк пакет - исключение, он передается в частично открытом виде и описан в следующей главе.
Для установки соединения нам нужно знать ip, порт и публичный ключ сервера, и сгенерировать свой приватный и публичный ключ ed25519.
Данные публичных серверов такие как ip, порт и ключ можно получить из конфига. Айпи в конфиге в числовом виде, в нормальный вид его можно привести используя, например этот инстурмент. Публичный ключ в конфиге в base64 формате.
Клиент генерирует 160 случайных байт, часть из которых будет использоваться сторонами в качестве основы для AES шифрования.
Из них создаются 2 постоянных AES-CTR шифра, которые будут использоваться сторонами для шифрования/дешифрования сообщений после хэндшейка.
- Шифр A - ключ 0 - 31 байты, iv 64 - 79 байты
- Шифр B - ключ 32 - 63 байты, iv 80 - 95 байты
Шифры применяются в таком порядке:
- Шифр A используется сервером для шифрования отправляемых сообщений.
- Шифр A используется клиентом для дешифрования полученных сообщений.
- Шифр B используется клиентом для шифрования отправляемых сообщений.
- Шифр B используется сервером для дешифрования полученных сообщений.
Для установки соединения клиент должен отправить хэндшейк пакет, содержащий:
- [32 байта] Айди ключа сервера [Подробнее]
- [32 байта] Наш публичный ключ ed25519
- [32 байта] SHA256 хэш от наших 160 байт
- [160 байт] Наши 160 байт в зашифрованом виде [Подробнее]
При получении хэндшейк пакета, сервер проделает те же самые действия у себя, получит ECDH ключ, расшифрует 160 байт и создаст 2 постоянных ключа. Если все получится, сервер ответит пустым ADNL пакетом, без полезных данных, для дешифровки которого (а также последующих) нужно использовать один из постоянных шифров.
С этого момента можно считать соединение установленным.
После того, как мы установили соединение, мы можем приступать к получению информации, для сериализации данных используется язык TL.
Ping пакет оптимально отправлять примерно раз в 5 секунд. Это нужно для поддержания соединения, пока обмен данными не происходит, иначе сервер оборвет соединение.
Ping пакет, как и все остальные, строится по стандартной схеме, описанной выше, и в качестве полезных данных несет идентификатор и айди запроса.
Найдем нужную схему для пинг запроса тут и вычислим айди схемы, как
crc32_IEEEE("tcp.ping random_id:long = tcp.Pong")
. При конвертации в байты с порядком little endian получим 9a2b084d.
Таким образом наш ADNL ping пакет будет выглядеть так:
- 4 байта размера пакета в little endian -> 64 + (4+8) = 76
- 32 байта nonce -> случайные 32 байта
- 4 байта ID TL схемы -> 9a2b084d
- 8 байт айди запроса -> случайное число uint64
- 32 байта чексумма SHA256 от nonce и полезных данных
Отправляем наш пакет и в ответ ждем tcp.pong, random_id
будет равен тому, который мы отправили в ping.
Все запросы, которые направлены на получение информации из блокчеина, обернуты в LiteServer Query схему, которая в свою очередь обернута в ADNL Query схему.
LiteQuery:
liteServer.query data:bytes = Object
, айди df068c79
ADNLQuery:
adnl.message.query query_id:int256 query:bytes = adnl.Message
, айди 7af98bb4
LiteQuery передается внутри ADNLQuery, как query:bytes
, а конечный запрос передается внутри LiteQuery, как data:bytes
.
Теперь, так как мы уже умеем формировать TL пакеты для Lite API, мы можем запросить информацию о текущем блоке мастерчеина TON. Блок мастерчеина используется во многих дальнейших запросах, как входящий параметр, для индикации состояния (момента), в котором нам нужна информация.
Ищем нужную нам TL схему, вычисляем ее айди и строим пакет:
- 4 байта размера пакета в little endian -> 64 + (4+32+(1+4+(1+4+3)+3)) = 116
- 32 байта nonce -> случайные 32 байта
- 4 байта ID ADNLQuery схемы -> 7af98bb4
- 32 байта
query_id:int256
-> случайные 32 байта -
- 1 байт размер массива -> 12
-
- 4 байта ID LiteQuery схемы -> df068c79
-
-
- 1 байт размер массива -> 4
-
-
-
- 4 байта ID getMasterchainInfo схемы -> 2ee6b589
-
-
-
- 3 нулевых байта падинга (выравнивание к 8)
-
-
- 3 нулевых байта падинга (выравнивание к 16)
- 32 байта чексумма SHA256 от nonce и полезных данных
Пример пакета в hex:
74000000 -> размер пакета (116)
5fb13e11977cb5cff0fbf7f23f674d734cb7c4bf01322c5e6b928c5d8ea09cfd -> nonce
7af98bb4 -> ADNLQuery
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id
0c -> размер массива
df068c79 -> LiteQuery
04 -> размер массива
2ee6b589 -> getMasterchainInfo
000000 -> 3 байта падинг
000000 -> 3 байта падинг
ac2253594c86bd308ed631d57a63db4ab21279e9382e416128b58ee95897e164 -> sha256
В ответ мы ожидаем получить liteServer.masterchainInfo, состоящий из last:ton.blockIdExt state_root_hash:int256 и init:tonNode.zeroStateIdExt.
Полученый пакет десериализуется тем же самым образом, что и отправленый, - тот же алгоритм, но в обратную сторону, разве что ответ завернут только в ADNLAnswer.
После расшифровки ответа, получаем пакет вида:
20010000 -> размер пакета (288)
5558b3227092e39782bd4ff9ef74bee875ab2b0661cf17efdfcd4da4e53e78e6 -> nonce
1684ac0f -> ADNLAnswer
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id (идентичен запросу)
b8 -> размер массива
81288385 -> liteServer.masterchainInfo
last:tonNode.blockIdExt
ffffffff -> workchain:int
0000000000000080 -> shard:long
27405801 -> seqno:int
e585a47bd5978f6a4fb2b56aa2082ec9deac33aaae19e78241b97522e1fb43d4 -> root_hash:int256
876851b60521311853f59c002d46b0bd80054af4bce340787a00bd04e0123517 -> file_hash:int256
8b4d3b38b06bb484015faf9821c3ba1c609a25b74f30e1e585b8c8e820ef0976 -> state_root_hash:int256
init:tonNode.zeroStateIdExt
ffffffff -> workchain:int
17a3a92992aabea785a7a090985a265cd31f323d849da51239737e321fb05569 -> root_hash:int256
5e994fcf4d425c0a6ce6a792594b7173205f740a39cd56f537defd28b48a0f6e -> file_hash:int256
000000 -> падинг 3 байта
520c46d1ea4daccdf27ae21750ff4982d59a30672b3ce8674195e8a23e270d21 -> sha256
Мы уже умеем получать блок мастерчеина, значит теперь мы можем вызывать любые методы лайт-сервера. Разберем runSmcMethod - это метод, который вызывает функцию из смарт контракта и возвращает результат. Здесь нам потребуется понять некоторые новые типы данных, такие как TL-B, Cell и BoC.
Для выполнения метода смарт-контракта нам нужно отправить запрос по TL схеме:
liteServer.runSmcMethod mode:# id:tonNode.blockIdExt account:liteServer.accountId method_id:long params:bytes = liteServer.RunMethodResult
И ждать ответ вида:
liteServer.runMethodResult mode:# id:tonNode.blockIdExt shardblk:tonNode.blockIdExt shard_proof:mode.0?bytes proof:mode.0?bytes state_proof:mode.1?bytes init_c7:mode.3?bytes lib_extras:mode.4?bytes exit_code:int result:mode.2?bytes = liteServer.RunMethodResult;
В запросе мы видим такие поля:
- mode:# - uint32 битовая маска того, что мы хотим видеть в ответе, например, result:mode.2?bytes будет присутствовать в ответе только, если бит с индексом 2 равен единице.
- id:tonNode.blockIdExt - наш стейт мастер блока, который мы получили в прошлой главе.
- account:liteServer.accountId - воркчеин и данные адреса смарт контракта.
- method_id:long - 8 байт, в которых пишется crc16 с таблицей XMODEM от имени вызываемого метода и установленый 17й бит [Расчет]
- params:bytes - Stack сериализованый в BoC, содержащий аргументы для вызова метода. [Пример реализации]
Например, нам нужен только result:mode.2?bytes
, тогда наш mode будет равен 0b100, то есть 4. В ответ мы получим:
- mode:# -> то, что и отправляли, - 4.
- id:tonNode.blockIdExt -> наш мастер блок, относительно которого был выполнен метод
- shardblk:tonNode.blockIdExt -> шард блок, в котором находится аккаунт контракта
- exit_code:int -> 4 байта, которые являются кодом выхода при выполнении метода. Если все успешно, то = 0, если нет - равен коду исключения
- result:mode.2?bytes -> Stack сериализованый в BoC, содержащий возвращенные методом значения.
Разберем вызов и получение результата от метода a2
контракта EQBL2_3lMiyywU17g-or8N7v9hDmPCpttzBPE2isF2GTzpK4
:
Код метода на FunC:
(cell, cell) a2() method_id {
cell a = begin_cell().store_uint(0xAABBCC8, 32).end_cell();
cell b = begin_cell().store_uint(0xCCFFCC1, 32).end_cell();
return (a, b);
}
Заполняем наш запрос:
mode
= 4, нам нужен только результат ->04000000
id
= результат выполнения getMasterchainInfoaccount
= воркчеин 0 (4 байта00000000
), и int256 полученный из адреса нашего контракта, то есть 32 байта4bdbfde5322cb2c14d7b83ea2bf0deeff610e63c2a6db7304f1368ac176193ce
method_id
= вычисленый id отa2
->0a2e010000000000
params:bytes
= Наш метод не принимает входных параметров, значит нам нужно передать ему пустой стек (000000
, ячейка 3 байта - стек 0 глубины) сериализованый в BoC ->b5ee9c72010101010005000006000000
-> сериализуем в bytes и получаем10b5ee9c72410101010005000006000000000000
0x10 - размер, 3 байта в конце - падинг.
В ответе получаем:
mode:#
-> не интересенid:tonNode.blockIdExt
-> не интересенshardblk:tonNode.blockIdExt
-> не интересенexit_code:int
-> равен 0, если все успешноresult:mode.2?bytes
-> Stack содержащий возвращенные методом данные в формате BoC, его мы распакуем.
Внутри result
мы получили b5ee9c7201010501001b000208000002030102020203030400080ccffcc1000000080aabbcc8
, это BoC содержащий стек с данными. Когда мы десериализуем его, мы получим ячейку:
32[00000203] -> {
8[03] -> {
0[],
32[0AABBCC8]
},
32[0CCFFCC1]
}
Если мы ее распарсим, то получим 2 значения типа cell, которые возвращает наш FunC метод.
Первые 3 байта корневой ячейки 000002
- это глубина стека, то есть 2. Значит метод вернул нам 2 значения.
Продолжаем парсинг, следующие 8 бит (1 байт) - это тип значения на текущем уровне стека. Для некоторых типов он может занимать 2 байта. Возможные варианты можно посмотреть в схеме. В нашем случае мы имеем 03
, что означает:
vm_stk_cell#03 cell:^Cell = VmStackValue;
Значит тип нашего значения - cell, и, судя по схеме, он хранит само значение, как ссылку. Но, если мы посмотрим на схему хранения элементов стека, -
vm_stk_cons#_ {n:#} rest:^(VmStackList n) tos:VmStackValue = VmStackList (n + 1);
То увидим, что первая ссылка rest:^(VmStackList n)
- это ячейка следующего значения на стеке, а наше значение tos:VmStackValue
идет вторым, значит для получения значения нам нужно читать вторую ссылку, то есть 32[0CCFFCC1]
- это наш первый cell, который вернул контракт.
Теперь мы можем залезть глубже и достать второй элемент стека, проходим по первой ссылке, теперь мы имеем:
8[03] -> {
0[],
32[0AABBCC8]
}
Повторяем тот же процесс. Первые 8 бит = 03
- то есть опять cell. Вторая ссылка - это значение 32[0AABBCC8]
и, так как глубина нашего стека равна 2, мы завершаем проход. Итого, мы имеем 2 значения, возвращенные контрактом, - 32[0CCFFCC1]
и 32[0AABBCC8]
.
Обратите внимание, что они идут в обратном порядке. Точно так же нужно передавать и аргументы при вызове функции - в обратном порядке от того, что мы видим в коде FunC.
Для получения данных о состоянии аккаунта, таких как баланс, код и хранимые данные мы, можем использовать getAccountState. Для запроса нам понадобится свежий мастер блок и адрес аккаунта. В ответ мы получим TL структуру AccountState.
Разберем AccountState TL схему:
liteServer.accountState id:tonNode.blockIdExt shardblk:tonNode.blockIdExt shard_proof:bytes proof:bytes state:bytes = liteServer.AccountState;
id
- это наш мастер блок, относительно которого мы получили данные.shardblk
- блок шарды воркчеина, на котором находится наш аккаунт, относительно которого мы получили данные.shard_proof
- merkle пруф блока шарды.proof
- merkle пруф состояния аккаунта.state
- BoC TLB схемы состояния аккаунта.
Из всех этих данных то, что нам нужно, находится в state
, разберем его.
Например, получим состояние аккаунта TF EQAhE3sLxHZpsyZ_HecMuwzvXHKLjYx4kEUehhOy2JmCcHCT
, state
в ответе будет:
b5ee9c720102350100051e000277c0021137b0bc47669b3267f1de70cbb0cef5c728b8d8c7890451e8613b2d899827026a886043179d3f6000006e233be8722201d7d239dba7d818134001020114ff00f4a413f4bcf2c80b0d021d0000000105036248628d00000000e003040201cb05060013a03128bb16000000002002012007080043d218d748bc4d4f4ff93481fd41c39945d5587b8e2aa2d8a35eaf99eee92d9ba96004020120090a0201200b0c00432c915453c736b7692b5b4c76f3a90e6aeec7a02de9876c8a5eee589c104723a18020004307776cd691fbe13e891ed6dbd15461c098b1b95c822af605be8dc331e7d45571002000433817dc8de305734b0c8a3ad05264e9765a04a39dbe03dd9973aa612a61f766d7c02000431f8c67147ceba1700d3503e54c0820f965f4f82e5210e9a3224a776c8f3fad1840200201200e0f020148101104daf220c7008e8330db3ce08308d71820f90101d307db3c22c00013a1537178f40e6fa1f29fdb3c541abaf910f2a006f40420f90101d31f5118baf2aad33f705301f00a01c20801830abcb1f26853158040f40e6fa120980ea420c20af2670edff823aa1f5340b9f2615423a3534e2a2d2b2c0202cc12130201201819020120141502016616170003d1840223f2980bc7a0737d0986d9e52ed9e013c7a21c2b2f002d00a908b5d244a824c8b5d2a5c0b5007404fc02ba1b04a0004f085ba44c78081ba44c3800740835d2b0c026b500bc02f21633c5b332781c75c8f20073c5bd0032600201201a1b02012020210115bbed96d5034705520db3c8340201481c1d0201201e1f0173b11d7420c235c6083e404074c1e08075313b50f614c81e3d039be87ca7f5c2ffd78c7e443ca82b807d01085ba4d6dc4cb83e405636cf0069006031003daeda80e800e800fa02017a0211fc8080fc80dd794ff805e47a0000e78b64c00015ae19574100d56676a1ec40020120222302014824250151b7255b678626466a4610081e81cdf431c24d845a4000331a61e62e005ae0261c0b6fee1c0b77746e102d0185b5599b6786abe06fedb1c68a2270081e8f8df4a411c4605a400031c34410021ae424bae064f613990039e2ca840090081e886052261c52261c52265c4036625ccd88302d02012026270203993828290111ac1a6d9e2f81b609402d0015adf94100cc9576a1ec1840010da936cf0557c1602d0015addc2ce0806ab33b50f6200220db3c02f265f8005043714313db3ced542d34000ad3ffd3073004a0db3c2fae5320b0f26212b102a425b3531cb9b0258100e1aa23a028bcb0f269820186a0f8010597021110023e3e308e8d11101fdb3c40d778f44310bd05e254165b5473e7561053dcdb3c54710a547abc2e2f32300020ed44d0d31fd307d307d33ff404f404d10048018e1a30d20001f2a3d307d3075003d70120f90105f90115baf2a45003e06c2170542013000c01c8cbffcb0704d6db3ced54f80f70256e5389beb198106e102d50c75f078f1b30542403504ddb3c5055a046501049103a4b0953b9db3c5054167fe2f800078325a18e2c268040f4966fa52094305303b9de208e1638393908d2000197d3073016f007059130e27f080705926c31e2b3e63006343132330060708e2903d08308d718d307f40430531678f40e6fa1f2a5d70bff544544f910f2a6ae5220b15203bd14a1236ee66c2232007e5230be8e205f03f8009322d74a9802d307d402fb0002e83270c8ca0040148040f44302f0078e1771c8cb0014cb0712cb0758cf0158cf1640138040f44301e201208e8a104510344300db3ced54925f06e234001cc8cb1fcb07cb07cb3ff400f400c9
Распарсим этот BoC и получим
большую ячейку
473[C0021137B0BC47669B3267F1DE70CBB0CEF5C728B8D8C7890451E8613B2D899827026A886043179D3F6000006E233BE8722201D7D239DBA7D818130_] -> {
80[FF00F4A413F4BCF2C80B] -> {
2[0_] -> {
4[4_] -> {
8[CC] -> {
2[0_] -> {
13[D180],
141[F2980BC7A0737D0986D9E52ED9E013C7A218] -> {
40[D3FFD30730],
48[01C8CBFFCB07]
}
},
6[64] -> {
178[00A908B5D244A824C8B5D2A5C0B5007404FC02BA1B048_],
314[085BA44C78081BA44C3800740835D2B0C026B500BC02F21633C5B332781C75C8F20073C5BD00324_]
}
},
2[0_] -> {
2[0_] -> {
84[BBED96D5034705520DB3C_] -> {
112[C8CB1FCB07CB07CB3FF400F400C9]
},
4[4_] -> {
2[0_] -> {
241[AEDA80E800E800FA02017A0211FC8080FC80DD794FF805E47A0000E78B648_],
81[AE19574100D56676A1EC0_]
},
458[B11D7420C235C6083E404074C1E08075313B50F614C81E3D039BE87CA7F5C2FFD78C7E443CA82B807D01085BA4D6DC4CB83E405636CF0069004_] -> {
384[708E2903D08308D718D307F40430531678F40E6FA1F2A5D70BFF544544F910F2A6AE5220B15203BD14A1236EE66C2232]
}
}
},
2[0_] -> {
2[0_] -> {
323[B7255B678626466A4610081E81CDF431C24D845A4000331A61E62E005AE0261C0B6FEE1C0B77746E0_] -> {
128[ED44D0D31FD307D307D33FF404F404D1]
},
531[B5599B6786ABE06FEDB1C68A2270081E8F8DF4A411C4605A400031C34410021AE424BAE064F613990039E2CA840090081E886052261C52261C52265C4036625CCD882_] -> {
128[ED44D0D31FD307D307D33FF404F404D1]
}
},
4[4_] -> {
2[0_] -> {
65[AC1A6D9E2F81B6090_] -> {
128[ED44D0D31FD307D307D33FF404F404D1]
},
81[ADF94100CC9576A1EC180_]
},
12[993_] -> {
50[A936CF0557C14_] -> {
128[ED44D0D31FD307D307D33FF404F404D1]
},
82[ADDC2CE0806AB33B50F60_]
}
}
}
}
},
872[F220C7008E8330DB3CE08308D71820F90101D307DB3C22C00013A1537178F40E6FA1F29FDB3C541ABAF910F2A006F40420F90101D31F5118BAF2AAD33F705301F00A01C20801830ABCB1F26853158040F40E6FA120980EA420C20AF2670EDFF823AA1F5340B9F2615423A3534E] -> {
128[DB3C02F265F8005043714313DB3CED54] -> {
128[ED44D0D31FD307D307D33FF404F404D1],
112[C8CB1FCB07CB07CB3FF400F400C9]
},
128[ED44D0D31FD307D307D33FF404F404D1],
40[D3FFD30730],
640[DB3C2FAE5320B0F26212B102A425B3531CB9B0258100E1AA23A028BCB0F269820186A0F8010597021110023E3E308E8D11101FDB3C40D778F44310BD05E254165B5473E7561053DCDB3C54710A547ABC] -> {
288[018E1A30D20001F2A3D307D3075003D70120F90105F90115BAF2A45003E06C2170542013],
48[01C8CBFFCB07],
504[5230BE8E205F03F8009322D74A9802D307D402FB0002E83270C8CA0040148040F44302F0078E1771C8CB0014CB0712CB0758CF0158CF1640138040F44301E2],
856[DB3CED54F80F70256E5389BEB198106E102D50C75F078F1B30542403504DDB3C5055A046501049103A4B0953B9DB3C5054167FE2F800078325A18E2C268040F4966FA52094305303B9DE208E1638393908D2000197D3073016F007059130E27F080705926C31E2B3E63006] -> {
112[C8CB1FCB07CB07CB3FF400F400C9],
384[708E2903D08308D718D307F40430531678F40E6FA1F2A5D70BFF544544F910F2A6AE5220B15203BD14A1236EE66C2232],
504[5230BE8E205F03F8009322D74A9802D307D402FB0002E83270C8CA0040148040F44302F0078E1771C8CB0014CB0712CB0758CF0158CF1640138040F44301E2],
128[8E8A104510344300DB3CED54925F06E2] -> {
112[C8CB1FCB07CB07CB3FF400F400C9]
}
}
}
}
}
},
114[0000000105036248628D00000000C_] -> {
7[CA] -> {
2[0_] -> {
2[0_] -> {
266[2C915453C736B7692B5B4C76F3A90E6AEEC7A02DE9876C8A5EEE589C104723A1800_],
266[07776CD691FBE13E891ED6DBD15461C098B1B95C822AF605BE8DC331E7D45571000_]
},
2[0_] -> {
266[3817DC8DE305734B0C8A3AD05264E9765A04A39DBE03DD9973AA612A61F766D7C00_],
266[1F8C67147CEBA1700D3503E54C0820F965F4F82E5210E9A3224A776C8F3FAD18400_]
}
},
269[D218D748BC4D4F4FF93481FD41C39945D5587B8E2AA2D8A35EAF99EEE92D9BA96000]
},
74[A03128BB16000000000_]
}
}
Теперь нам нужно распарсить ячейку в соответствии с TL-B структурой:
account_none$0 = Account;
account$1 addr:MsgAddressInt storage_stat:StorageInfo
storage:AccountStorage = Account;
Наша структура ссылается на другие, такие как:
anycast_info$_ depth:(#<= 30) { depth >= 1 } rewrite_pfx:(bits depth) = Anycast;
addr_std$10 anycast:(Maybe Anycast) workchain_id:int8 address:bits256 = MsgAddressInt;
addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
storage_info$_ used:StorageUsed last_paid:uint32 due_payment:(Maybe Grams) = StorageInfo;
storage_used$_ cells:(VarUInteger 7) bits:(VarUInteger 7) public_cells:(VarUInteger 7) = StorageUsed;
account_storage$_ last_trans_lt:uint64 balance:CurrencyCollection state:AccountState = AccountStorage;
currencies$_ grams:Grams other:ExtraCurrencyCollection = CurrencyCollection;
var_uint$_ {n:#} len:(#< n) value:(uint (len * 8)) = VarUInteger n;
var_int$_ {n:#} len:(#< n) value:(int (len * 8)) = VarInteger n;
nanograms$_ amount:(VarUInteger 16) = Grams;
account_uninit$00 = AccountState;
account_active$1 _:StateInit = AccountState;
account_frozen$01 state_hash:bits256 = AccountState;
Как мы видим, ячейка содержит очень много данных, но мы разберем основные кейсы и получение баланса, остальное вы сможете разрбрать аналогичным способом.
Начнем парсинг. В данных корневой ячейки мы имеем:
C0021137B0BC47669B3267F1DE70CBB0CEF5C728B8D8C7890451E8613B2D899827026A886043179D3F6000006E233BE8722201D7D239DBA7D818130_
Переведем в бинарный вид и получим:
11000000000000100001000100110111101100001011110001000111011001101001101100110010011001111111000111011110011100001100101110110000110011101111010111000111001010001011100011011000110001111000100100000100010100011110100001100001001110110010110110001001100110000010011100000010011010101000100001100000010000110001011110011101001111110110000000000000000000000110111000100011001110111110100001110010001000100000000111010111110100100011100111011011101001111101100000011000000100110
Посмотрим на нашу основную TL-B структуру, мы видим, что у нас есть 2 варианта того, что там может быть - account_none$0
или account$1
. Понять, какой вариант у нас, мы можем прочитав префикс, заявленный после символа $, в нашем случае это 1 бит. Если там 0, то у нас account_none
, если 1, то account
.
Наш первый бит из данных выше = 1, значит мы работаем с account$1
и будем использовать схему:
account$1 addr:MsgAddressInt storage_stat:StorageInfo
storage:AccountStorage = Account;
Далее у нас идет addr:MsgAddressInt
, мы видим, что для MsgAddressInt у нас тоже есть несколько вариантов:
addr_std$10 anycast:(Maybe Anycast) workchain_id:int8 address:bits256 = MsgAddressInt;
addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
Чтобы понять с каким именно работать, мы, как и в прошлый раз, читаем биты префикса, в этот раз мы читаем 2 бита. Отрезаем уже прочитанный бит, остается 1000000...
, читаем первые 2 бита и получаем 10
, значит мы работаем с addr_std$10
.
Следующим мы должны распарсить anycast:(Maybe Anycast)
, Maybe значит, что мы должны прочитать 1 бит, и если там единица - читать Anycast, иначе пропустить. Наши оставшиеся биты - это 00000...
, читаем 1 бит, это 0, значит пропускаем Anycast.
Далее у нас идет workchain_id:int8
, тут все просто, читаем 8 бит, это будет айди воркчеина. Читаем следующие 8 бит, все нули, значит воркчеин равен 0.
Далее читаем address:bits256
, это 256 бит адреса, по тому же принципу, что и с workchain_id
. Прочитав, мы получим 21137B0BC47669B3267F1DE70CBB0CEF5C728B8D8C7890451E8613B2D8998270
в hex представлении.
Мы прочитали адрес addr:MsgAddressInt
, далее у нас идет storage_stat:StorageInfo
из основной структуры, его схема:
storage_info$_ used:StorageUsed last_paid:uint32 due_payment:(Maybe Grams) = StorageInfo;
Первым идет used:StorageUsed
, со схемой:
storage_used$_ cells:(VarUInteger 7) bits:(VarUInteger 7) public_cells:(VarUInteger 7) = StorageUsed;
Это количество используемых ячеек и битов для хранения данных аккаунта. Каждое поле определено, как VarUInteger 7
, что значит uint динамического размера, но максимум 7 бит. Понять, как он устроен, можно по схеме:
var_uint$_ {n:#} len:(#< n) value:(uint (len * 8)) = VarUInteger n;
В нашем случае n будет равен 7. В len у нас будет (#< 7)
что значит количество бит, которое вмещает число до 7. Определить его можно, переведя 7-1=6 в бинарный вид - 110
, получаем 3 бита, значит длина len = 3 бита. А value - это (uint (len * 8))
. Чтобы его определить, нам нужно прочитать 3 бита длины, получить число и умножить на 8, это будет размер value
, то есть количество битов, которое нужно прочитать для получения значения VarUInteger.
Прочитаем cells:(VarUInteger 7)
, возьмем наши следующие биты из корневой ячейки, посмотрим на следующие 16 бит для понимания, это 0010011010101000
. Читаем первые 3 бита len, это 001
, то есть 1, получим размер (uint (1 * 8)), получим uint 8, читаем 8 бит, это будет cells
, 00110101
, то есть 53 в десятеричном виде. Делаем то же самое для bits
и public_cells
.
Мы успешно прочитали used:StorageUsed
, следующим у нас идет last_paid:uint32
, тут все просто, читаем 32 бита. Так же все просто с due_payment:(Maybe Grams)
тут мейби, который будет 0, соответственно Grams мы пропускаем. Но, если мейби равен 1, мы можем взглянуть на схему Grams amount:(VarUInteger 16) = Grams
и сразу понять, что мы уже умеем с таким работать. Как в прошлый раз, только вместо 7 - у нас 16.
Далее у нас storage:AccountStorage
со схемой:
account_storage$_ last_trans_lt:uint64 balance:CurrencyCollection state:AccountState = AccountStorage;
Читаем last_trans_lt:uint64
, это 64 бита, хранящие lt последней транзакции аккаунта. И, наконец, баланс, представленный схемой:
currencies$_ grams:Grams other:ExtraCurrencyCollection = CurrencyCollection;
Отсюда мы прочитаем grams:Grams
, который будет являться балансом аккаунта в нано-тонах.
grams:Grams
это VarUInteger 16
, для хранения 16 (в бинарном виде 10000
, отняв 1 получим 1111
) значит читаем первые 4 бита, и полученное значение умножаем на 8, далее читаем полученное количество бит, это и будет нашим балансом.
Разберем на наших данных наши оставшиеся биты:
100000000111010111110100100011100111011011101001111101100000011000000100110
Читаем первые 4 бита - 1000
, это 8. 8*8=64, читаем следующие 64 бита = 0000011101011111010010001110011101101110100111110110000001100000
, убрав лишние нулевые биты, получим 11101011111010010001110011101101110100111110110000001100000
, что равно 531223439883591776
, и, переведя из нано в тон, получаем 531223439.883591776
.
На этом мы остановимся, так как уже разобрали все основные кейсы, остальное можно получить аналогичным образом с тем, что мы разбрали. Так же, дополнительную информацию по разобру TL-B можно найти в оффициальной документации
Теперь, изучив всю информацию, вы можете вызывать и обрабатывать ответы и от других методов lite-server'а. Принцип тот же :)
Айди ключа - это SHA256 хэш сериализованой TL схемы.
Чаще всего применяются следующие TL схемы:
pub.ed25519 key:int256 = PublicKey -- ID c6b41348
pub.aes key:int256 = PublicKey -- ID d4adbc2d
pub.overlay name:bytes = PublicKey -- ID cb45ba34
pub.unenc data:bytes = PublicKey -- ID 0a451fb6
pk.aes key:int256 = PrivateKey -- ID 3751e8a5
Как пример, для ключей типа ED25519, которые используются для хендшейка, ID ключа будет являться sha256 хешом от [0xC6, 0xB4, 0x13, 0x48] и публичного ключа, (массива байт размером 36, префикс + ключ)
Хэндшейк пакет отправляется в полуоткрытом виде, зашифрованы только 160 байт, содержащие информацию о постоянных шифрах.
Чтобы их зашифровать, нам нужен AES-CTR шифр, для его получения нам нужен SHA256 хэш от 160 байт и общий ключ ECDH
Шифр собирается следующим образом:
- ключ = (0 - 15 байты общего ключа) + (16 - 31 байты хэша)
- iv = (0 - 3 байты хэша) + (20 - 31 байты общего ключа)
После того, как шифр собран, мы шифруем им наши 160 байт.
Для расчета общего ключа нам понадобится наш приватный ключ и публичный ключ сервера.
Суть DH в получении общего секретного ключа, без разглашения приватной информации. Приведу пример, как это происходит, в максимально упрощенном виде. Предположим, что нужно сгенерировать общий ключ между нами и сервером, процесс будет выглядеть так:
- Мы генерируем секретное и публичное числа, например 6 и 7
- Сервер генерирует секретное и публичное числа, например 5 и 15
- Мы с сервером обмениваемся публичными числами, отправляем серверу 7, он нам отправляет 15.
- Мы высчитываем: 7^6 mod 15 = 4
- Сервер высчитывает: 7^5 mod 15 = 7
- Обмениваемся полученными числами, мы серверу 4, он нам 7
- Мы высчитываем 7^6 mod 15 = 4
- Сервер высчитывает: 4^5 mod 15 = 4
- Общий ключ = 4
Детали самого ECDH будут опущены, чтобы не усложнять прочтение. Он вычисляется с помощью 2 ключей, приватного и публичного, путем нахождения общей точки на кривой. Если интересно, то лучше почитать про это отдельно.