Skip to content

Commit

Permalink
docs: clarify interactions of message sending functions and their mod…
Browse files Browse the repository at this point in the history
…es (#1634)

Also listed functions with implicit mode, plus made some minor fixes on
related pages

* docs: `keccak512()` and a note on limitations of `HASHEXT` instructions

* test: message, `RAWRESERVE`, then message with `SendRemainingBalance`

* docs: rewrite the intro to outbound message processing for clarity and separation of concerns

* slightly adjust description of `emptyMap()` function
  • Loading branch information
novusnota authored Feb 12, 2025
1 parent 4dc424d commit 081cbc5
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 33 deletions.
2 changes: 2 additions & 0 deletions dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support overriding constants and methods of BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
- Forbid traits inherit implicitly from BaseTrait: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
- Forbid the `override` modifier for constants without the corresponding super-constant: PR [#1591](https://github.com/tact-lang/tact/pull/1591)
- Check map types for `deepEquals` method: PR [#1718](https://github.com/tact-lang/tact/pull/1718)
- Remove "remainder" from error messages: PR [#1699](https://github.com/tact-lang/tact/pull/1699)
- Check map types for `deepEquals` method: PR [#1718](https://github.com/tact-lang/tact/pull/1718)

Expand Down Expand Up @@ -126,6 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added description of `.boc`, `.ts`, `.abi`, `.pkg` files and completed Compilation page: PR [#1676](https://github.com/tact-lang/tact/pull/1676)
- Marked gas-expensive functions and expressions: PR [#1703](https://github.com/tact-lang/tact/pull/1703)
- Added a Security audits page, with the first assessment from the Trail of Bits: PR [#1791](https://github.com/tact-lang/tact/pull/1791)
- Listed functions with implicit mode and further clarified the interactions of message sending functions and their modes: PR [#1634](https://github.com/tact-lang/tact/pull/1634)

### Release contributors

Expand Down
12 changes: 9 additions & 3 deletions docs/src/content/docs/book/assembly-functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ When there are literals involved, they'll be shown as is. Additionally, when val

```tact
// Computes and returns the Keccak-256 hash as an 256-bit unsigned `Int`
// from a passed `Slice` `s`. Uses the Ethereum-compatible implementation.
// from a passed `Slice` `s`. Uses the Ethereum-compatible* implementation.
asm fun keccak256(s: Slice): Int {
// s:Slice → s:Slice, 1
// —————————————————————
Expand All @@ -498,8 +498,8 @@ The [`HASHEXT_KECCAK512`](https://docs.ton.org/v3/documentation/tvm/instructions

```tact
// Computes and returns the Keccak-512 hash in two 256-bit unsigned `Int`
// values from a passed `Slice` `s`. Uses the Ethereum-compatible implementation.
asm fun keccak256(s: Slice): Hash512 {
// values from a passed `Slice` `s`. Uses the Ethereum-compatible* implementation.
asm fun keccak512(s: Slice): Hash512 {
// s:Slice → s:Slice, 1
// —————————————————————
// s0 → s1 s0
Expand All @@ -520,6 +520,12 @@ asm fun keccak256(s: Slice): Hash512 {
struct Hash512 { h1: Int; h2: Int }
```

While it is said that these sample `keccak256(){:tact}` and `keccak512(){:tact}` functions use the Ethereum-compatible implementation, note that the underlying `HASHEXT` family of [TVM][tvm] instructions has its own drawbacks.

These drawbacks stem from the limitations of the [`Slice{:tact}`][slice] type itself — `HASHEXT_KECCAK256` and other hashing instructions of the `HASHEXT` family ignore any references present in the passed slice(s), i.e. only up to $1023$ bits of its data are used.

To work around this, you can recursively load all the refs from the given [`Slice{:tact}`][slice], and then hash them all at once by specifying their exact number instead of the `ONE` [TVM][tvm] instruction used earlier. See an example below: [`onchainSha256`](#onchainsha256).

:::note[Useful links:]

[`HASHEXT_KECCAK256`](https://docs.ton.org/v3/documentation/tvm/instructions#F90403)\
Expand Down
11 changes: 8 additions & 3 deletions docs/src/content/docs/book/maps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,19 @@ No other [allowed key or value types](#allowed-types) besides [`Int{:tact}`][int

### Declare, `emptyMap()` {#emptymap}

As a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library:
```tact
// K and V correspond to the key and value types of the target map
fun emptyMap(): map<K, V>;
```

Declaring a map as a [local variable](/book/statements#let), using `emptyMap(){:tact}` function of standard library:

```tact
let fizz: map<Int, Int> = emptyMap();
let fizz: map<Int, Int> = null; // identical to the previous line, but less descriptive
```

As a [persistent state variable](/book/contracts#variables):
Declaring a map as a [persistent state variable](/book/contracts#variables):

```tact
contract Example {
Expand Down Expand Up @@ -412,7 +417,7 @@ There, both maps are formed manually and both contain the same key-value pair. I

```tact
// K and V correspond to the key and value types of the given map
extends fun asCell(self: map<K, V>): Cell;
extends fun asCell(self: map<K, V>): Cell?;
```

On [TVM][tvm], maps are represented as a [`Cell{:tact}`][cell] type and it's possible to construct and parse them directly. However, doing so is highly error-prone and quite messy, which is why Tact provides maps as a standalone composite type with many of the helper methods mentioned above.
Expand Down
23 changes: 16 additions & 7 deletions docs/src/content/docs/book/message-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@ As it was previously mentioned, messages sent via [`send(){:tact}`](/ref/core-co

It's possible to use raw [`Int{:tact}`][int] values and manually provide them for the `mode`, but for your convenience there's a set of constants which you may use to construct the compound `mode` with ease. Take a look at the following tables for more information on base modes and optional flags.

Note that there are other [message sending functions](/book/send#message-sending-functions) — they do not use the `SendParameters{:tact}` [Struct](/book/structs-and-messages#structs), but accept the `mode` as one of their parameters.
Note that there are other [message-sending functions](/book/send#message-sending-functions) — they do not use the `SendParameters{:tact}` [Struct](/book/structs-and-messages#structs), but accept the `mode` as one of their parameters.

## Base modes

Mode value | Constant name | Description
---------: | :---------------------------- | -----------
$0$ | <Badge text="Since Tact 1.6 (not released yet)" variant="tip"/> `SendDefaultMode{:tact}` | Ordinary message (default).
$64$ | `SendRemainingValue{:tact}` | Carry all the remaining value of the inbound message in addition to the value initially indicated in the new message.
$128$ | `SendRemainingBalance{:tact}` | Carry all the remaining balance of the current smart contract instead of the value originally indicated in the message.
$1024$ | <Badge text="Since Tact 1.5" variant="tip"/> `SendOnlyEstimateFee{:tact}` | Doesn't send the message, only estimates the forward fees if the [message sending function](/book/send#message-sending-functions) computes those.
$128$ | <Badge text="Use with caution" title="Careless use can result in a total balance loss" variant="danger"/> `SendRemainingBalance{:tact}` | Carry **all the remaining balance** of the current smart contract instead of the value originally indicated in the message.
$1024$ | <Badge text="Since Tact 1.5" variant="tip"/> `SendOnlyEstimateFee{:tact}` | Doesn't send the message, only estimates the forward fees if the [message-sending function](/book/send#message-sending-functions) computes those.

The base mode `SendRemainingValue{:tact}` does **not** take previous actions into account, i.e. it doesn't recalculate the remaining value of the incoming message based on previously sent messages or performed actions during [action phase](https://docs.ton.org/learn/tvm-instructions/tvm-overview#transactions-and-phases).

Unlike `SendRemainingValue{:tact}`, the base mode `SendRemainingBalance{:tact}` always calculates the current value of the contract balance, which can help solve problems with [complex outbound message processing](/book/send#outbound-message-processing).

However, be **very** careful when using `SendRemainingBalance{:tact}`, because it works with the balance of the whole contract and any mistake with it can lead to a total loss of funds.

## Optional flags

Expand Down Expand Up @@ -50,16 +56,19 @@ send(SendParameters{
});
```

Note that there can be only **one** [base mode](#base-modes), but number of [optional flags](#optional-flags) may vary: you can use them all, none or just some.

:::caution

Note, that while adding ([`+{:tact}`](/book/operators#binary-add)) base modes together with optional flags is possible, it is discouraged due to the possibility of excess values. Use the bitwise OR ([`|{:tact}`](/book/operators#binary-bitwise-or)) instead, as it's designed to work with such flag and bit manipulations of the `mode`.
While adding ([`+{:tact}`](/book/operators#binary-add)) base modes together with optional flags is possible, it is discouraged due to the possibility of excess values. Use the [bitwise OR `|{:tact}`](/book/operators#binary-bitwise-or) instead, as it's designed to work with such flag and bit manipulations of the `mode`.

:::

:::note
## Functions with implicit mode

Also note, that there can be only one [base mode](#base-modes), but number of [optional flags](#optional-flags) may vary: you can use them all, none or just some.
Some [message-sending functions](/book/send#message-sending-functions) do not allow to set a mode by passing an argument. That's because their internal logic requires specific fixed set of modes to be used instead:

:::
* [`emit(){:tact}`](/ref/core-common#emit) sends a message with the `SendDefaultMode{:tact}` ($0$).
* [`self.reply(){:tact}`](/ref/core-base#self-reply), [`self.notify(){:tact}`](/ref/core-base#self-notify), and [`self.forward(){:tact}`](/ref/core-base#self-forward) all use the `SendRemainingValue{:tact}` mode unless the [`self.storageReserve{:tact}`](/ref/core-base#self-storagereserve) constant is overwritten to be greater than $0$, in which case they attempt to use the `SendRemainingBalance{:tact}` mode.

[int]: /book/integers
111 changes: 101 additions & 10 deletions docs/src/content/docs/book/send.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Sending messages
description: "TON Blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send messages."
---

TON blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send messages.
TON Blockchain is message-based — to communicate with other contracts and to deploy new ones you need to send messages.

Messages in Tact are commonly composed using a built-in [Struct](/book/structs-and-messages#structs) `SendParameters{:tact}`, which consists of:

Expand Down Expand Up @@ -101,9 +101,11 @@ send(SendParameters{

## Outbound message processing

Each transaction on TON Blockchain consists of [multiple phases][phases]. Outbound messages are evaluated in [compute phase][compute], but are **not** sent in that phase. Instead, they're queued in order of appearance for the [action phase][phases], where all actions listed in [compute phase][compute], like outbound messages or [reserve requests](/ref/core-advanced#nativereserve), are executed.
Each transaction on TON Blockchain consists of [multiple phases][phases]. Outbound messages are evaluated in [compute phase][compute], but are **not** sent in that phase. Instead, they're queued for execution in the [action phase][phases] in order of their appearance in the compute phase.

As all the values are computed in [compute phase][compute], all the fees computed by the end of it, and exceptions do not revert the transaction during [action phase][phases], outbound message sends can fail without bounce due to insufficient [action fees](https://docs.ton.org/develop/howto/fees-low-level#action-fee) or [forward fees][fwdfee].
Outgoing message sends may fail in [action phase][phases] due to insufficient [action fees](https://docs.ton.org/develop/howto/fees-low-level#action-fee) or [forward fees][fwdfee], in which case they won't bounce and **won't revert** the transaction. This can happen because all values are calculated in the [compute phase][compute], all fees are computed by the end of it, and exceptions do not roll back the transaction during the action phase.

To skip or ignore the queued messages at the [action phase][phases] in case they cannot be sent, set the optional [`SendIgnoreErrors{:tact}`](/book/message-mode#optional-flags) flag when composing the message.

Consider the following example:

Expand All @@ -112,40 +114,128 @@ Consider the following example:
contract FailureIsNothingButAnotherStep {
// And all the funds it gets are obtained from inbound internal messages
receive() {
// 1st outbound message evaluated and queued (but not sent yet)
// 1st outbound message evaluated and queued (but not yet sent)
send(SendParameters{
to: sender(),
value: ton("0.042"), // plus forward fee due to SendPayGasSeparately
mode: SendIgnoreErrors | SendPayGasSeparately,
// body is null by default
});
// 2nd outbound message evaluated and queued (but not sent yet, and never will be!)
// 2nd outbound message evaluated and queued,
// but not yet sent, and never will be!
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingValue | SendIgnoreErrors,
// body is null by default
});
}
} // exit code 37 during action phase!
}
```

There, the second message won't actually be sent:

* After finishing the [compute phase][compute], the remaining value $\mathrm{R}$ of the contract is computed.

* During the outbound message processing and assuming that there was enough value provided in the inbound message, the first message leaves $\mathrm{R} - (0.042 + \mathrm{forward\_fees})$ [nanoToncoins](/book/integers#nanotoncoin) on the balance.
* During the outbound message processing and assuming that there was enough value provided in the inbound message, the first message leaves $\mathrm{R} - (0.042 + \mathrm{forward\_fees})$ [nanoToncoins][nano] on the balance.

* When the second message is processed, contract tries to send $\mathrm{R}$ [nanoToncoins][nano], but fails to do so because there is already a smaller amount left.

* Thus, an error with [exit code 37](/book/exit-codes#37) is thrown: `Not enough Toncoin`.

Note that such failures are not exclusive to the [`send(){:tact}`](/ref/core-common#send) function and may also occur when using other [message-sending functions](#message-sending-functions).

For instance, let's replace the first call to the [`send(){:tact}`](/ref/core-common#send) function in the previous example with the [`emit(){:tact}`](/ref/core-common#emit) function. The latter queues the message using the default mode, i.e. $0$, and spends some [nanoToncoins][nano] to pay the [forward fees][fwdfee].

If a subsequent message is then sent with a [`SendRemainingValue{:tact}`](/book/message-mode#base-modes) base mode, it will cause the same error as before:

```tact
// This contract initially has 0 nanoToncoins on the balance
contract IfItDiesItDies {
// And all the funds it gets are obtained from inbound internal messages
receive() {
// 1st outbound message evaluated and queued (but not yet sent)
// with the mode 0, which is the default
emit("Have you seen this message?".asComment());
// 2nd outbound message evaluated and queued,
// but not yet sent, and never will be!
send(SendParameters{
to: sender(),
value: 0,
bounce: false, // brave and bold
mode: SendRemainingValue,
body: "Not this again!".asComment(),
});
} // exit code 37 during action phase!
}
```

:::note

To avoid dealing with similar cases and to simplify future [debugging sessions](/book/debug), consider having only one call to one of the [message-sending functions](#message-sending-functions) per [receiver function](/book/receive).

* When the second message is processed, contract tries to send $\mathrm{R}$ [nanoToncoins](/book/integers#nanotoncoin), but fails to do so because there is already a smaller amount left.
Alternatively, see the suggested solutions below.

:::

Previous examples discussed a case where the contract has $0$ [nanoToncoins][nano] on the balance, which is rather rare — in most real-world scenarios there would be some funds present. As such, it's usually better to use the [`SendRemainingBalance{:tact}`](/book/message-mode#base-modes) base mode, paired with the _necessary_ call to the [`nativeReserve(){:tact}`](/ref/core-advanced#nativereserve) function.

Like outbound messages, [reserve requests](/ref/core-advanced#nativereserve) are queued during the [compute phase][compute] and executed during the [action phase][phases].

```tact
// This contract has some Toncoins on the balance, say, 0.2 or more
contract MyPrecious {
// The extra funds can be received via a "topup" message
receive("topup") {}
// And the rest of the logic is expressed here
receive() {
// 1st outbound message evaluated and queued (but not yet sent)
// with the mode 0, which is the default
emit("Have you seen this message?".asComment());
// Let's try to keep the most out of the balance prior to this transaction
// Notice that nativeReserve() only queues an action to be performed during action phase
nativeReserve(ton("0.05"), ReserveAtMost | ReserveAddOriginalBalance);
// ----------- ------------- -------------------------
// ↑ ↑ ↑
// | | keeping the balance from before the compute phase start
// | might keep less, but won't fail in doing so
// just a tad more on top of the balance, for the fees
// 2nd outbound message evaluated and queued
// with the SendRemainingBalance mode
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingBalance, // because of the prior nativeReserve(),
// using this mode is safe and we'll keep
// the original balance plus a little more
body: "I give you my all! Well, all that's not mine!".asComment(),
});
}
}
```

Instead, if you want all outgoing messages to preserve a fixed amount of funds on the balance and **send the rest of the balance**, consider using one of the following functions instead. Note that they require a prior override of the [`self.storageReserve{:tact}`](/ref/core-base#self-storagereserve) constant:

* [`self.reply(){:tact}`](/ref/core-base#self-reply)
* [`self.notify(){:tact}`](/ref/core-base#self-notify)
* [`self.forward(){:tact}`](/ref/core-base#self-forward)

If you take only one thing away from this section, please remember this: be very careful with the [base modes](/book/message-mode#base-modes) of the message-sending functions, including the [implicitly set modes](/book/message-mode#functions-with-implicit-mode).

## Message sending limits

In total, there could be no more than $255$ actions queued for execution, which means that the maximum allowed number of messages sent per transaction is $255$.

Attempts to queue more throw an exception with an [exit code 33](/book/exit-codes#33) during [action phase][phases]: `Action list is too long`.

## Message sending functions
## Message-sending functions

Read more about all message sending functions in the Reference:
Read more about all message-sending functions in the Reference:

* [`send(){:tact}`](/ref/core-common#send)
* [`emit(){:tact}`](/ref/core-common#emit)
Expand All @@ -159,6 +249,7 @@ Read more about all message sending functions in the Reference:
[int]: /book/integers
[cell]: /book/cells#cells
[opt]: /book/optionals
[nano]: /book/integers#nanotoncoin

[phases]: https://docs.ton.org/learn/tvm-instructions/tvm-overview#transactions-and-phases
[compute]: https://docs.ton.org/learn/tvm-instructions/tvm-overview#compute-phase
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/ref/core-advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ Attempts to queue more than $255$ messages throw an exception with an [exit code

## nativeReserve

<Badge text="Gas-expensive" title="Uses 500 gas units or more" variant="danger" size="medium"/><p/>

```tact
fun nativeReserve(amount: Int, mode: Int);
```
Expand Down
Loading

0 comments on commit 081cbc5

Please sign in to comment.