一条消息的三种哈希
TON 消息以 bag of cells——BoC——的形式传输,而“消息哈希”这个说法,根据说话的人不同,可能指三种不同的哈希。TONNode 内存池流为每条待处理消息交付全部三种,名称如下:
cell_hash——根 cell 的表示哈希。这就是区块浏览器所说的消息哈希,消息被打包进区块后,它等于该交易的in_msg哈希。它只取决于 cell,而与 cell 被序列化成字节的方式无关。norm_hash——TEP-467 归一化哈希:把同一条外部入站消息改写成固定形式后的哈希——ext_in_msg_info$10 src:addr_none dest:<unchanged> import_fee:0 init:nothing body:right$1 ^body。消息被重新序列化,或其src、import_fee、init字段发生变化时,它保持不变。如果消息已经是这种形式,norm_hash等于cell_hash。sha256——对交付时的 BoC 字节原样计算的 SHA-256。同样的 cell 可以用几种合法方式序列化(带或不带索引,带或不带 CRC32C),每种序列化的 SHA-256 都不同,而两个 cell 哈希不会变。
工具把每一个都以 hex 和 base64 输出,因为各 API 并不一致:区块浏览器和 liteserver 协议使用 hex,一些 HTTP API 返回 base64。
归一化哈希为什么存在
外部消息由钱包签名,再由中继者交给网络。途中它可能被重新编码,中继者也可能添加或去掉 src 地址、import_fee 或附带的 init——这些都不在钱包签名的范围内。每一处改动都会产生新的根 cell 和新的 cell_hash,所以按 cell 哈希去重的节点会多次接受同一个已签名的载荷。TEP-467 固定了被哈希的形式,让所有这些变体归并为同一个身份,节点自己的 get_ext_in_msg_hash_norm(位于 validator/impl/external-message.cpp)正是这样计算的。
构造方法很简单:原样取目标地址的比特,源地址置零,手续费置零,丢弃 init,并把消息体放进引用——无论它原来是否在引用里。只有外部入站消息才有归一化哈希;对于内部消息或外部出站消息,工具显示另外两个哈希并如实说明。
内存池流携带什么
TONNode 内存池流的每个 msg 帧都携带 boc 本身、它的 size,以及上面定义的 cell_hash、norm_hash 和 sha256。请按 cell_hash 对你自己的处理去重:同一条消息可能到达两次,先是 early(未验证),然后是 checked,重连后还可能再来一次。sha256 是节点用来对收到的字节去重的键,这就是为什么它对交付的字节而非重新编码后的字节做哈希。
用真实消息测试过
这里的实现与流自身的解析器以完全相同的方式构造归一化 cell,代码仓库的测试套件把 72 条真实主网外部消息——流的测试数据集,每条都带有流交付的 cell_hash、norm_hash 和 sha256——输入其中,并断言三者全部匹配。内存池文档中的示例消息就是其中之一;粘贴它即可并排看到这三个值。
