TON:建议和最佳做法

本文是在TON区块链页面smc-guidelines.txt上发布的文档的翻译。 也许这将帮助某人朝着该区块链的发展迈出一步。 另外,最后我做了一个简短的总结。


内部讯息


智能合约通过发送所谓的内部消息相互交互。 当内部消息到达其指定的目的地时,将以目标帐户的名称创建常规交易,并根据指定的代码和该帐户的常量数据来处理内部消息(智能合约)。 特别地,处理事务可以创建一个或多个内部消息,其中一些可以寻址到正在处理的内部消息的源地址。 当将请求嵌入(封装)在内部消息中并发送到另一个处理该请求并将响应作为内部消息发送回的智能合约时,此方法可用于创建简单的“客户端-服务器应用程序”。


这种方法导致需要区分到“请求”和“响应”的内部消息(作为“查询”或“响应”),或者不需要任何其他处理(例如简单的汇款)。 另外,当答案到达时,必须有一种方法来了解它与哪个请求有关。


为了实现此目标,建议使用以下内部消息模板(请记住,TON区块链对消息正文没有施加任何限制,也就是说,这只是建议):


0)消息的主体可以嵌入消息本身,也可以存储在消息中引用的单独的单元格(cell *)中,如图TL-B片段所示(英语更容易理解:或存储在单独的位置消息引用的单元格,如TL-B方案片段所示):


message$_ {X:Type} ... body:(Either X ^X) = Message X; 

https://core.telegram.org/mtproto-在这里您可以阅读有关TL方案的信息)


接收方智能合约必须至少接受嵌入消息主体的内部消息(即使将它们放置在包含消息的单元格中-只要它们适合包含消息的单元格-尚不清楚这意味着什么,因此附上原始文本)。 如果协定在单独的单元格中接受消息正文(使用“正确的”构造函数(Either X ^X) ),则传入消息的处理不应依赖于嵌入消息正文的特定方法。 另一方面,绝对不要在单独的单元中支持消息正文以简化请求和响应,这是绝对合法的。


1)邮件正文通常以以下字段开头:


  • op -32位(大端)无符号整数,标识要执行的操作或要调用的智能合约方法。
  • query_id是所有内部问答消息中使用的64位(大端)无符号整数,用于标识响应与请求的关系(响应的query_id必须等于相应请求的query_id )。 如果op不是请求-响应方法(它调用不会期望响应的方法),则可以省略query_id
  • 消息正文的其余部分特定于op参数的每个受支持的值

2)如果op为零,则该消息是带有注释的简单传输消息。 注释包含在消息的其余部分中(没有query_id等,即从第5个字节开始(说明:如果query_id不是,则op字段占用前4个字节))。 如果它不是以字节0xff开头,则注释为文本1;); 它可以“按原样”显示给钱包的最终用户(在过滤掉无效和控制字符并检查这是有效的UTF-8字符串之后)。 例如,用户可以在此字段中指定从他们的钱包到另一用户的钱包的简单转移的目的。 另一方面,如果注释以字节0xff开头,则消息的其余部分为“二进制注释”,不应以文本形式(如果需要,仅以十六进制转储)形式显示给最终用户。 提议的二进制注释用法例如是在商店中包含用于付款的付款标识符,并由商店软件自动生成和处理。


大多数智能合约在收到“简单转移消息”时,不必执行重要动作或拒绝传入消息。 因此,当op变为零时,用于处理传入内部消息(通常称为recv_internal() )的智能合约函数应立即以代码0退出,表示成功(例如,如果未在智能合约中安装自定义处理程序,则抛出异常0例外)。 这将导致以下事实:通过邮件转账的金额将被记入收件人的帐户,而没有任何进一步的影响。


3)“没有注释的简单消息传输”的正文为空(即使没有op字段)。 以上注意事项适用于此类消息。 请注意,此类消息必须在消息单元中嵌入自己的主体。


4)我们期望请求消息的op字段的第一位(“高位”翻译为第一位,这可能是不正确的,但是稍后会清楚说明)为空,即该字段的值应在1 .. 2^31-1的范围内1 .. 2^31-1 ,并且对于响应消息,第一(高阶)位应等于1,即字段值在2^31 .. 2^32-1范围内。 如果消息既不是请求也不是响应(主体不包含query_id参数),则它必须包含op参数,其范围应与请求消息相同: 1 .. 2^31 - 1


5)有几个“标准”响应消息,其op为0xffffffff和0xffffffffe。 通常,从0xfffffff0到0xffffffff的op值保留用于此类标准答案。


  • op = 0xffffffff表示“不支持该操作”。 其后是从原始查询中提取的64位query_id ,以及原始查询的32位op 。 除最简单的智能合约外,所有其他合约在收到未知操作的范围为1 ... 2 ^ 31-1的请求时均应返回此错误。
  • op = 0xfffffffe表示“不允许操作”。 它后面原始查询的64位query_id ,然后是从原始查询中提取的32位op

请注意,未知的“答案”( op的范围为2 ^ 31 ... 2 ^ 32-1)应被忽略(特别是,您不应生成op等于0xffffffff的响应),以及意外的返回值(退回)-消息(设置了“退回”标志)。


支付处理请求和发送回复的费用


通常,如果智能合约要向另一个智能合约发送请求,则它必须为向目标智能合约发送内部消息(消息转发费),在目的地处理此消息(加油费:煤气费),并在需要时发送回复(邮件转发费)。


在大多数情况下,发件人会将少量的克(例如1克)附加到内部消息上(足以支付处理此消息的费用),并在其上设置“ bounce”标志(即,它将发送可反弹的内部消息); 接收者将在返回值中返回未使用的部分以及答案(减去从中发送消息的费用)。 通常可以通过调用mode = 64的SENDRAWMSG来实现(请参见TON VM文档的附录A)。


如果收件人无法处理收到的消息,并且执行以非零的退出代码结束(例如,由于未处理的单元反序列化异常),则该消息将自动“退回”给发件人,并且“退回”标志将被取消选中并设置。标记为“反弹”。 退回邮件的正文将与原始邮件相同。 因此,重要的是在解析智能合约中的op字段并处理相应的请求之前检查传入的内部消息的“退回”标志(否则存在退回消息中包含的请求将由其原始发件人作为新的单独请求进行处理的风险)。 如果设置了“退回”标志,则特殊代码可以了解哪个请求失败(例如,通过退回退回的消息中的opquery_id来反序列化)并采取适当的措施。 一个更简单的智能合约可以简单地忽略所有返回的消息(如果设置了“退回”标志,则以零退出代码终止)。


另一方面,接收器可以成功解析传入的请求,并发现不支持所请求的op方法或已满足另一个错误条件。 然后,如上所述,应使用SENDRAWMSG(模式= 64)将op等于0xffffffff或其他适当值的响应发送回去。


在某些情况下,发件人是否想同时转移一定数量的钱? 给发件人? (在这里,显然是一个错误,并且是给“收件人”的),并且收到确认或错误消息。 例如,验证人选举智能合约会接收参加选举的请求以及投标作为增值。 在这种情况下,在估算值[成本]上附加一个额外的克是有意义的(这里“值”一词在任何地方都使用,在某种意义上说是采取某种行动的代价,所以我使用“成本”一词)。 如果发生错误(例如,由于任何原因而无法接受出价),则应将收到的全部金额(减去处理费)与错误消息一起退回给发件人(例如,使用模式= 64的SENDRAWMSG,如所述)以上)。 如果成功,将创建一条确认消息,并精确发送一克(从该值中减去消息传输费;这是SENDRAWMSG的mode = 1)。


使用不可退回的邮件


应该返回在智能合约之间发送的几乎所有内部​​消息(您可以将其翻译为“弹跳”,但为了不引起混淆,使用此术语会更容易),也就是说,它们必须具有“反弹”位不为空。 然后,如果目标智能合约不存在,或者在处理此消息时它创建了未处理的异常,则该消息将被“返回”,并承担其余的初始成本(价值)(减去所有传输消息和天然气的费用)。 返回的消息将具有相同的正文,但清除了“反弹”标志,并设置了“反弹”标志。 因此,所有智能合约都应检查所有传入消息的“退回”标志,并以静默方式接收它们(立​​即以零退出码终止)或执行某些特殊处理以确定哪个传出请求失败。 返回的消息正文中包含的请求不应执行。


在某些情况下,必须使用不可反弹的内部消息。 例如,如果没有发送至少一个不可撤销的内部消息,则无法创建一个新帐户。 如果此消息不包含带有新智能合约代码和数据的StateInit,则在不返回的内部消息中包含非空主体是没有意义的。


最好不要让最终用户(例如钱包)发送携带大量(例如超过5克)的不可撤消消息,或者如果他们尝试这样做,则至少要警告他们。 最好先发送少量金额,然后创建新的智能合约,然后再发送较大金额。


外部讯息


外部消息从外部发送到位于TON区块链上的智能合约,以强制它们执行某些操作。 例如,钱包的智能合约希望接收包含钱包所有者签名的包含订单的外部消息(例如,将从钱包的智能合约发送的内部消息); 当钱包的智能合约收到这样的外部消息时,它首先验证签名,然后接收消息(通过启动TVM ACCEPT原语),然后执行所有必要的操作。


请注意,必须保护所有外部消息免受重播攻击。 验证程序通常会从建议的外部消息池中(从网络接收)删除外部消息; 但是,在某些情况下,另一个验证程序可能会处理同一条外部消息两次(从而为同一条外部消息创建第二笔交易,从而导致原始操作重复)。 更糟糕的是,攻击者可以从包含处理事务的块中提取外部消息,然后稍后重新发送。 例如,这可能导致智能钱包合同重复付款。


保护智能合约免受与外部消息相关的嗅探攻击的最简单方法是将32位cur-seqno计数器存储在智能合约常量数据中,并等待所有传入外部消息的(带符号的部分) req-seqno值 。 然后,仅当签名有效且req-seqno等于cur-seqno时,才接受外部消息(接受-基本ACCEPT的提示)。 成功处理后,持久数据中的cur-seqno值将增加1,因此将不再收到相同的外部消息。


您还可以在外部消息中包括expire-at字段,并且仅在当前Unix时间小于此字段的值时才接受消息。 这种方法可以与seqno结合使用; 或者,接收方智能合约可以在其永久数据中存储所有最后(未过期)接收到的外部消息的集合(哈希),并在其与保存的消息之一重复的情况下拒绝新的外部消息。 您还应该在此集中实施过期消息的收集和删除,以避免持久数据的无限增长。


通常,外部消息以256位签名(如果需要),32位req-seqno (如果需要),32位expire-at (如果需要),以及可能的32位op和其他必需参数开头。取决于op 。 外部消息模板不必像内部消息模板一样标准化,因为外部消息不用于不同智能合约(由不同开发人员编写并由不同所有者管理)之间的交互。


获取方法


某些智能合约有望实现某些定义明确的获取方法。 例如,任何用于TON DNS的dns解析器智能合约都应实现dnsresolve get方法。 自定义智能合约可以定义其特定的获取方法。 目前,我们唯一的通用建议是实现“ seqno” get方法(不带参数),该方法返回智能合约的当前seqno ,该序列使用序号来防止与传入的外部方法相关联的播放攻击,只要该方法具有意义。


字典:


  • 信元-TVM信元最多包含1023位数据,并且最多包含对其他信元的四个引用。 TON区块链中的所有持久数据(包括TVM代码)都表示为TVM单元的集合(参见[1,2.5.14])。 -TVM信元由不超过1023位的数据和不超过四个与其他信元的链接组成。 TON区块链中的所有持久数据(包括TVM代码)都以一组TVM单元的形式呈现(参见[1,2.5.14])。 -TON虚拟机描述的摘录( https://test.ton.org/tvm.pdf

根据我的读物可以得出什么结论?


  1. 您可以向合同发送外部消息以触发操作。
  2. 攻击-例如,有重放攻击
  3. 值得采用seqno方法来防止重放攻击。
  4. dns解析器具有dnsresolve方法
  5. 您可以存储外部消息的哈希值以防止受到攻击,但是您需要及时删除它们,为此,值得对外部消息使用expired_at字段
  6. 仅创建合同需要非返回消息;否则,将返回所有内部消息
  7. 请求响应消息应包含以下字段: op,query_id-可选,还有更多取决于op的值
  8. 您可以附加人的UTF-8格式的文本注释和“二进制注释”,以便第三方软件自动阅读和处理。
  9. 值得处理异常并明智地处理异常
  10. “没有评论的简单消息”-必须为空
  11. 请求响应消息的高位对于请求消息取值为0,对于响应消息取值为1
  12. 响应消息具有标准的op值以识别错误
  13. 如果收到带有未知操作的响应消息,则应将其忽略,即使用代码0完整执行
  14. 您必须为发送消息,加油和发送响应付费。 同时,如果他发送的邮件超出了必要,多余的邮件将返回答案。
  15. 接收消息时,始终值得首先检查退回标志

感谢您的关注,我们很乐意提供建设性的反馈!

Source: https://habr.com/ru/post/zh-CN470772/


All Articles