Skip to content
18 changes: 13 additions & 5 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,14 @@
"foundations/shards",
"foundations/limits",
"foundations/config",
{
"group": "Network protocols",
"pages": [
"foundations/network/adnl",
"foundations/network/adnl-tcp",
"foundations/network/adnl-udp"
]
},
{
"group": "Web3 services",
"pages": [
Expand Down Expand Up @@ -1395,27 +1403,27 @@
},
{
"source": "/v3/documentation/network/protocols/adnl/overview",
"destination": "https://old-docs.ton.org/v3/documentation/network/protocols/adnl/overview",
"destination": "/foundations/network/adnl",
"permanent": true
},
{
"source": "/v3/documentation/network/protocols/adnl/low-level",
"destination": "https://old-docs.ton.org/v3/documentation/network/protocols/adnl/low-level",
"destination": "/foundations/network/adnl",
Comment thread
coalus marked this conversation as resolved.
"permanent": true
},
{
"source": "/learn/overviews/adnl",
"destination": "https://old-docs.ton.org/learn/overviews/adnl",
"destination": "/foundations/network/adnl",
"permanent": true
},
{
"source": "/v3/documentation/network/protocols/adnl/tcp",
"destination": "https://old-docs.ton.org/v3/documentation/network/protocols/adnl/tcp",
"destination": "/foundations/network/adnl-tcp",
"permanent": true
},
{
"source": "/v3/documentation/network/protocols/adnl/udp",
"destination": "https://old-docs.ton.org/v3/documentation/network/protocols/adnl/udp",
"destination": "/foundations/network/adnl-udp",
"permanent": true
},
{
Expand Down
251 changes: 251 additions & 0 deletions foundations/network/adnl-tcp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
---
title: "ADNL TCP - liteserver communication"
sidebarTitle: "ADNL TCP"
---

import { Aside } from "/snippets/aside.jsx";

ADNL over TCP is used for communication with liteservers.

## Packet structure

Each ADNL TCP packet (except the handshake) has the following structure:

| Field | Size | Description |
| ---------- | ----------------------- | ------------------------------------------------ |
| `size` | 4 bytes (little-endian) | Total packet size `N`, excluding this field |
| `nonce` | 32 bytes | Random bytes protecting against checksum attacks |
| `payload` | `N - 64` bytes | Actual data |
| `checksum` | 32 bytes | SHA-256 of `nonce \|\| payload` |

The entire packet, including the size field, is encrypted with AES-CTR.

After decryption, verify that the checksum matches by computing it independently. The handshake is an exception — see [Handshake](#handshake) below.

## Connection establishment

Prerequisites:

- Server IP, port, and public key from the [global config](https://ton-blockchain.github.io/global.config.json);
- A freshly generated Ed25519 private and public key pair.

<Aside type="tip">
The IP address in the config is a decimal integer. Convert it to dotted-decimal IPv4 format, for example using [this tool](https://www.browserling.com/tools/dec-to-ip). The public key is base64-encoded.
</Aside>
Comment thread
coalus marked this conversation as resolved.
Outdated

### Handshake

1. The client performs a key agreement protocol (x25519) using its private key and the server's public key, selecting the key representation based on its `type_id`, to derive a `secret`.

1. The client generates AES-CTR session parameters for both transmit (client -> server) and receive (server -> client) directions. Each direction uses a 32-byte key and a 16-byte nonce. The parameters are serialized into a 160-byte buffer:

| Parameter | Size |
| ---------- | -------- |
| `rx_key` | 32 bytes |
| `tx_key` | 32 bytes |
| `rx_nonce` | 16 bytes |
| `tx_nonce` | 16 bytes |
| `padding` | 64 bytes |

Fill the entire 160-byte buffer with random bytes. If any part is predictable, an attacker can perform an active man-in-the-middle attack using compromised AES-CTR session parameters.

1. The client encrypts the session parameters (`aes_params`) using AES-256-CTR with a 128-bit big-endian counter. The encryption key and nonce are derived from the SHA-256 hash of `aes_params` and the ECDH `secret` — see [Handshake encryption](#handshake-encryption) for the exact derivation.

1. The client sends a 256-byte handshake packet with the following structure:

| Field | Size | Description |
| ------------------------------------------- | --------- | ------------------------------------- |
| Server key ID (`receiver_address`) | 32 bytes | Server peer identity |
| Client Ed25519 public key (`sender_public`) | 32 bytes | Client public key |
| `SHA-256(aes_params)` | 32 bytes | Integrity proof of session parameters |
| [`E(aes_params)`](#handshake-encryption) | 160 bytes | Encrypted session parameters |

1. The server decrypts the session parameters and performs these checks:

- It possesses the private key corresponding to `receiver_address`.
- `SHA-256(aes_params) == SHA-256(D(E(aes_params)))`.

If any check fails, the server drops the connection. If all checks pass, the server sends an empty datagram to confirm ownership of the private key.

After this exchange the connection is established. Data is serialized using [TL (Type Language)](https://core.telegram.org/mtproto/TL).

### Cipher assignment

Both sides derive two permanent AES-CTR ciphers from the 160-byte session parameters:

| Cipher | Key (bytes) | Initialization vector (bytes) | Used for |
| -------- | ----------- | ----------------------------- | -------------------------------- |
| Cipher A | 0-31 | 64-79 | Server encrypts, client decrypts |
| Cipher B | 32-63 | 80-95 | Client encrypts, server decrypts |

## Security considerations

- **Handshake padding.** The 64-byte padding field in `aes_params` is unused in current implementations. It may have been intended to support future migration to alternative encryption primitives.
- **Session key derivation.** The encryption key is derived from both the static `secret` and `SHA-256(aes_params)`. Since `aes_params` is random for each session, this results in a unique encryption key per connection. However, the concatenation-based derivation combines subarrays of the secret and the hash, which is considered suboptimal by modern standards.
- **Datagram nonce.** In CTR mode, AES operates as a stream cipher, making bit-flipping attacks possible if the plaintext is known. The per-packet `nonce` mitigates this: even if an attacker reconstructs the key stream and computes a valid `SHA-256(buffer)`, they cannot forge a valid hash without knowing the random nonce.

## Ping and pong

Send a ping packet every 5 seconds to keep the connection alive. Without pings, the server terminates the connection during idle periods.

Ping TL schema: `tcp.ping random_id:long = tcp.Pong` with schema ID `9a2b084d`, derived as the CRC32 of the schema string, little-endian.

Example ADNL ping packet:

| Field | Size | Value |
| ---------- | -------- | ------------------------ |
| `size` | 4 bytes | 76 (64 + 4 + 8) |
| `nonce` | 32 bytes | random |
| schema ID | 4 bytes | `9a2b084d` |
| request ID | 8 bytes | random uint64 |
| `checksum` | 32 bytes | SHA-256(nonce + payload) |

Wait for [`tcp.pong`](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/tl/generate/scheme/ton_api.tl#L23) with matching `random_id`.

## Liteserver queries

All blockchain queries are wrapped in two layers:

- ADNL query: `adnl.message.query query_id:int256 query:bytes = adnl.Message` (ID `7af98bb4`)
- Lite query: `liteServer.query data:bytes = Object` (ID `df068c79`)

The specific liteserver method is serialized inside `data:bytes` of the lite query, which is itself serialized inside `query:bytes` of the ADNL query.

### `getMasterchainInfo`

The masterchain block is required as an input for many subsequent requests.

TL schema: `liteServer.getMasterchainInfo = liteServer.MasterchainInfo` (ID `2ee6b589`).

Packet layout:

```text
74000000 -> packet size (116)
5fb13e11977cb5cff0fbf7f23f674d734cb7c4bf01322c5e6b928c5d8ea09cfd -> nonce
7af98bb4 -> adnl.message.query
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id
0c -> array size (12)
df068c79 -> liteServer.query
04 -> array size (4)
2ee6b589 -> getMasterchainInfo
000000 -> padding (align to 8)
000000 -> padding (align to 16)
ac2253594c86bd308ed631d57a63db4ab21279e9382e416128b58ee95897e164 -> sha256
```

The response is wrapped in `adnl.message.answer` (ID `1684ac0f`) and contains [`liteServer.masterchainInfo`](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/tl/generate/scheme/lite_api.tl#L30), which includes `last:tonNode.blockIdExt`, `state_root_hash:int256`, and `init:tonNode.zeroStateIdExt`.

Example response:

```text
20010000 -> packet size (288)
5558b3227092e39782bd4ff9ef74bee875ab2b0661cf17efdfcd4da4e53e78e6 -> nonce
1684ac0f -> adnl.message.answer
77c1545b96fa136b8e01cc08338bec47e8a43215492dda6d4d7e286382bb00c4 -> query_id
b8 -> array size
81288385 -> liteServer.masterchainInfo
ffffffff -> workchain (int)
0000000000000080 -> shard (long)
27405801 -> seqno (int)
e585a47bd5978f6a4fb2b56aa2082ec9deac33aaae19e78241b97522e1fb43d4 -> root_hash
876851b60521311853f59c002d46b0bd80054af4bce340787a00bd04e0123517 -> file_hash
8b4d3b38b06bb484015faf9821c3ba1c609a25b74f30e1e585b8c8e820ef0976 -> state_root_hash
ffffffff -> workchain (int)
17a3a92992aabea785a7a090985a265cd31f323d849da51239737e321fb05569 -> root_hash
5e994fcf4d425c0a6ce6a792594b7173205f740a39cd56f537defd28b48a0f6e -> file_hash
000000 -> padding
520c46d1ea4daccdf27ae21750ff4982d59a30672b3ce8674195e8a23e270d21 -> sha256
```

### `runSmcMethod`

Call a smart contract get method using:

```tl
liteServer.runSmcMethod mode:# id:tonNode.blockIdExt account:liteServer.accountId method_id:long params:bytes
= liteServer.RunMethodResult
```

Fields:

- `mode` – `uint32` bitmask controlling which response fields are present. Set bit 2 to receive `result`.
- `id` – masterchain block from `getMasterchainInfo`.
- `account` – [`liteServer.accountId`](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/tl/generate/scheme/lite_api.tl#L27) with workchain and address.
- `method_id` – CRC16 (XMODEM table) of the method name, with bit 17 set. See [calculation example](https://github.com/xssnick/tonutils-go/blob/88f83bc3554ca78453dd1a42e9e9ea82554e3dd2/ton/runmethod.go#L16).
- `params` – [stack](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/crypto/block/block.tlb#L783) serialized in [BoC](/foundations/serialization/cells#bag-of-cells), containing arguments.

With `mode = 4` (only `result`), the response includes:

- `exit_code` – `0` on success, or an exception code otherwise.
- `result` – stack in BoC format with returned values.

The stack is parsed according to the [`VmStackValue` TL-B schema](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/crypto/block/block.tlb#L766). Stack elements are stored in reverse order using `vm_stk_cons`, where the first reference points to the rest of the stack and the second contains the current value.

<Aside type="note">
Arguments must be passed in reverse order from what appears in the FunC source code. Return values are also in reverse order.
</Aside>

### `getAccountState`

Retrieve account data (balance, code, storage) using [`getAccountState`](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/tl/generate/scheme/lite_api.tl#L68):

```tl
liteServer.accountState id:tonNode.blockIdExt shardblk:tonNode.blockIdExt shard_proof:bytes proof:bytes state:bytes
= liteServer.AccountState;
```

The `state` field contains a [BoC](/foundations/serialization/cells#bag-of-cells) with the account's [TL-B structure](https://github.com/ton-blockchain/ton/blob/ad736c6bc3c06ad54dc6e40d62acbaf5dae41584/crypto/block/block.tlb#L232):

```tlb
account_none$0 = Account;
account$1 addr:MsgAddressInt storage_stat:StorageInfo storage:AccountStorage = Account;
```

Parse it by reading prefix bits to determine the variant (`account_none$0` vs `account$1`), then read fields sequentially: address, storage info, and the balance from `AccountStorage > CurrencyCollection > grams:Grams`, which is a `VarUInteger 16`.

For a complete parsing walkthrough, see [TL-B documentation](/languages/tl-b/overview).

## Key ID calculation

The key ID is the SHA-256 hash of the serialized TL schema. For Ed25519 keys:

```tl
pub.ed25519 key:int256 = PublicKey -- ID c6b41348
```

The key ID is computed as `SHA-256([0xC6, 0xB4, 0x13, 0x48] || public_key)`, where the input consists of the 4-byte TL constructor ID and the 32-byte public key.

Other key types:

```tl
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
```

[Implementation example](https://github.com/xssnick/tonutils-go/blob/2b5e5a0e6ceaf3f28309b0833cb45de81c580acc/liteclient/crypto.go#L16).

## Handshake encryption

The 160-byte session parameters are encrypted using an AES-CTR cipher derived from the SHA-256 hash of the 160 bytes and the [ECDH `secret`](#ecdh-shared-key):

```text
key = secret[0..16] || hash[16..32] // 16 bytes each
nonce = hash[0..4] || secret[20..32] // 4 + 12 bytes
```

[Implementation example](https://github.com/xssnick/tonutils-go/blob/2b5e5a0e6ceaf3f28309b0833cb45de81c580acc/liteclient/connection.go#L361).

## ECDH shared key

The shared key is computed from one party's private key and the other party's public key using Elliptic-curve Diffie-Hellman. In practice, ADNL uses x25519 (ECDH over Curve25519).

[Implementation example](https://github.com/xssnick/tonutils-go/blob/2b5e5a0e6ceaf3f28309b0833cb45de81c580acc/liteclient/crypto.go#L32).

## See also

- [ADNL specification](/foundations/network/adnl)
- [ADNL UDP](/foundations/network/adnl-udp)
- [TL-B documentation](/languages/tl-b/overview)
- [Cell and BoC serialization](/foundations/serialization/cells)
Loading