跳到主要内容

发送消息

消息的组成、解析和发送位于TL-B schemas交易阶段和TVM的交汇处。

事实上,FunC有send_raw_message函数,该函数期望一个序列化消息作为参数。

由于TON是一个功能广泛的系统,支持所有这些功能的消息可能看起来相当复杂。尽管如此,大多数情况下并不使用那么多功能,消息序列化在大多数情况下可以简化为:

  cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();

因此,开发者不应该害怕,如果这份文档中的某些内容在第一次阅读时看起来难以理解,也没关系。只需把握总体思路。

让我们深入了解!

消息类型

有三种类型的消息:

  • 外部消息 — 从区块链外部发送到区块链内部智能合约的消息。这类消息应该在所谓的credit_gas阶段被智能合约明确接受。如果消息未被接受,节点不应该将其纳入进区块或转发给其他节点。
  • 内部消息 — 从一个区块链实体发送到另一个区块链实体的消息。与外部消息不同,这类消息可以携带一些TON并为自己支付费用。接收此类消息的智能合约可能没有接受它,在这种情况下,消息价值中的gas将被扣除。
  • 日志 — 从区块链实体发送到外部世界的消息。一般来说,没有将这类消息发送出区块链的机制。实际上,尽管网络中的所有节点对是否创建了消息达成共识,但没有关于如何处理它们的规则。日志可能被直接发送到/dev/null,记录到磁盘,保存到索引数据库,甚至通过非区块链手段(电子邮件/Telegram/短信)发送,所有这些都取决于给定节点的自行决定。

消息布局

我们将从内部消息布局开始。

描述智能合约可以发送的消息的TL-B方案如下:

message$_ {X:Type} info:CommonMsgInfoRelaxed 
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = MessageRelaxed X;

让我们用语言来描述。任何消息的序列化都包括三个字段:info(某种标题,描述来源、目的地和其他元数据)、init(仅在消息初始化时需要的字段)和body(消息有效载荷)。

MaybeEither和其他类型的表达式意味着以下内容:

  • 当我们有字段info:CommonMsgInfoRelaxed时,意味着CommonMsgInfoRelaxed的序列化直接注入到序列化cell中。
  • 当我们有字段body:(Either X ^X)时,意味着当我们(反)序列化某种类型X时,我们首先放置一个either位,如果X被序列化到同一cell,则为0,如果它被序列化到单独的cell,则为1
  • 当我们有字段init:(Maybe (Either StateInit ^StateInit))时,意味着我们首先放置01,要取决于这个字段是否为空;如果不为空,我们序列化Either StateInit ^StateInit(再次,放置一个either位,如果StateInit被序列化到同一cell则为0,如果被序列化到单独的cell则为1)。

CommonMsgInfoRelaxed的布局如下:

int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddress dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;

ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;

让我们现在专注于int_msg_info。 它以1位的前缀0开始,然后有三个1位的标志位,分别表示是否禁用即时超立方路由(目前始终为真)、是否在处理过程中出错时弹回消息,以及消息本身是否是弹回的结果。然后序列化来源和目的地址,接着是消息值和四个与消息转发费用和时间有关的整数。

如果消息是从智能合约发送的,其中一些字段将被重写为正确的值。特别是,验证者将重写bouncedsrcihr_feefwd_feecreated_ltcreated_at。这意味着两件事:首先,另一个智能合约在处理消息时可以信任这些字段(发送者无法伪造来源地址、bounced标志位等);其次,在序列化时我们可以将任何有效值放入这些字段中(无论如何这些值都将被重写)。

消息的直接序列化如下所示:

  var msg = begin_cell()
.store_uint(0, 1) ;; tag
.store_uint(1, 1) ;; ihr_disabled
.store_uint(1, 1) ;; allow bounces
.store_uint(0, 1) ;; not bounced itself
.store_slice(source)
.store_slice(destination)
;; serialize CurrencyCollection (see below)
.store_coins(amount)
.store_dict(extra_currencies)
.store_coins(0) ;; ihr_fee
.store_coins(fwd_value) ;; fwd_fee
.store_uint(cur_lt(), 64) ;; lt of transaction
.store_uint(now(), 32) ;; unixtime of transaction
.store_uint(0, 1) ;; no init-field flag (Maybe)
.store_uint(0, 1) ;; inplace message body flag (Either)
.store_slice(msg_body)
.end_cell();

然而,开发者通常使用快捷方式而不是逐步序列化所有字段。因此,让我们考虑如何使用elector-code中的示例从智能合约发送消息。

() send_message_back(addr, ans_tag, query_id, body, grams, mode) impure inline_ref {
;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(grams)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(ans_tag, 32)
.store_uint(query_id, 64);
if (body >= 0) {
msg~store_uint(body, 32);
}
send_raw_message(msg.end_cell(), mode);
}

首先,它将0x18值放入6位,即放入0b011000。这是什么?

  • 第一位是0 — 1位前缀,表示它是int_msg_info

  • 然后有3位110,表示即时超立方路由被禁用,消息可以在处理过程中出错时回弹,消息本身不是回弹的结果。

  • 然后应该是发送者地址,但由于它无论如何都会被重写,因此可以存储任何有效地址。最短的有效地址序列化是addr_none的序列化,它序列化为两位字符串00

因此,.store_uint(0x18, 6)是序列化标签和前4个字段的优化后的方式。

下一行序列化目的地址。

然后我们应该序列化值。一般来说,消息值是一个CurrencyCollection对象,其方案如下:

nanograms$_ amount:(VarUInteger 16) = Grams;

extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
= ExtraCurrencyCollection;

currencies$_ grams:Grams other:ExtraCurrencyCollection
= CurrencyCollection;

这个方案意味着除了TON值之外,消息可能还携带了extra-currencies的字典。然而,目前我们可以忽略它,只假设消息值被序列化为“作为变量整数的nanotons数量”和“0 - 空字典位”。

事实上,在上面的选出代码中,我们通过.store_coins(grams)序列化代币数量,但接着只放置了长度等于1 + 4 + 4 + 64 + 32 + 1 + 1的零字符串。这代表着什么?

  • 第一个位表示空的extra-currencies字典。
  • 然后我们有两个长度为4位的字段。它们以VarUInteger 16编码为0。事实上,由于ihr_feefwd_fee将被重写,我们同样可以在那里放置零。
  • 然后我们将零放入created_ltcreated_at字段。这些字段也将被重写;然而,与费用不同,这些字段有固定长度,因此被编码为64位和32位长的字符串。
  • (我们已经序列化了消息头并传递到init/body)
  • 接下来的零位表示没有init字段。
  • 最后一个零位表示消息体将就地序列化。
  • 之后,消息体(具有任意布局)就完成了编码。

这样,我们执行了4个序列化原语,而不是单独序列化了14个参数。

完整方案

消息布局和所有构成字段的完整方案(以及TON中所有对象的方案)在block.tlb中呈现。

消息大小

cell大小

请注意,任何Cell最多可包含1023位。如果您需要存储更多数据,您应该将其分割成块并存储在引用cell中。

例如,如果您的消息体大小为900位长,您无法将其存储在与消息头相同的cell中。 实际上,除了消息头字段外,cell的总大小将超过1023位,在序列化过程中将出现cell溢出异常。在这种情况下,原本代表“就地消息体标志位(Either)”的0应该变成1,消息体应该存储在引用cell中。

由于某些字段具有可变大小,因此应小心处理这些事项。

例如,MsgAddress可以由四个构造器表示:addr_noneaddr_stdaddr_externaddr_var,长度从2位(对于addr_none)到586位(对于最大形式的addr_var)。nanotons的数量也是如此,它被序列化为VarUInteger 16。这意味着,4位指示整数的字节长度,然后指示整数本身的较前面的字节。这样,0 nanotons将被序列化为0b0000(4位编码着零字节长度字符串,然后是零字节),而100,000,000 TON(或100000000000000000 nanotons)将被序列化为0b100000000001011000110100010101111000010111011000101000000000000000000b1000表示8个字节长度,然后是8个字节其本身)。

消息模式

如您可能已经注意到,我们使用send_raw_message发送消息,除了消耗消息本身外,还接受mode(模式)。要了解最适合您需求的模式,请查看以下表格:

Mode描述
0普通消息
64除了新消息中最初指示的值之外,携带来自入站消息的所有剩余值
128携带当前智能合约的所有余额,而不是消息中最初指示的值
Flag描述
+1单独支付转账费用
+2忽略处理此消息期间的Action Phase出现的任何错误
+16在action失败的情况下 - 弹回交易。如果使用了+2则无效。
+32如果当前账户的结果余额为零,则必须销毁该账户(通常与Mode 128一起使用)

要为send_raw_message构建一个模式,您只需通过将Mode和Flag结合来组合它们。例如,如果您想发送常规消息并单独支付转账费用,请使用Mode0和Flag+1得到mode = 1。如果您想发送整个合约余额并立即销毁它,请使用Mode128和Flag+32得到mode = 160