交易生命周期

⚠️ Update Notice:

Please read Substrate to Polkadot SDK page first.


在 Substrate 中,交易包含要包含在区块中的数据。 由于交易中的数据源自运行时外部,因此交易有时更广泛地称为外部数据或外部函数。 但是,最常见的外部函数是签名交易。 因此,关于交易生命周期的讨论重点介绍了如何验证和执行签名交易。

您已经了解到签名交易包括发送请求以执行某些运行时调用的账户的签名。 通常,使用提交请求的账户的私钥对请求进行签名。 在大多数情况下,提交请求的账户还会支付交易费用。 但是,交易费用和交易处理的其他要素取决于运行时逻辑的定义方式。

交易的定义位置

运行时开发中所述,Substrate 运行时包含定义交易属性的业务逻辑,包括:

  • 什么构成有效交易。
  • 交易是作为签名交易还是未签名交易发送。
  • 交易如何更改链的状态。

通常,您使用模块来组合运行时函数,并实现您希望链支持的交易。 在编译运行时之后,用户将与区块链进行交互以提交请求,这些请求将作为交易进行处理。 例如,用户可能会提交一个请求,将资金从一个账户转移到另一个账户。 该请求将成为一个签名交易,其中包含该用户账户的签名,如果用户账户中有足够的资金支付交易费用,则交易将成功执行,并进行转移。

交易如何在区块生成节点上进行处理

根据网络的配置,您可能拥有授权生成区块的节点和未授权生成区块的节点的组合。 如果 Substrate 节点被授权生成区块,则它可以处理收到的签名交易和未签名交易。 下图说明了提交到网络并由生成节点处理的交易的生命周期。

交易生命周期概述

发送到非生成节点的任何签名交易或未签名交易都会被闲聊到网络中的其他节点,并进入它们的交易池,直到被生成节点接收。

验证和排队交易

共识中所述,网络中的大多数节点必须就区块中交易的顺序达成一致,才能就区块链的状态达成一致,并继续安全地添加区块。 为了达成共识,三分之二的节点必须就执行的交易的顺序以及由此产生的状态更改达成一致。 为了准备共识,交易首先在本地节点的交易池中进行验证和排队。

在交易池中验证交易

交易池使用在运行时定义的规则来检查每个交易的有效性。 这些检查确保只有满足特定条件的有效交易才会被排队以包含在区块中。 例如,交易池可能会执行以下检查以确定交易是否有效:

  • 交易索引(也称为交易 nonce)是否正确?
  • 用于对交易进行签名的账户是否有足够的资金支付相关的费用?
  • 用于对交易进行签名的签名是否有效?

在进行初始有效性检查之后,交易池会定期检查池中现有的交易是否仍然有效。 如果发现交易无效或已过期,则将其从池中删除。

交易池只处理交易的有效性和放置在交易队列中的有效交易的排序。 有关验证机制如何工作的具体细节(包括处理费用、账户或签名的处理方式)可以在validate_transaction 方法中找到。

将有效交易添加到交易队列

如果交易被识别为有效,则交易池会将交易移到交易队列中。 对于有效交易,有两个交易队列:

  • 就绪队列包含可以包含在新的待处理区块中的交易。 如果运行时使用 FRAME 构建,则交易必须按照它们在就绪队列中放置的顺序进行。
  • 未来队列包含将来可能变为有效的交易。 例如,如果交易的 nonce 对其账户来说过高,它可以等待在未来队列中,直到该账户的适当数量的交易被包含在链中。

无效交易处理

如果交易无效(例如,因为它太大或不包含有效的签名),则它会被拒绝,并且不会被添加到区块中。 交易可能因以下任何原因被拒绝:

  • 交易已被包含在区块中,因此它从验证队列中删除。
  • 交易的签名无效,因此它会被立即拒绝。
  • 交易太大,无法放入当前区块,因此它会被放回队列以进行新的验证轮次。

按优先级排序的交易

如果节点是下一个区块生成者,则节点使用优先级系统对下一个区块的交易进行排序。 交易按优先级从高到低排序,直到区块达到最大权重或长度。

交易优先级在运行时计算,并作为交易的标签提供给外部节点。 在 FRAME 运行时中,使用一个特殊的模块根据与交易相关的权重和费用来计算优先级。 此优先级计算适用于除内在交易之外的所有类型的交易。 内在交易始终使用EnsureInherentsAreFirst 特性放在第一位。

基于账户的交易排序

如果您的运行时使用 FRAME 构建,则每个签名交易都包含一个 nonce,每次特定账户进行新的交易时,该 nonce 都会递增。 例如,来自新账户的第一个交易的 nonce = 0,而来自同一账户的第二个交易的 nonce = 1。 区块生成节点可以在对要包含在区块中的交易进行排序时使用 nonce。

对于具有依赖关系的交易,排序会考虑交易支付的费用以及它包含的任何其他交易的依赖关系。 例如:

  • 如果有一个未签名交易的 TransactionPriority::max_value() 和另一个签名交易,则未签名交易将被放在队列中的第一位。
  • 如果有两个来自不同发送者的交易,则 priority 决定哪个交易更重要,应该首先包含在区块中。
  • 如果有两个来自相同发送者的交易,并且具有相同的 nonce:区块中只能包含一个交易,因此只有费用更高的交易才会被包含在队列中。

执行交易和生成区块

在将有效交易放入交易队列之后,一个单独的执行模块会协调如何执行交易以生成区块。 执行模块调用运行时模块中的函数,并按特定顺序执行这些函数。

作为运行时开发人员,了解执行模块如何与系统模块以及构成区块链业务逻辑的其他模块进行交互非常重要,因为您可以在执行模块执行以下操作时插入要执行的逻辑:

  • 初始化区块
  • 执行要包含在区块中的交易
  • 完成区块构建

初始化区块

为了初始化区块,执行模块首先调用系统模块中的 on_initialize 函数,然后调用所有其他运行时模块中的函数。on_initialize 函数使您能够定义在执行交易之前应该完成的业务逻辑。 系统模块 on_initialize 函数始终首先执行。 其余模块按它们在 construct_runtime! 宏中定义的顺序调用。

在所有 on_initialize 函数都执行完毕之后,执行模块会检查区块头中的父哈希和 Trie 根,以验证信息是否正确。

执行交易

在区块初始化之后,每个有效交易都按交易优先级顺序执行。 重要的是要记住,在执行之前不会缓存状态。 相反,状态更改在执行期间直接写入存储。 如果交易在执行过程中失败,则在失败之前发生的任何状态更改都不会被恢复,从而使区块处于不可恢复的状态。 在将任何状态更改提交到存储之前,运行时逻辑应该执行所有必要的检查,以确保外部函数将成功。

请注意,事件 也会写入存储。 因此,运行时逻辑不应该在执行补充操作之前发出事件。 如果交易在发出事件后失败,则不会恢复事件。

完成区块

在执行完所有排队的交易之后,执行模块会调用每个模块的 on_idleon_finalize 函数,以执行在区块结束时应该执行的任何最终业务逻辑。 模块再次按它们在 construct_runtime! 宏中定义的顺序执行,但在这种情况下,系统模块中的 on_finalize 函数最后执行。

在所有 on_finalize 函数都执行完毕之后,执行模块会检查区块头中的摘要和存储根是否与初始化区块时计算的结果匹配。

on_idle 函数还会传递区块的剩余权重,以允许根据区块链的使用情况进行执行。

区块生成和区块导入

到目前为止,您已经了解了交易是如何包含在本地节点生成的区块中的。 如果本地节点被授权生成区块,则交易生命周期将遵循类似于以下路径:

  1. 本地节点监听网络上的交易。
  2. 验证每个交易。
  3. 将有效交易放入交易池。
  4. 交易池按适当的交易队列对有效交易进行排序,执行模块调用运行时以开始下一个区块。
  5. 执行交易,状态更改存储在本地内存中。
  6. 生成的区块发布到网络。

在区块发布到网络之后,其他节点可以导入它。 区块导入队列是每个 Substrate 节点中外部节点的一部分。 区块导入队列监听传入的区块和与共识相关的消息,并将它们添加到池中。 在池中,会检查传入的信息的有效性,如果无效则将其丢弃。 在验证区块或消息有效之后,区块导入队列会将传入的信息导入本地节点的状态,并将其添加到节点知道的区块数据库中。

在大多数情况下,您不需要了解交易是如何被闲聊的,或者区块是如何被网络中的其他节点导入的。 但是,如果您打算编写任何自定义共识逻辑,或者想了解有关区块导入队列实现的更多信息,则可以在 Rust API 文档中找到详细信息。

下一步去哪里