ADNL over UDP: internode communication
ADNL over UDP is the protocol used by TON nodes to communicate with each other. It serves as the foundation for higher-level protocols such as the distributed hash table (DHT) and Reliable Large Datagram Protocol (RLDP).
Unlike ADNL over TCP, the UDP implementation uses channels for ongoing communication and establishes the connection when the first data exchange occurs. Initial packets use public-key encryption and identity signatures, while established channels use directional symmetric keys.
Transport limits
TON applies these limits before or during packet construction:
| Limit | Size | Purpose |
|---|---|---|
| ADNL message MTU | 1,024 bytes | Maximum adnl.Message size without fragmentation |
| UDP transport MTU | 1,440 bytes | Maximum UDP payload passed to the network manager |
| Reassembled message | 8,192 bytes | Maximum serialized message accepted through adnl.message.part |
ADNL over UDP remains an unreliable datagram protocol — although sequence numbers reject duplicates and stale packets, the missing packages are not retransmitted.
Peer endpoint discovery
A sender needs the peer's full public key, 256-bit ADNL short identifier, and a usable endpoint. Static bootstrap entries are available in the dht.nodes section of the global configuration. Nodes can subsequently update peer information through address lists and the DHT.
For adnl.address.udp, ip is an unsigned 32-bit IPv4 value and port is a 16-bit UDP port encoded in a TL int field.
For example, a dht.nodes entry can contain:
{
"@type": "dht.node",
"id": {
"@type": "pub.ed25519",
"key": "fZnkoIAxrTd4xeBgVpZFRm5SvVvSx7eN3Vbe8c83YMk="
},
"addr_list": {
"addrs": [
{
"@type": "adnl.address.udp",
"ip": 1091897261,
"port": 15813
}
]
}
}To connect to a node, lite-client:
- Takes its public
key,ipaddress, and theport. - Decodes the Ed25519 public key from base64
key. - Converts the
ipvalue to dotted-decimal format. For example,1091897261becomes65.21.7.173. - Combines the IP address with the port. For example,
65.21.7.173:15813. - Establishes a UDP connection with the resulting
IP:PORTaddress.
Packet contents
UDP envelopes encrypt a boxed adnl.packetContents object:
adnl.packetContents
rand1:bytes -- random 7 or 15 bytes
flags:# -- bit flags for field presence
from:flags.0?PublicKey -- sender's public key
from_short:flags.1?adnl.id.short -- sender's ID
message:flags.2?adnl.Message -- single message
messages:flags.3?(vector adnl.Message) -- multiple messages
address:flags.4?adnl.addressList -- sender's address list
priority_address:flags.5?adnl.addressList -- priority address list
seqno:flags.6?long -- packet sequence number
confirm_seqno:flags.7?long -- last received seqno
recv_addr_list_version:flags.8?int -- address version
recv_priority_addr_list_version:flags.9?int -- priority address version
reinit_date:flags.10?int -- connection reinitialization date
dst_reinit_date:flags.10?int -- peer's reinitialization date
signature:flags.11?bytes -- ed25519 signature
rand2:bytes -- random 7 or 15 bytes
= adnl.PacketContentsrand1 and rand2 are independently generated 7-byte or 15-byte values. A packet can contain either message or messages, but not both. Both reinitialization dates are present when flag bit 10 is set.
Envelope before channel establishment
A UDP packet sent before channel establishment has this wire layout:
| Field | Size | Description |
|---|---|---|
| Destination ADNL short ID | 32 bytes | SHA-256 of the destination identity's boxed public key |
| Ephemeral Ed25519 public key | 32 bytes | Fresh key generated by the public-key encryptor |
| Plaintext hash | 32 bytes | SHA-256 of the serialized adnl.packetContents |
| Encrypted packet contents | Variable | AES-CTR encryption of adnl.packetContents |
The last 3 fields are the output of TON's Ed25519 encryptor. It derives a shared secret from the ephemeral private key and the destination identity public key. The destination uses its identity private key and the transmitted ephemeral public key to derive the same secret.
The sender signs the boxed packet contents with the source identity private key. Signature calculation clears signature and flag bit 11, serializes the remaining object, and signs those bytes. The sender then inserts the signature and serializes the object again before encryption.
The receiver performs these checks before delivering messages:
- Decrypts the envelope with the private key selected by the destination short ID.
- Verifies the plaintext SHA-256 value.
- Parses
adnl.packetContentsand validates its flags. - Resolves the source public key and verifies the packet signature.
- Applies re-initialization, sequence-number, acknowledgment, and address-list checks.
Ed25519 envelope implementation in the TON monorepo defines the exact key and initialization-vector derivation.
Channel negotiation
Peers negotiate channels with these messages:
adnl.message.createChannel
key:int256
date:int
= adnl.Message;
adnl.message.confirmChannel
key:int256
peer_key:int256
date:int
= adnl.Message;There, key is a freshly generated Ed25519 channel public key and date is its Unix creation time. In confirmChannel, peer_key must equal the public key proposed by the peer — confirmations for another or superseded key are rejected.
A typical first packet contains createChannel and an application message such as adnl.message.query. The application message is not required to be a query — channel negotiation is appended to whichever messages are queued for the peer.
The numeric constructor ID of adnl.message.createChannel is 0xe673c3bb, and its little-endian wire bytes are bb c3 73 e6. Constructor IDs elsewhere on this page follow the same numeric-ID and wire-byte distinction.
Serialized createChannel example:
bbc373e6 -- createChannel constructor wire bytes
d59d8e3991be20b54dde8b78b3af18b379a62fa30e64af361c75452f6af019d7 -- key
555c8763 -- dateAn accompanying query uses this schema:
adnl.message.query query_id:int256 query:bytes = adnl.Message;The actual request, such as dht.getSignedAddressList, is serialized inside query. Its sample encoding is:
7af98bb4 -- adnl.message.query constructor wire bytes
d7be82afbc80516ebca39784b8e2209886a69601251571444514b7f17fcd8875 -- random query_id
04 ed4879a9 000000 -- query: 4-byte payload and 3-byte paddingFor this exchange, the peer responds with adnl.message.confirmChannel, which provides its channel public key, and adnl.message.answer, which carries the query response.
Channel key derivation
Each peer derives a 32-byte shared secret from its channel private key and the other peer's channel public key. Two AES keys are then defined:
shared_key = shared secret
reversed_key = shared secret with its 32 bytes reversedDirection is selected by comparing the peers' ADNL short identity IDs as unsigned 256-bit values. The comparison does not use channel-key IDs.
| Condition | Outgoing encryption key | Incoming decryption key |
|---|---|---|
| Local ADNL ID < peer ADNL ID | reversed_key | shared_key |
| Local ADNL ID > peer ADNL ID | shared_key | reversed_key |
The incoming and outgoing channel IDs are the ADNL short IDs of their respective AES keys.
Channel implementation in the TON monorepo defines the derivation and comparison.
Channel packet envelope
An established channel packet has this wire layout:
| Field | Size | Description |
|---|---|---|
| Outgoing channel ID | 32 bytes | ADNL short ID of the directional AES key |
| Plaintext hash | 32 bytes | SHA-256 of the serialized adnl.packetContents |
| Encrypted packet contents | Variable | AES-CTR encryption of adnl.packetContents |
Channel packets omit the source identity and signature because possession of the directional key authenticates the sender. They normally omit reinitialization dates. They can still contain a single message or message vector, sequence fields, address lists, priority address lists, and received-version fields.
Sequence and session state
-
seqnoidentifies a packet within the current reinitialization epoch. TON tracks the highest received number plus a 64-packet mask. It rejects duplicates and packets more than 63 positions behind the highest accepted number. -
confirm_seqnoacknowledges the highest packet number accepted from the peer. A value greater than the highest locally sent sequence number is invalid. Acknowledgment updates state but does not cause ADNL to retransmit missing packets. -
reinit_dateidentifies the sender's epoch, anddst_reinit_datestates which destination epoch the sender has observed. A newer valid peer epoch resets channel, sequence, acknowledgment, address-version, and partial-message state. Reinitialization metadata is carried in signed packets sent outside a channel.
Address-list propagation
Packets can advertise ordinary and priority address lists. The recv_addr_list_version fields tell the peer which versions have already been received, allowing it to omit unchanged lists.
adnl.addressList
addrs:(vector adnl.Address)
version:int
reinit_date:int
priority:int
expire_at:int
= adnl.AddressList;Supported address variants include IPv4 UDP, IPv6 UDP, ADNL tunnels, reverse connectivity, and IPv4 QUIC endpoints. Nodes reject address lists whose serialized representation exceeds the implementation limit.
Message types
Query and answer, adnl.message.query
adnl.message.query query_id:int256 query:bytes = adnl.Message;
adnl.message.answer query_id:int256 answer:bytes = adnl.Message;query_id correlates one answer with one outstanding query. Higher-level protocol data is serialized inside query or answer.
Part, adnl.message.part
adnl.message.part
hash:int256
total_size:int
offset:int
data:bytes
= adnl.Message;The blockchain fragments the complete boxed serialization of an adnl.Message, not only its application payload. Reassembly has these constraints:
- The first accepted part has offset
0. - Parts for a message arrive contiguously and in order.
- Only one hash is assembled per peer at a time.
total_sizedoes not exceed 8,192 bytes.- The final SHA-256 value equals
hash. - The reassembled bytes parse as one boxed
adnl.Message.
Custom, adnl.message.custom
adnl.message.custom data:bytes = adnl.Message;custom delegates the payload to a subscribed higher-level protocol. RLDP uses custom messages because its transfer protocol does not follow a single-request, single-response exchange.
No-operation, adnl.message.nop
adnl.message.nop = adnl.Message;nop carries no application data.
Reinitialization, adnl.message.reinit
adnl.message.reinit date:int = adnl.Message;reinit asks the peer to adopt a newer session epoch — stale dates have no effect.