发送消息
消息的组成、解析和发送位于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();
因此,开发者不用担忧,如果这份文档中的某些内容在第一次阅读时看起来难以理解,没有关系。只需把握总体思路即可。
有时文档中可能会提到**'gram'这个词,但大多是在代码示例中,它只是toncoin**的一个过时名称。
让我们深入了解!
消息类型
有三种类型的消息:
- 外部消息 — 从区块链外部发送到区块链内部智能合约的消息。这类消息应该在所谓的
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(消息有效载荷)。
Maybe
、Either
和其他类型的表达式意味着以下内容:
- 当我们有字段
info:CommonMsgInfoRelaxed
时,意味着CommonMsgInfoRelaxed
的序列化直接注入到序列化cell中。 - 当我们有字段
body:(Either X ^X)
时,意味着当我们(反)序列化某种类型X
时,我们首先放置一个either
位,如果X
被序列化到同一cell,则为0
,如果它被序列化到单独的cell,则为1
。 - 当我们有字段
init:(Maybe (Either StateInit ^StateInit))
时,意味着我们首先放置0
或1
,要取决于这个字段是否为空;如果不为空,我们序列化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位的标志位,分别表示是否禁用即时超立方路由(目前始终为真)、是否在处理过程中出错时弹回消息,以及消息本身是否是弹回的结果。然后序列化来源和目的地址,接着是消息值和四个与消息转发费用和时间有关的整数。
如果消息是从智能合约发送的,其中一些字段将被重写为正确的值。特别是,验证者将重写bounced
、src
、ihr_fee
、fwd_fee
、created_lt
和created_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位
1
、1
和0
,表示即时超立方路由被禁用,消息可以在处理过程中出错时回弹,消息本身不是回弹的结果。 -
然后应该是发送者地址,但由于它无论如何都会被重写,因此可以存储任何有效地址。最短的有效地址序列化是
addr_none
的序列化,它序列化为两位字符串00