RLDP
Please see the implementations:
Overview
The Reliable Large Datagram Protocol (RLDP) operates on top of the ADNL UDP protocol and is designed for transferring large data blocks. It incorporates Forward Error Correction (FEC) algorithms, which allow it to replace acknowledgment packets typically sent from the receiver back to the sender.
This capability enables more efficient data transfer between network components, although it results in increased traffic consumption.
RLDP plays a crucial role throughout the TON infrastructure. It is used for various purposes, such as downloading blocks from other nodes, transferring data to those nodes, and accessing TON websites and TON Storage.
Protocol
RLDP utilizes the following TL structures for communication:
fec.raptorQ data_size:int symbol_size:int symbols_count:int = fec.Type;
fec.roundRobin data_size:int symbol_size:int symbols_count:int = fec.Type;
fec.online data_size:int symbol_size:int symbols_count:int = fec.Type;
rldp.messagePart transfer_id:int256 fec_type:fec.Type part:int total_size:long seqno:int data:bytes = rldp.MessagePart;
rldp.confirm transfer_id:int256 part:int seqno:int = rldp.MessagePart;
rldp.complete transfer_id:int256 part:int = rldp.MessagePart;
rldp.message id:int256 data:bytes = rldp.Message;
rldp.query query_id:int256 max_answer_size:long timeout:int data:bytes = rldp.Message;
rldp.answer query_id:int256 data:bytes = rldp.Message;
The serialized structure is encapsulated in the adnl.message.custom
TL schema and transmitted over ADNL UDP.
RLDP transfers are utilized for sending large amounts of data. A random transfer_id
is generated, and the data is then processed using the FEC algorithm.
The resulting segments are wrapped in a rldp.messagePart
structure and sent to the peer until the peer responds with rldp.complete
or a timeout occurs.
Once the receiver has gathered the necessary rldp.messagePart
pieces to reconstruct the complete message, it concatenates them, decodes them using FEC, and then deserializes the resulting byte array into either an rldp.query
or rldp.answer
structure, depending on the type indicated by the tl prefix id
.
FEC
Valid Forward Error Correction (FEC) algorithms suitable for RLDP include RoundRobin, Online, and RaptorQ.
Currently, RaptorQ is used for data encoding.
RaptorQ
The core concept of RaptorQ is the division of data into symbols, which are blocks of a fixed, predetermined size.
These blocks are organized into matrices, where discrete mathematical operations are performed. This process enables the creation of an almost limitless number of symbols from the same original data.
All the generated symbols are combined and sent to the recipient, allowing for the recovery of lost packets without the need for additional requests to the server. This method uses fewer packets than would be required if the same pieces of data were sent repeatedly.
The symbols are transmitted to the peer until they confirm that all data has been received and successfully restored (decoded) by applying the same discrete operations.
[Please see implementation example of RaptorQ in Golang].
RLDP-HTTP
To interact with TON Sites, the RLDP (Reverse Lightweight Data Protocol) is used to wrap HTTP requests. The host sets up their site on any standard HTTP web server and runs rldp-http-proxy
alongside it.
All incoming requests from the TON network are directed to the proxy via the RLDP protocol. The proxy then converts these requests into standard HTTP format and calls the original web server locally.
On the user's side, they launch a proxy, such as Tonutils Proxy, to access the .ton sites. All traffic is wrapped in the reverse order: requests are sent to the local HTTP proxy, which then forwards them via RLDP to the remote TON site.
HTTP communication within RLDP is structured using TL formats:
http.header name:string value:string = http.Header;
http.payloadPart data:bytes trailer:(vector http.header) last:Bool = http.PayloadPart;
http.response http_version:string status_code:int reason:string headers:(vector http.header) no_payload:Bool = http.Response;
http.request id:int256 method:string url:string http_version:string headers:(vector http.header) = http.Response;
http.getNextPayloadPart id:int256 seqno:int max_chunk_size:int = http.PayloadPart;
This is not pure HTTP in text form; everything is wrapped in a binary TL and unwrapped before being sent to the web server or browser by the proxy itself.
The scheme of work is as follows:
- Client sends
http.request
- The server checks the
Content-Length
header when receiving a request- If not 0, sends a
http.getNextPayloadPart
request to the client - When receiving a request, the client sends
http.payloadPart
- the requested body piece depending onseqno
andmax_chunk_size
. - The server repeats requests, incrementing
seqno
, until it receives all the chunks from the client, i.e. until thelast:Bool
field of the last chunk received is true.
- If not 0, sends a
- After processing the request, the server sends
http.response
, the client checks theContent-Length
header- If it is not 0, then sends a
http.getNextPayloadPart
request to the server, and the operations are repeated, as in the case of the client but vice-versa.
- If it is not 0, then sends a
Request the TON site
To understand how RLDP works, let's look at an example of getting data from the TON site foundation.ton
.
Assuming say we have already got its ADNL address by calling the Get method of the NFT-DNS contract, determined the address and port of the RLDP service using DHT, and connected to it over ADNL UDP.
Send a GET request to foundation.ton
To accomplish this, please complete the following structure:
http.request id:int256 method:string url:string http_version:string headers:(vector http.header) = http.Response;
Serialize http.request
by filling in the fields:
e191b161 -- TL ID http.request
116505dac8a9a3cdb464f9b5dd9af78594f23f1c295099a9b50c8245de471194 -- id = {random}
03 474554 -- method = string `GET`
16 687474703a2f2f666f756e646174696f6e2e746f6e2f 00 -- url = string `http://foundation.ton/`
08 485454502f312e31 000000 -- http_version = string `HTTP/1.1`
01000000 -- headers (1)
04 486f7374 000000 -- name = Host
0e 666f756e646174696f6e2e746f6e 00 -- value = foundation.ton
Now let's wrap our serialized http.request
into rldp.query
and serialize it too:
694d798a -- TL ID rldp.query
184c01cb1a1e4dc9322e5cabe8aa2d2a0a4dd82011edaf59eb66f3d4d15b1c5c -- query_id = {random}
0004040000000000 -- max_answer_size = 257 KB, can be any sufficient size that we accept as headers
258f9063 -- timeout (unix) = 1670418213
34 e191b161116505dac8a9a3cdb464f9b5dd9af78594f23f1c295099a9b50c8245 -- data (http.request)
de4711940347455416687474703a2f2f666f756e646174696f6e2e746f6e2f00
08485454502f312e310000000100000004486f73740000000e666f756e646174
696f6e2e746f6e00 000000
Encoding and sending packets
We now need to apply the FEC RaptorQ algorithm to our data.
First, we will create an encoder, which requires us to convert the resulting byte array into symbols of a fixed size. In this case, the symbol size is 768 bytes.
To achieve this, we'll divide the array into segments of 768 bytes each. If the last segment is smaller than 768 bytes, we will pad it with zero bytes to reach the required size.
Our current array is 156 bytes long, which means it will consist of only one segment. To make it 768 bytes, we need to add 612 zero bytes for padding.
Additionally, the constants chosen for the encoder depend on the data size and the symbol size. For more detailed information, you can refer to the RaptorQ documentation. However, to simplify the process and avoid complex mathematical calculations, we recommend using a pre-existing library that implements this encoding.
Please see the examples:
Symbols are encoded and transmitted in a round-robin manner. We start with an initial sequence number, seqno
, set to 0, and increment it by 1 for each subsequent encoded packet. For instance, if we have two symbols, we first encode and send the first symbol, then increase seqno
by 1. Next, we encode and send the second symbol and again increase seqno
by 1. After that, we return to the first symbol and increment seqno
(which is now 2) by another 1.
This process continues until we receive a message indicating that the peer has accepted the data.
Having created the encoder, we are now ready to send data. To do this, we will fill in the TL schema:
fec.raptorQ data_size:int symbol_size:int symbols_count:int = fec.Type;
rldp.messagePart transfer_id:int256 fec_type:fec.Type part:int total_size:long seqno:int data:bytes = rldp.MessagePart;
transfer_id
- random int256, the same for all messageParts within the same data transfer.fec_type
isfec.raptorQ
.data_size
= 156symbol_size
= 768symbols_count
= 1
part
in our case always 0, can be used for transfers that hit the size limit.total_size
= 156. The size of our transfer data.seqno
- for the first packet will be equal to 0, and for each subsequent packet it will increase by 1, will be used as parameter to decode and encode symbol.data
- our encoded symbol, 768 bytes in size.
After serializing rldp.messagePart
, wrap it in adnl.message.custom
and send it over ADNL UDP.
We will send packets in a continuous loop, incrementing the seqno
each time, until we either receive the rldp.complete
message from the peer or reach a timeout. Once we have sent a number of packets equal to the number of our symbols, we can slow down the transmission and send additional packets, for example, once every 10 milliseconds or even less frequently.
These extra packets are intended for recovery in case of data loss, as UDP is a fast but unreliable protocol.
[Please see implementation example].
Processing the response from foundation.ton
During the sending process, we can expect a response from the server. In our case, we are waiting for rldp.answer
containing http.response
.
The response will arrive in the same format as it was sent during the request, but the transfer_id
will be inverted (each byte will undergo an XOR
operation with 0xFF
).
We will receive adnl.message.custom
messages that include rldp.messagePart
.
First, we need to extract FEC information from the initial message received during the transfer. Specifically, we are looking for the data_size
, symbol_size
, and symbols_count
parameters from the fec.raptorQ
messagePart structure.
These parameters are essential for initializing the RaptorQ decoder. [Please see the example].
After initialization, we add the received symbols along with their seqno
to our decoder. Once we have gathered the minimum required number of symbols, equal to symbols_count
, we can attempt to decode the full message. If successful, we will send rldp.complete
. [Please see the example].
The result will be a rldp.answer
message containing the same query_id
as in the sent rldp.query
. The data must include http.response
.
http.response http_version:string status_code:int reason:string headers:(vector http.header) no_payload:Bool = http.Response;
The main fields are generally straightforward, as they function similarly to those in HTTP.
One notable flag is no_payload
. If this flag is set to true, it indicates that there is no body in the response, meaning Content-Length
is 0. In this case, the response from the server can be considered received.
If no_payload
is false, this means there is content in the response, and we need to retrieve it. To do this, we should send a request using the TL schema http.getNextPayloadPart
, which should be wrapped in rldp.query
.
http.getNextPayloadPart id:int256 seqno:int max_chunk_size:int = http.PayloadPart;
id
must match the value sent in http.request
, seqno
should be 0, and increment by 1 for each subsequent part. The max_chunk_size
indicates the largest chunk size we can accept, with a typical value of 128 KB (131072 bytes).
In response, we will receive the following information:
http.payloadPart data:bytes trailer:(vector http.header) last:Bool = http.PayloadPart;
If last
is true, then we have reached the end. We can combine all the pieces to create a complete response body, such as HTML.
References
Here is the link to the original article by Oleg Baranov.