53 个版本 (34 个重大更新)

37.0.0 2024年7月18日
35.0.0 2024年6月21日
34.0.0 2024年5月23日
33.0.0 2024年5月1日
2.0.0-alpha.52020年3月24日

#1423 in 神奇豆子

Download history 2729/week @ 2024-04-26 2313/week @ 2024-05-03 2195/week @ 2024-05-10 2458/week @ 2024-05-17 3044/week @ 2024-05-24 3038/week @ 2024-05-31 2471/week @ 2024-06-07 2066/week @ 2024-06-14 3309/week @ 2024-06-21 1628/week @ 2024-06-28 932/week @ 2024-07-05 2449/week @ 2024-07-12 2533/week @ 2024-07-19 2043/week @ 2024-07-26 2274/week @ 2024-08-02 2398/week @ 2024-08-09

9,798 每月下载
用于 79 个crate (直接使用10个)

GPL-3.0-or-later…

1.5MB
23K SLoC

Substrate 交易池实现。

许可证: GPL-3.0-or-later WITH Classpath-exception-2.0

问题陈述

交易池负责维护一组可能被区块作者包含在即将到来的区块中的交易。交易可以是通过网络(由其他节点传播)或通过RPC(本地提交)接收。

池的主要任务是准备一个有序的交易列表供区块作者模块使用。同一列表对于向其他节点传播也很有用,但请注意,传播的交易必须是完全相同的并不是强制要求(见以下实现说明)。

区块作者的激励机制在于存储和排序交易,以

  1. 最大化区块作者的利润(产生的区块价值)
  2. 最小化区块作者的工作量(产生区块的时间)

对于FRAME而言,第一个属性仅仅确保每单位重量的费用是最高的(高tip值),第二个属性是关于避免喂养那些不能成为下一个区块一部分的交易(它们无效、过时等)。

从交易池的角度来看,交易只是字节序列的不可见块,需要通过TaggedTransactionQueue运行时API查询以验证交易的简单正确性并提取任何有关交易如何与其他交易在池中以及当前链上状态相关联的信息。只有有效的交易应存储在池中。

每个导入的区块都可能影响池中已存在交易的合法性。区块作者期望从池中获得有关可以包含在即将构建的区块中的交易的最最新信息。确保这一属性的过程称为修剪。在修剪过程中,池应删除运行时(在当前最佳导入的区块中查询)认为无效的交易。

由于区块链不总是线性的,交易池也需要正确处理分叉。在分叉的情况下,一些区块从主链中被撤回,而其他一些区块在某个共同祖先之上被实施。撤回的区块中的交易可以简单地被丢弃,但最好确保如果它们在最近的实施区块(即链重新组织后的分叉)中被运行时状态认为是有效的,它们仍然被考虑在内。

交易池还应提供一种方式来跟踪交易在池中的生命周期,包括其广播状态、区块包含、最终确定等。

交易有效性细节

从运行时检索的信息封装在TransactionValidity类型中。

pub type TransactionValidity = Result<ValidTransaction, TransactionValidityError>;

pub struct ValidTransaction {
  pub requires: Vec<TransactionTag>,
  pub provides: Vec<TransactionTag>,
  pub priority: TransactionPriority,
  pub longevity: TransactionLongevity,
  pub propagate: bool,
}

pub enum TransactionValidityError {
  Invalid(/* details */),
  Unknown(/* details */),
}

我们现在将逐一介绍每个参数,以了解它们对交易排序的要求。

运行时应以确定性的方式返回这些值。在给出完全相同的状态的情况下多次调用API必须返回相同的结果。字段特定规则将在下面描述。

requires / provides

这两个字段包含与给定交易相关联的一组TransactionTag(不可见块)。这是运行时能够表达交易之间依赖关系(此交易池可以考虑到)的一种机制。通过查看这些字段,我们可以确定交易是否适合包含在区块中。

provides集包含在交易成功添加到区块时将得到满足的属性。只有区块中的交易可以提供特定的标签。requires包含在交易可以包含到区块之前必须满足的属性。

请注意,具有空requires集的交易可以立即添加到区块中,没有其他交易期望在它之前被包含。

对于某些给定的交易系列,providesrequires字段将创建一个(简单的)有向无环图。如果图中的没有其他额外的requires标签(即它们的所有依赖都已经被满足),则应首先考虑将其包含在区块中。对于多个已准备好包含在区块中的交易,应按priority(见下文)进行排序。

请注意,将交易包含到区块中的过程基本上是构建图,然后选择“最佳”源顶点(交易)具有所有满足的标签,并从图中移除它。

示例

  • 在类似比特币的链中,交易将provide生成的UTXO并将require它仍然等待的UTXO(注意这并不一定是所有需要输入,因为其中一些可能已经可以花费(即UTXO处于状态))

  • 基于账户的链中的交易将提供(作为单个标签)provide一个(sender, transaction_index/nonce),并在on_chain_nonce < nonce - 1的情况下需要(sender, nonce - 1)

规则与注意事项

  • provides不能为空
  • 具有重叠provides标签的交易是互斥的
  • 在包含提供该标签的交易后,检查需要标签A的交易有效性时,不应再次返回Arequires
  • 运行时开发者应避免重复使用provides标签(即它应该是唯一的)
  • 交易依赖中不应存在循环
  • 注意事项:即使没有requires标签,链上状态条件也可能使交易无效
  • 注意事项:即使有一些requires标签,链上状态条件也可能使交易有效
  • 注意事项:将交易包含到链中可能会立即使它们重新有效(例如,UTXO交易进入,但由于我们不存储已花费的输出,它将再次有效,等待相同的输入/标签得到满足)

优先级

交易优先级描述了交易相对于池中其他交易的重要性。区块作者可以期望在其他人之前包含此类交易以获得利益。

请注意,我们不能简单地将池中的交易按priority排序,因为首先我们需要确保所有交易的要求都得到满足(请参阅requires/provides部分)。然而,如果我们考虑一组所有要求(标签)都得到满足的交易,区块作者应该首先选择优先级最高的交易包含到下一个区块中。

priority可以是介于0(最低包含优先级)到u64::MAX(最高包含优先级)之间的任何数字。

规则与注意事项

  • 交易优先级可能会随时间变化
  • 链上条件可能会影响priority
  • 对于具有重叠provides标签的两个交易,应优先考虑优先级较高的交易。然而,我们还可以查看以该交易为根的子树的总优先级,并比较该优先级(即尽管交易本身的优先级较低,但它“解锁”了其他高优先级交易)。

持久性

持久性描述了交易预期有效的时长(以区块计)。此参数仅向交易池提供有关当前交易可能仍然有效的提示。请注意,这并不保证交易在所有时间内都有效。

规则与注意事项

  • 交易持久性可能会随时间变化
  • 链上条件可能会影响longevity
  • 在持久性到期后,交易可能仍然有效

传播

此参数指示池将交易传播/传播给节点对等方。默认情况下,这应该是true,但在某些情况下可能不希望进一步传播交易。例如,这可能会包括由区块作者在链外工作者中产生的重交易(DoS)或冒被他人抢先的风险,在找到一些非平凡解决方案或歧义之后,等等。

'交易来源`

为了让运行时能够区分正在验证的交易是通过网络接收的,还是使用本地RPC提交的,或者它只是正在导入的区块的一部分,交易池应将额外的 TransactionSource 参数传递给运行时调用有效性函数。

这可以供运行时开发者快速拒绝那些例如不应该在网络中传播的交易。

无效 交易

如果运行时返回一个 Invalid 错误,这意味着该交易根本不能添加到区块中。提取无效的实际原因可以提供更多关于来源的细节。例如,Stale 交易仅表示交易已被包含在区块中,而 BadProof 则表示无效签名。无效性也可能是临时的。在 ExhaustsResources 的情况下,交易不适合当前区块,但它可能适合下一个区块。

未知 交易

Unknown 有效性情况下,运行时无法确定交易在当前区块中是否有效。然而,这种情况可能是临时的,因此预计交易将在未来重试。

实现

理想的交易池应仅存储运行时在当前最佳导入区块中认为是有效的交易。在每次导入区块后,池应该

  1. 重新验证池中的所有交易并删除无效的交易。
  2. 根据 provides/requires 标签构建交易包含图。一些交易可能无法到达(有未满足的依赖项),它们应该只是被排除在池之外。
  3. 在区块作者请求时,应复制图,并从具有最高优先级且所有条件都满足的交易开始,逐个从图中删除交易。

根据当前的Gossip协议,网络应以与区块作者包含它们的顺序传播交易。如果传播的交易的总权重不超过即将到来的 N 个区块,则可能是可以接受的(选择 N 是受网络条件和区块时间的影响)。

请注意,这不是强制性的要求,传播与准备包含在区块中的交易完全相同的交易。传播是尽力而为的,特别是对于区块作者,并且没有直接激励。然而,网络协议可能会惩罚发送无效或无用交易的对等方,因此我们应该对他人友好。还可以参考以下建议,而不是传播所有内容,让其他对等方请求他们感兴趣的交易。

由于池预计要存储比单个区块能容纳的更多交易,因此每次区块导入时验证整个池可能不可行。这意味着实际实现可能需要采取一些捷径。

建议与注意事项

  1. 交易的有效性不应该在区块之间发生重大变化。即有效性变化应该是有预见性的,例如 longevity 减少1,priority 保持不变,如果包含标签的交易被包含在区块中,则 requires 会改变,provides 不会改变等。

  2. 这意味着我们不需要在每次区块导入后重新验证每个交易,但我们需要注意删除可能过时的交易。

  3. 具有完全相同字节的交易很可能会产生相同的有效性结果。我们可以基本上将它们视为相同。

  4. 注意重组和重新导入被撤回区块的交易。

  5. 过去在运行具有许多重组的小型网络时发现了许多问题。确保交易永远不会丢失。

  6. UTXO模型非常具有挑战性。一笔交易一旦被包含在区块中就变得有效,但它正在等待完全相同的输入被消耗,因此它永远不会再次被包含。

  7. 请注意,在不理想实现中,池的状态很可能会始终有点不准确,即一些交易可能仍然在池中,但它们是无效的。关于权衡的艰难决策。

  8. 请注意,导入通知并不可靠 - 你可能不会收到关于每个导入的区块的通知。

可能的实现想法

  1. 区块作者在创建区块时从池中删除交易。我们仍然保留它们以备在区块未成为主链时重新导入。这仅在区块正在积极创建区块的情况下才有效(也请参见以下内容)。

  2. 我们不进行修剪,而是从池的前端删除固定数量的交易(基于过去每个区块的平均/最大交易数量)并重新验证它们,重新导入仍然有效的交易。

  3. 我们定期分批验证池中的所有交易。

  4. 为了最小化运行时调用,我们引入了批验证调用。请注意,每次验证后应重置状态(覆盖)。

  5. 考虑利用最终确定性。也许我们可以针对最新的已最终确定的区块进行验证。这样,不同节点中的池可以更相似,这可能有助于八卦(参见集合调整)。请注意,对于Substrate链来说,最终确定性并非严格要求。

  6. 也许我们可以避免像现在这样维护就绪/未来队列,而是如果交易没有通过现有交易满足所有要求,我们尝试在未来重新导入它。

  7. 而不是维护一个具有总排序的全池,我们尝试维护一组(几个)下一区块。我们可以引入批验证运行时API方法,该方法基本上尝试模拟一组此类交易的实际区块包含(而不一定完全运行/调度它们)。导入交易将包括确定此交易有可能被包含的下一区块,然后尝试将其推回或替换一些现有交易。

  8. 也许我们可以使用一些不可变图结构来轻松添加/删除交易。我们需要一种考虑优先级和可达性的遍历方法。

  9. 过去曾讨论过使用集合调整策略代替简单地向所有/选定对等方广播所有/一些交易。以太坊的EIP-2464可能是减少交易八卦的一个好方法。

当前实现

当前池的实现是Ethereum池实现的经验结果,但也有些瑕疵来自Substrate通用性质和学习轻客户端支持的过程。

池由基本上两个独立部分组成

  1. 交易池本身。
  2. 维护后台任务。

池分为ready池和future池。后者包含尚未满足其要求的交易,而前者持有可以用于构建依赖关系图的交易。请注意,图是在遍历过程中(使用ready迭代器)临时构建的。这使得导入过程更便宜(我们不需要找到队列或图中的确切位置),但遍历过程更慢(对数)。然而,我们大多数时候只需要交易的总排序的开头,以便于区块包含或网络传播,因此做出了这个决定。

维护任务负责

  1. 定期重新验证池中的交易(重新验证队列)。
  2. 处理区块导入通知,并进行修剪+重新导入已撤回区块中的交易。
  3. 处理最终确定性通知,并将其传递给特定于交易的监听器。

此外,我们维护一个最近包含/拒绝的交易列表(PoolRotator),以便快速拒绝可能无效的交易,以限制运行时验证调用的数量。

每次导入交易时,我们首先验证其有效性,然后检查它所要求的标签是否可以通过已存在于ready池中的交易来满足。如果在ready池中导入交易,如果交易恰好满足其要求,我们将从future池中提升交易。请注意,我们需要考虑交易可能替换池中已存在交易的情况。在这种情况下,我们将检查即将替换的交易的整个子树,比较它们的累积优先级,以确定保留哪个子树。

导入区块后,我们启动修剪程序。我们首先尝试确定该区块中由交易满足的标签。对于每个区块交易,我们要么调用运行时以获取其ValidTransaction对象,要么检查池中该交易是否已知以避免运行时调用。从这些信息中,我们收集完整的provides标签集,并根据这些标签修剪ready池。此外,我们提升所有其标签得到满足的future交易。

如果我们不确定是否已将交易包含在当前区块或过去某个区块中,我们将将其添加到重新验证队列中,并尝试在未来由后台任务重新导入。

验证交易的运行时调用从单独的(有限制的)线程池中执行,以避免过多地干扰节点中的其他子系统。我们绝对不希望所有核心都在验证网络交易,因为这些交易都需要被视为不受信任的(可能DoS)。

依赖项

~62MB
~1M SLoC