Перейти к основному содержимому

ADNL TCP - Liteserver

warning

Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.

Это низкоуровневый протокол, на котором построено все взаимодействие в сети TON, он может работать поверх любого протокола, но чаще всего используется поверх TCP и UDP. UDP используется для связи между узлами, а TCP - для связи с lite-серверами.

Теперь мы проанализируем ADNL, работающий поверх TCP, и узнаем, как напрямую взаимодействовать с lite-серверами.

В TCP-версии ADNL сетевые узлы используют открытые ключи ed25519 в качестве адресов и устанавливают соединение с помощью общего ключа, полученного с помощью процедуры Диффи-Хеллмана на эллиптических кривых - ECDH.

Структура пакета

Каждый пакет ADNL TCP, за исключением пакета рукопожатия, имеет следующую структуру:

  • 4 байта размера пакета в порядке от младшего к старшему (N)
  • 32 байта nonce (случайные байты для защиты от атак с контрольной суммой)
  • (N - 64) байта полезной нагрузки
  • 32 байта контрольной суммы SHA256 из nonce и полезной нагрузки

Весь пакет, включая размер, зашифрован AES-CTR. После расшифровки необходимо проверить, соответствует ли контрольная сумма данным, для проверки нужно просто самостоятельно посчитать контрольную сумму и сравнить результат с тем, что у нас в пакете.

Пакет подтверждения связи является исключением, он передается в частично незашифрованном виде и описан в следующей главе.

Установка соединения

Чтобы установить соединение, нам нужно знать IP, порт и открытый ключ сервера, а также сгенерировать собственный закрытый и открытый ключ ed25519.

Данные публичного сервера, такие как IP, порт и ключ, можно получить из глобальной конфигурации. IP в конфигурации в числовой форме, его можно привести к нормальной форме, используя, например, этот инструмент. Открытый ключ в конфигурации в формате base64.

Клиент генерирует 160 случайных байтов, некоторые из которых будут использоваться сторонами в качестве основы для шифрования AES.

Из них создаются 2 постоянных шифра AES-CTR, которые будут использоваться сторонами для шифрования/расшифровки сообщений после подтверждения связи.

  • Шифр ​​A - ключ 0 - 31 байт, iv 64 - 79 байт
  • Шифр ​​B - ключ 32 - 63 байта, iv 80 - 95 байт

Шифры применяются в следующем порядке:

  • Шифр ​​A используется сервером для шифрования отправляемых им сообщений.
  • Шифр ​​A используется клиентом для расшифровки полученных сообщений.
  • Шифр ​​B используется клиентом для шифрования отправляемых им сообщений.
  • Шифр ​​B используется сервером для расшифровки полученных сообщений.

Чтобы установить соединение, клиент должен отправить пакет подтверждения, содержащий:

  • [32 байта] ID ключа сервера [Подробнее]
  • [32 байта] Наш открытый ключ ed25519
  • [32 байта] Хеш SHA256 из наших 160 байт
  • [160 байт] Наши 160 байт зашифрованы [Подробнее]

При получении пакета подтверждения связи сервер выполнит те же действия, получит ключ ECDH, расшифрует 160 байт и создаст 2 постоянных ключа. Если все получится, сервер ответит пустым пакетом ADNL, без полезной нагрузки, для расшифровки которого (а также последующих) нам нужно использовать один из постоянных шифров.

С этого момента соединение можно считать установленным.

После установления соединения мы можем начать получать информацию; язык TL используется для сериализации данных.

Подробнее о TL

Ping&Pong

Оптимально отправлять пакет ping каждые 5 секунд. Это необходимо для поддержания соединения во время отсутствия передачи данных, в противном случае сервер может разорвать соединение.

Пакет ping, как и все остальные, строится по стандартной схеме, описанной выше, и несет в себе идентификатор запроса и идентификатор ping в качестве полезных данных.

Давайте найдем нужную схему для запроса ping здесь и вычислим идентификатор схемы как crc32_IEEE("tcp.ping random_id:long = tcp.Pong"). При преобразовании в байты с прямым порядком байтов получаем 9a2b084d.

Таким образом, наш пакет ping ADNL будет выглядеть так:

  • 4 байта размера пакета в прямом порядке -> 64 + (4+8) = 76
  • 32 байта nonce -> случайные 32 байта
  • 4 байта схемы ID TL -> 9a2b084d
  • 8 байт идентификатора запроса -> случайное число uint64
  • 32 байта контрольной суммы SHA256 из nonce и полезной нагрузки

Мы отправляем наш пакет и ждем tcp.pong, random_id будет равен тому, который мы отправили в пакете ping.

Получение информации от Liteserver

Все запросы, направленные на получение информации из блокчейна, упакованы в схему Liteserver запроса, которая в свою очередь упакована в схему ADNL запроса.

Lite запрос: liteServer.query data:bytes = Object, id df068c79

ADNL запрос: adnl.message.query query_id:int256 query:bytes = adnl.Message, id 7af98bb4

Lite запрос передается внутри ADNL запроса как query:bytes, а окончательный запрос передается внутри Lite запроса как data:bytes.

Анализ кодирующих байтов в TL

getMasterchainInfo

Теперь, поскольку мы уже знаем, как генерировать пакеты TL для API Lite, мы можем запросить информацию о текущем блоке мастерчейна TON. Блок мастерчейна используется во многих дальнейших запросах как входной параметр для указания состояния (момента), в котором нам нужна информация.

Мы ищем нужную нам схему TL, вычисляем ее идентификатор и создаем пакет:

  • 4 байта размера пакета в прямом порядке -> 64 + (4+32+(1+4+(1+4+3)+3)) = 116
  • 32 байта nonce -> случайные 32 байта
  • 4 байта схемы ID ADNL запроса -> 7af98bb4
  • 32 байта query_id:int256 -> случайные 32 байта
    • 1 байт размера массива -> 12
    • 4 байта схемы ID Lite запроса -> df068c79
      • 1 байт размера массива -> 4
      • 4 байта схемы ID getMasterchainInfo -> 2ee6b589
      • 3 нулевых байта заполнения (выравнивание по 8)
    • 3 нулевых байта заполнения (выравнивание по 16)
  • 32 байта контрольной суммы SHA256 из одноразового nonce и полезной нагрузки

Пример пакета в шестнадцатеричном формате:

74000000                                                             -> packet size (116)
5fb13e11977cb5cff0fbf7f23f674d734cb7c4bf01322c5e6b928c5d8ea09cfd -> nonce
7af98bb4 -> ADNLQuery
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id
0c -> array size
df068c79 -> LiteQuery
04 -> array size
2ee6b589 -> getMasterchainInfo
000000 -> 3 bytes of padding
000000 -> 3 bytes of padding
ac2253594c86bd308ed631d57a63db4ab21279e9382e416128b58ee95897e164 -> sha256

В ответ мы ожидаем получить liteServer.masterchainInfo, состоящий из последнего: ton.blockIdExt state_root_hash:int256 и init:tonNode.zeroStateIdExt.

Полученный пакет десериализуется так же, как и отправленный — имеет тот же алгоритм, но в противоположном направлении, за исключением того, что ответ оборачивается только в ADNL запросе.

После расшифровки ответа получаем пакет вида:

20010000                                                                  -> packet size (288)
5558b3227092e39782bd4ff9ef74bee875ab2b0661cf17efdfcd4da4e53e78e6 -> nonce
1684ac0f -> ADNLAnswer
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id (identical to request)
b8 -> array size
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 bytes of padding
520c46d1ea4daccdf27ae21750ff4982d59a30672b3ce8674195e8a23e270d21 -> sha256

runSmcMethod

Мы уже знаем, как получить блок мастерчейна, поэтому теперь мы можем вызывать любые методы lite-сервера. Давайте проанализируем 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;

В запросе мы видим следующие поля:

  1. mode:# - битовая маска uint32 того, что мы хотим видеть в ответе, например, result:mode.2?bytes будет присутствовать в ответе только в том случае, если бит с индексом 2 равен единице.
  2. id:tonNode.blockIdExt - это наше состояние главного блока, которое мы получили в предыдущей главе.
  3. account:liteServer.accountId - данные адреса воркчейна и смарт-контракта.
  4. method_id:long - 8 байт, в которых от имени вызываемого метода записан crc16 с таблицей XMODEM + установлен бит 17 [Расчет]
  5. params:bytes - Стек сериализованный в BoC, содержащий аргументы для вызова метода. [Пример реализации]

Например, нам нужно только result:mode.2?bytes, тогда наш режим будет равен 0b100, то есть 4. В ответ мы получим:

  1. mode:# -> то, что было отправлено - 4.
  2. id:tonNode.blockIdExt -> наш главный блок, на котором был выполнен метод
  3. shardblk:tonNode.blockIdExt -> блок шарда, в котором находится аккаунт контракта
  4. exit_code:int -> 4 байта, которые являются кодом выхода при выполнении метода. Если все успешно, то = 0, если нет, то равно коду исключения.
  5. result:mode.2?bytes -> Стек сериализованный в 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 = результат выполнения getMasterchainInfo
  • account = воркчейн 0 (4 байта 00000000), и int256 полученое из нашего адреса контракта, т. е. 32 байта 4bdbfde5322cb2c14d7b83ea2bf0deeff610e63c2a6db7304f1368ac176193ce
  • method_id = вычисленный id из a2 -> 0a2e010000000000
  • params:bytes = Наш метод не принимает входные параметры, поэтому нам нужно передать ему пустой стек (000000, ячейка 3 байта - глубина стека 0), сериализованный в BoC -> b5ee9c72010101010005000006000000 -> сериализовать в байтах и ​​получить 10b5ee9c72410101010005000006000000000000 0x10 - размер, 3 байта в конце - заполнение.

В ответ получаем:

  • mode:# -> не интересно
  • id:tonNode.blockIdExt -> не интересно
  • shardblk:tonNode.blockIdExt -> не интересно
  • exit_code:int -> равно 0, если выполнение прошло успешно
  • result:mode.2?bytes -> Стек, содержащий данные, возвращаемые методом в формате BoC, распакуем его.

Внутри result мы получили b5ee9c7201010501001b000208000002030102020203030400080ccffcc1000000080aabbcc8, это BoC, содержащий данные. При десериализации мы получим ячейку:

32[00000203] -> {
8[03] -> {
0[],
32[0AABBCC8]
},
32[0CCFFCC1]
}

Если мы ее разберем, то получим 2 значения типа ячейки, которые возвращает наш метод FunC. Первые 3 байта корневой ячейки 000002 - это глубина стека, то есть 2. Это значит, что метод вернул 2 значения.

Продолжаем разбор, следующие 8 бит (1 байт) - это тип значения на текущем уровне стека. Для некоторых типов может потребоваться 2 байта. Возможные варианты можно увидеть в схеме. В нашем случае у нас 03, что означает:

vm_stk_cell#03 cell:^Cell = VmStackValue;

То есть тип нашего значения - ячейка, и, согласно схеме, она хранит само значение как ссылку. Но, если мы посмотрим на схему хранения элементов стека:

vm_stk_cons#_ {n:#} rest:^(VmStackList n) tos:VmStackValue = VmStackList (n + 1);

Мы увидим, что первая ссылка rest:^(VmStackList n) - это ячейка следующего значения в стеке, а наше значение tos:VmStackValue идет вторым, поэтому для получения значения нам нужно прочитать вторую ссылку, то есть 32[0CCFFCC1] - это наша первая ячейка, которую вернул контракт.

Теперь мы можем пойти глубже и получить второй элемент стека, проходим по первой ссылке, теперь у нас есть:

8[03] -> {
0[],
32[0AABBCC8]
}

Повторяем тот же процесс. Первые 8 бит = 03 - то есть снова ячейка. Вторая ссылка - это значение 32[0AABBCC8] и так как глубина нашего стека равна 2, мы завершаем проход. Всего у нас есть 2 значения, возвращаемых контрактом - 32[0CCFFCC1] и 32[0AABBCC8].

Обратите внимание, что они идут в обратном порядке. Точно так же нужно передавать аргументы при вызове функции - в обратном порядке от того, что мы видим в коде FunC.

Пример реализации

getAccountState

Чтобы получить данные о состоянии аккаунта, такие как баланс, код и данные контракта, мы можем использовать getAccountState. Для запроса нам нужен новый главный блок и адрес аккаунта. В ответ мы получим структуру TL AccountState.

Давайте проанализируем схему TL AccountState:

liteServer.accountState id:tonNode.blockIdExt shardblk:tonNode.blockIdExt shard_proof:bytes proof:bytes state:bytes = liteServer.AccountState;
  1. id - наш главный блок, относительно которого мы получили данные.
  2. shardblk - блок шарда воркчейна, где находится наш аккаунт, относительно которого мы получили данные.
  3. shard_proof - доказательство Меркла блока шарда.
  4. proof - доказательство Меркла состояния аккаунта.
  5. state - BoC TL-B схема состояния аккаунта.

Из всех этих данных, то, что нам нужно, находится в состоянии, мы его и проанализируем.

Например, получим статус аккаунта 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 в шестнадцатеричном представлении.

Считываем адрес 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, что означает единицу динамического размера, но максимум 7 бит. Вы можете понять, как оно устроено в соответствии со схемой:

var_uint$_ {n:#} len:(#< n) value:(uint (len * 8)) = VarUInteger n;

В нашем случае n будет равно 7. В len у нас будет (#< 7), что означает количество бит, которые могут содержать число до 7. Вы можете определить это, переведя 7-1=6 в двоичную форму - 110, мы получаем 3 бита, поэтому длина len = 3 бита. А значение равно (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) здесь Maybe, который будет равен 0, поэтому мы пропускаем Grams. Но, если Maybe равен 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, который будет балансом аккаунта в nano-tone. grams:Grams - это VarUInteger 16, для хранения 16 (в двоичной форме 10000, вычитая 1, получаем 1111), затем считываем первые 4 бита и умножаем полученное значение на 8, затем считываем полученное количество бит, это наш баланс.

Давайте проанализируем наши оставшиеся биты в соответствии с нашими данными:

100000000111010111110100100011100111011011101001111101100000011000000100110

Считываем первые 4 бита - 1000, это 8. 8*8=64, читаем следующие 64 бита = 0000011101011111010010001110011101101110100111110110000001100000, удаляя лишние нулевые биты, получаем 11101011111010010001110011101101110100111110110000001100000, что равно 531223439883591776, и, переводя из nano в TON, получаем 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, которые используются для подтверждения связи, идентификатором ключа будет хэш SHA256 из [0xC6, 0xB4, 0x13, 0x48] и открытого ключа, (массив из 36 байт, префикс + ключ)

Пример кода

Шифрование пакетных данных при установлении связи

Пакет подтверждения связи отправляется в полуоткрытом виде, зашифровано всего 160 байт, содержащих информацию о постоянных шифрах.

Для их шифрования нам понадобится шифр AES-CTR, для его получения нам понадобится хэш SHA256 из 160 байт и общий ключ ECDH

Шифр строится следующим образом:

  • key = (0 - 15 байт открытого ключа) + (16 - 31 байт хеша)
  • iv = (0 - 3 байта хеша) + (20 - 31 байт открытого ключа)

После того, как шифр собран, мы шифруем с его помощью наших 160 байт.

Пример кода

Получение общего ключа с помощью ECDH

Для вычисления общего ключа нам понадобится наш закрытый ключ и открытый ключ сервера.

Суть DH заключается в получении общего секретного ключа, не раскрывая закрытую информацию. Приведу пример того, как это происходит, в максимально упрощенном виде. Предположим, нам нужно сгенерировать общий ключ между нами и сервером, процесс будет выглядеть так:

  1. Мы генерируем секретные и открытые числа, например 6 и 7
  2. Сервер генерирует секретные и открытые числа, например 5 и 15
  3. Мы обмениваемся открытыми числами с сервером, отправляем 7 на сервер, он отправляет нам 15.
  4. Мы вычисляем: 7^6 mod 15 = 4
  5. Сервер вычисляет: 7^5 mod 15 = 7
  6. Мы обмениваемся полученными числами, отдаем серверу 4, он дает нам 7
  7. Мы вычисляем 7^6 mod 15 = 4
  8. Сервер вычисляет: 4^5 mod 15 = 4
  9. Общий ключ = 4

Детали самого ECDH будут опущены для простоты. Он вычисляется с использованием 2 ключей, закрытого и открытого, путем нахождения общей точки на кривой. Если интересно, лучше почитать об этом отдельно.

Пример кода

Ссылки

Вот ссылка на оригинальную статью Олега Баранова.