TON: recommendations and best practices

This article is a translation of a document published on the TON blockchain page: smc-guidelines.txt . Perhaps this will help someone to take a step towards development for this blockchain. Also, at the end I made a short summary.







Internal messages



Smart contracts interact with each other by sending so-called internal messages. When the internal message reaches its specified destination, a regular transaction is created in the name of the destination account, and the internal message is processed according to the specified code and the constant data of this account (smart contract). In particular, a processing transaction may create one or more internal messages, some of which may be addressed to the source address of the internal message being processed. This can be used to create simple "client-server applications" when a request is embedded (encapsulated) in an internal message and sent to another smart contract that processes the request and sends the response back, again as an internal message.







This approach leads to the need to distinguish between internal messages on the "request" and "response" (as a "query" or as a "response"), or not requiring any additional processing (such as a simple money transfer). In addition, when an answer arrives, there must be a way to understand which request it relates to.







To achieve this goal, it is recommended to use the following internal message template (remember that the TON blockchain does not impose any restrictions on the message body, that is, it is just a recommendation):







0) The body of the message can be embedded in the message itself, or it can be stored in a separate cell (cell *), which is referenced in the message, as indicated in the TL-B fragment of the diagram (in English it is easier to understand: or be stored in a separate cell referred to from the message, as indicated by the TL-B scheme fragment):







message$_ {X:Type} ... body:(Either X ^X) = Message X;
      
      





( https://core.telegram.org/mtproto - here you can read about TL-schemes)







The receiving smart contract must accept at least internal messages with the message body embedded (even if they are placed in the cell containing the message - whenever they fit into the cell containing the message - it is not very clear what this means, therefore, attached the original text). If the contract accepts message bodies in separate cells (using the "right" constructor (Either X ^X)



), the processing of the incoming message should not depend on the particular method of embedding the message body. On the other hand, it is completely legal not to support the message body in a separate cell at all to simplify requests and responses.







1) The message body usually begins with the following fields:









2) If op is zero, then the message is a simple transfer message with a comment. The comment is contained in the remainder of the message (without query_id and so on, that is, starting from the 5th byte (explanation: if there is no query_id , then the op field occupies the first 4 bytes)). If it does not begin with the byte 0xff, the comment is a text one;); it can be displayed to the end user of the wallet "as is" (after filtering invalid and control characters and checking that this is a valid UTF-8 string). For example, users can specify the purpose of a simple transfer from their wallet to the wallet of another user in this field. On the other hand, if a comment begins with byte 0xff, the rest of the message is a “binary comment” that should not be displayed to the end user as text (only as hex dump if necessary). The proposed use of binary comments, for example, is to contain a payment identifier for payment in the store, and be automatically generated and processed by the store software.







Most smart contracts do not have to perform nontrivial actions or reject an incoming message when they receive a “simple transfer message”. Thus, when op turns out to be zero, the smart contract function for processing incoming internal messages (usually called recv_internal()



) should immediately exit with code 0, indicating success (for example, throwing exception 0 if a custom handler is not installed in the smart contract exceptions). This will lead to the fact that the amount transferred by the message will be credited to the recipient's account without any further effect.







3) "A simple message without comments" has an empty body (even without an op field). The above considerations apply to such messages. Please note that such messages must have their own body embedded in the message cell.







4) We expect that the op field of the request messages will have the first bit ("high-order bit", translated as the first, this may be incorrect, but as explained later it becomes clear) is empty, that is, the value of the field should be in the range 1 .. 2^31-1



, and for response messages the first (high-order) bit must be equal to 1, that is, the value of the field in the range 2^31 .. 2^32-1



. If the message is neither a request nor a response (the body does not contain the query_id parameter), then it must contain the op parameter in the range as in the request message: 1 .. 2^31 - 1



.







5) There are several “standard” response messages for which op is 0xffffffff and 0xffffffffe. In general, op values ​​from 0xfffffff0 to 0xffffffff are reserved for such standard answers.









Note that unknown "answers" (with op in the range 2 ^ 31 ... 2 ^ 32-1) should be ignored (in particular, you should not generate a response with op equal to 0xffffffff), as well as unexpected return ( bounced) -messages (with the "bounced" flag set).







Payment for processing requests and sending replies



In general, if a smart contract wants to send a request to another smart contract, it must pay for sending an internal message to the target smart contract (message forwarding fees), for processing this message at the destination (gas fee: gas fees) and for sending a response if required (message forwarding fees).







In most cases, the sender will attach a small amount of gram to the internal message (for example, 1 gram) (enough to pay for processing this message) and set the “bounce” flag on it (that is, it will send a bounceable internal message); the recipient will return the unused part of the received value with the answer (subtracting the fee for sending the message from it). This is usually achieved by calling SENDRAWMSG with mode = 64 (cf. Appendix A to the TON VM documentation).







If the recipient cannot process the received message and execution ends with a non-zero exit code (for example, due to an unhandled cell deserialization exception), the message will be automatically “bounced” back to the sender, and the “bounce” flag will be unchecked and set. flag "bounced". The body of the bounced message will be the same as the original message; therefore, it is important to check the "bounced" flag of the incoming internal message before parsing the op field in the smart contract and processing the corresponding request (otherwise there is a risk that the request contained in the bounced message will be processed by its original sender as a new separate request). If the "bounced" flag is set, special code can understand which request failed (for example, by deserializing op and query_id from a bounced message) and take appropriate action. A simpler smart contract can simply ignore all returned messages (terminate with a zero exit code if the "bounced" flag is set).







On the other hand, the receiver can successfully parse the incoming request and find that the requested op method is not supported or that another error condition has been met. Then a response with op equal to 0xffffffff or another appropriate value should be sent back using SENDRAWMSG with mode = 64, as mentioned above.







In some situations, the sender wants to transfer a certain amount of money at the same time? to the sender? (here, apparently, an error, and was meant to the "recipient") and receive either a confirmation or an error message. For example, a validator elections smart contract receives a request to participate in an election along with a bid as a value added. In such cases, it makes sense to attach, say, one extra gram to the estimated value [cost] (Here the word value is used everywhere, in the meaning of payment for some action, so I used the word "cost"). If an error occurs (for example, the bid cannot be accepted for any reason), the full amount received (minus the processing fee) should be returned to the sender along with the error message (for example, using SENDRAWMSG with mode = 64, as described above). If successful, a confirmation message is created and exactly one gram is sent back (the message transfer fee is subtracted from this value; this is mode = 1 of SENDRAWMSG).







Using non-bounceable messages



Almost all internal messages sent between smart contracts should be returned (you can translate it as "bouncing", but in order not to get confused, it is easier to use this terminology), that is, they must have the "bounce" bit not empty. Then, if the target smart contract does not exist, or if it creates an unhandled exception when processing this message, the message will be “returned” back, bearing the rest of the initial cost (value) (minus all fees for transmitting messages and gas). The returned message will have the same body, but with the bounce flag cleared and the bounced flag set. Therefore, all smart contracts should check the "bounced" flag of all incoming messages and either silently accept them (immediately terminating with a zero exit code) or perform some special processing to determine which outgoing request failed. The request contained in the body of the returned message should never be executed.







In some cases, non-bounceable internal messages need to be used. For example, a new account cannot be created without at least one irrevocable internal message sent to it. If this message does not contain StateInit with the code and data of the new smart contract, it makes no sense to have a non-empty body in a non-returning internal message.







It is a good idea not to allow the end user (e.g., wallet) to send irrevocable messages that carry a large amount (e.g., more than five grams), or at least warn them if they try to do this. It’s better to send a small amount first, then create a new smart contract, and then send a larger amount.







External Messages



External messages are sent externally to smart contracts located on the TON blockchain to force them to perform certain actions. For example, the wallet’s smart contract expects to receive external messages containing orders (for example, internal messages that will be sent from the wallet’s smart contract) signed by the wallet owner; when such an external message is received by the wallet’s smart contract, it first verifies the signature, then receives the message (by launching the TVM ACCEPT primitive), and then performs all necessary actions.







Please note that all external messages must be protected from replay attacks. Validators typically remove an external message from the pool of proposed external messages (received from the network); however, in some situations, another validator may process the same external message twice (thus creating a second transaction for the same external message, which leads to duplication of the original action). Even worse, an attacker could extract an external message from a block containing a processing transaction and resend it later. This can cause, for example, a smart wallet contract to repeat the payment.







The easiest way to protect smart contracts from sniffing attacks related to external messages is to store the 32-bit cur-seqno counter in the constant data of the smart contract and wait for the req-seqno value in the (signed part) of any incoming external messages. Then the external message is accepted (ACCEPTed - a hint of the primitive ACCEPT) only if the signature is valid and req-seqno is equal to cur-seqno . After successful processing, the value of cur-seqno in persistent data is increased by one, so the same external message will never be received again.







You can also include the expire-at field in an external message and accept the message only if the current Unix time is less than the value of this field. This approach can be used in combination with seqno ; alternatively, the receiving smart contract can store the set (hashes) of all the last (not expired) received external messages in its permanent data and reject a new external message if it is a duplicate of one of the saved messages. You should also implement the collection and removal of expired messages in this set to avoid unlimited growth of persistent data.







As a rule, an external message starts with a 256-bit signature (if necessary), a 32-bit req-seqno (if necessary), a 32-bit expire-at (if necessary) and, possibly, a 32-bit op and other necessary parameters in depending on op . The external message template does not have to be as standardized as the internal message template, since external messages are not used for interaction between different smart contracts (written by different developers and managed by different owners).







Get methods



It is expected that some smart contracts implement certain well-defined get-methods. For example, any dns resolver smart contract for TON DNS is expected to implement the dnsresolve get method. Custom smart contracts can define their specific get methods. Our only general recommendation at the moment is to implement the "seqno" get method (without parameters), which returns the current seqno of the smart contract, which uses sequence numbers to prevent playback attacks associated with incoming external methods, whenever such a method has meaning.







Dictionary:









What conclusions can be drawn based on what I read?



  1. You can send external messages to contracts to trigger an action.
  2. Attacks - there are, for example, replay attacks
  3. It’s worth making the seqno method to protect against replay attacks.
  4. For dns resolvers, the dnsresolve method
  5. You can store hashes of external messages to protect against attacks, but you need to delete them in time, for this it is worth using the expired_at field for external messages
  6. Non-return messages are only needed to create contracts; otherwise, all internal messages are return
  7. Request-response messages should contain the following fields: op, query_id - optional, and some more depending on the value of op
  8. You can attach text comments in UTF-8 format for people and “binary comments” for automatic reading and processing by third-party software.
  9. It’s worth handling exceptions and doing it wisely
  10. "A simple transmission message without comments" - must have an empty body
  11. High-order bit of request-response messages takes a value of 0 for request messages, and a value of 1 for response messages
  12. There are standard op values ​​for response messages for identifying errors
  13. If a response message is received with an unknown op, it should be ignored, that is, complete execution with code 0
  14. You have to pay for sending messages, for gas and for sending a response. At the same time, if he sent more than necessary, the excess will return in the answer.
  15. When you receive messages, it is always worth checking the bounced flag first.


Thank you for your attention, I will be glad to constructive feedback!








All Articles