不要低估 TON

本文深入探讨了TON区块链中费用计算的复杂性以及不正确的gas预估可能导致的严重问题。TON 使用异步消息传递和细粒度的费用模型,开发者需要仔细验证消息附带的 TON 数量,以确保能够支付所有可能的执行分支的费用,包括计算费、转发费和存储费,尤其是在处理回弹消息时。从 TVM 12 开始,回弹消息的成本增加,使得保守的费用预估变得更加重要。

返回概览

不要低估 TON:不正确的 gas 预估如何导致严重问题

2025年12月16日

TON 是一种新型区块链,它结合了复杂的费用系统和异步消息。我们探讨了 TON 中如何计算费用,以及为什么正确估计费用至关重要。

TON 和精细化费用

开放网络 (TON) 是一种 Layer-1 区块链,最初由 Telegram 设计,现在由一个独立的社区维护。它结合了分片架构、异步消息传递和高度精细化的费用模型,以支持高吞吐量的智能合约执行。其核心是 TON 虚拟机 (TVM),它使合约能够对存储和传输的数据进行细粒度、位级别的控制。

TON 使用精细化的费用模型,费用以网络的原生货币 Toncoin (TON) 支付。每个合约都有一个 TON 余额,消息可以携带 TON,方式类似于 EVM 中的 msg.value。费用可以从两个来源支付:传入消息值被计入之前的合约余额,或者消息值被计入之后的余额。在第一种情况下,合约必须已经持有足够的 TON 来支付费用,而与消息值无关。在第二种情况下,费用仅在转移的值添加到合约余额后才扣除。

一般来说,有三种类型的费用需要考虑:

计算费用

与以太坊类似,TVM 执行的每条指令都有 gas 单位的成本。执行成本取决于交易期间消耗的 gas 单位。一个 gas 单位的价格在区块链配置中是固定的,只能通过验证者投票来改变。

简而言之,计算费用与计算费用价格和计算消耗的 gas 成正比。

$comp\_fee = comp\_fee\_price \times gas\_consumed$ (简化)

除非合约明确同意从自己的余额中支付计算费用(TVM 有一个特殊的指令用于此),否则计算费用不得超过消息值。如果附加的资金不足以支付计算费用,则执行会回滚。

存储费用

这是在区块链上存储合约所需支付的金额。

这些费用与合约的大小成正比,并且在合约部署的每秒都会累积。当合约收到消息时,存储费用会被结算。

$storage\_fee = storage\_fee\_price \times contract\_size\_bits \times seconds\_elapsed\_since\_last\_settle$ (简化)

通常,存储费用从消息值被计入之前的合约余额中支付。因此,合约必须有足够的资金来支付自己的租金。当余额不足时,存储费用会作为债务累积,如果未偿还,最终可能导致合约从链中移除。

转发费用

转发费用是为在合约之间发送消息而支付的成本。

转发费用与转发费用价格和消息大小成正比。

$fwd\_fee = fwd\_fee\_price \times msg\_bits$ (简化)

对于计算费用,通常预计附加到消息的资金足以支付转发费用。如果合约无法支付转发费用,则执行会回滚。

异步消息

TON 消息是异步的,这意味着合约无法同步查询另一个合约,并在同一交易内立即根据结果继续执行。相反,响应可能会在稍后传递。

考虑以下示例,其中 Alice 的 USDC 钱包(合约 A)将代币发送到 Bob 的 USDC 钱包(合约 B)。

  • A 持有 10 USDC 的余额,B 持有 0 USDC 的余额。

  • Alice 调用 A.transfer(Bob, 8 USDC)。

  • A 计算 balance = 10 - 8 = 2 USDC

  • A 发送消息给 B。m = (transfer, 8 USDC)

  • B 接收 m,并通过计算 balance = 0 + 8 = 8 USDC 来增加其余额

  • 转移成功:A 持有 2 USDC 的余额,B 持有 8 USDC 的余额。

反弹消息(Bouncing messages)

现在让我们考虑 B 在处理 A 发送的消息时回滚的情况。与 EVM 不同,B 回滚不会导致 A 也原子性地回滚。因此,A 必须实现一种机制来检测 B 是否已回滚并采取相应措施。这是消息反弹的目标。

如果 B 在处理消息 $m$ 时回滚,则消息会被反弹,这意味着 $m$ 会被加上一个反弹标志并发送回发送者。

回到上面的例子,假设 Bob 的钱包 (B) 被列入黑名单,因此无法接收资金。

  • 如上所述,A 发送消息给 B。m = (transfer, 8 USDC)

  • B 接收 $m$ 并回滚,将反弹标志添加到 $m$ 前面,产生 m' = (BOUNCED, transfer, 8USDC)

  • A 接收 m',检测到它是来自 B 的反弹消息,并通过计算 balance = 2 + 8 = 10 USDC 来撤消转移

  • 转移失败,状态恢复到初始状态:A 持有 10 USDC 的余额,B 持有 0 USDC 的余额。

如所示,反弹消息及其正确的处理对于保持状态一致性至关重要。

反弹失败时

我们已经发现了反弹消息的作用有多重要,但我们也注意到发送消息需要足够的资金。当合约耗尽余额,因此无法发送反弹消息时,就会出现一个有问题的场景。考虑一下前面示例中的这种情况。

  • 如上所述,B 接收 $m$ 并回滚。但是,B 耗尽了资金,无法将反弹消息 m' 发送回 A。
  • A 没有收到 m',也无法检测到 B 已回滚。
  • 转移失败,但 A 的状态保持不变:A 持有 2 USDC 的余额,B 持有 0 USDC 的余额;8 USDC 已经丢失。

Gas 预估至关重要

为了防止上述情况发生,开发人员必须在调用路径的最开始验证附加到消息的 TON 数量。只有在附加的资金足以支付所有可能执行分支的所有费用时,合约执行才能继续进行。这确保了,无论执行如何展开,所有合约最终都将处于一致的状态。

在实践中,这意味着合约应该保守地估计 gas 使用量,并明确要求:

msg_value ≥ expected_compute_fee + expected_forwarding_fee

只有在满足此条件后,合约才能继续执行。

此外,如果预计执行流程会部署一个新的合约,则估算还必须包括足够的 TON 来支付新合约的存储费用。换句话说,部署应该使合约有足够的余额来支付其自身的租金一段时间,这段时间足以完成其目的。

为了估计这些费用,开发人员可以利用以下 TVM 指令:

  • GETGASFEE:将使用的 gas 单位作为输入,并返回相应的计算费用。开发人员可以通过对最昂贵的调用路径进行基准测试来测量消耗的 gas。
  • GETFORWARDFEE:将消息大小作为输入,并返回发送消息所支付的转发费用。为了计算总转发费用,开发人员应该估计可能的最大消息大小和最长调用路径中发送的消息总数。
关于 TVM 12 的说明

从 TVM 12 开始,由于反弹消息的序列化和转发方式的更改,反弹消息的成本有所增加。特别是,反弹消息现在包含更多上下文数据,这增加了它们的大小,并因此增加了它们的转发费用。开发人员应考虑在包含反弹消息的调用路径中更高的转发成本。

要点

  • TON 依赖于异步消息传递,因此跨合约的状态一致性取决于对反弹消息的正确处理,而不是原子回滚。
  • 转发、计算和存储费用是独立收取的,并且可以来自不同的余额来源,这使得费用核算变得不简单。
  • 如果合约无法负担发送反弹消息的费用,则可能会发生静默失败,从而导致状态不一致和可能的资金损失。
  • 开发人员必须始终验证附加的消息值是否涵盖所有可能执行分支的计算和转发费用,然后再继续执行。
  • 从 TVM 12 开始,反弹路径的成本更高,这使得保守的费用估算变得更加重要。

外部链接

  • 原文链接: chainsecurity.com/blog/t...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
chainsecurity
chainsecurity
江湖只有他的大名,没有他的介绍。