Skip to content

交易确认及到期

对于许多新开发者来说,在构建应用程序时,与交易确认相关的问题很常见。本文旨在加深对 Solana 区块链上确认机制的整体理解,同时提供一些推荐的最佳实践。

交易简要背景

在深入了解 Solana 交易确认和到期的工作原理之前,我们先简要了解以下几件事:

  • 什么是交易
  • 交易的生命周期
  • 什么是区块哈希
  • 简要了解历史证明(PoH)及其与区块哈希的关系

什么是交易?

交易由两个部分组成:消息和签名列表。交易消息是最神奇的地方,从高层次来看,它有三个组成部分:

  • 要调用的指令列表
  • 要加载的帐户列表
  • 最近的区块哈希

交易生命周期概览

以下是交易生命周期的高度概括。本文将讨论除步骤 1 和 4 之外的所有内容。

  1. 创建指令列表以及指令需要读取和写入的帐户列表
  2. 获取最近的区块哈希并使用它来准备交易消息
  3. 模拟交易以确保其行为符合预期
  4. 提示用户使用私钥对准备好的交易消息进行签名
  5. 将交易发送到 RPC 节点,该节点尝试将其转发给当前的区块生产者
  6. 希望区块生产者验证交易并将其提交到他们生产的区块中
  7. 确认交易已包含在区块中或检测交易何时过期

什么是区块哈希?

“区块哈希”是指“插槽”的最终历史证明(PoH)哈希(如下所述)。由于 Solana 使用 PoH 作为可信时钟,因此交易的最近区块哈希可以被视为时间戳。

历史证明概述

Solana 的历史证明机制使用很长的递归 SHA-256 哈希链来构建可信时钟。

历史证明中“历史”的含义:区块生产者将交易 ID 哈希到 “流” (递归的哈希链) 中,以记录在其区块中处理了哪些交易。

PoH 哈希计算:next_hash = hash(prev_hash, hash(transaction_ids))

PoH 可以用作可信时钟,因为每个哈希必须按顺序生成。每个生成的块都包含一个块哈希和一个称为 “ticks” 的哈希检查点列表,以便验证器可以并行验证完整的哈希链并证明确实已经过去了一段时间。

交易到期

默认情况下,如果在一定时间内未提交到区块,所有 Solana 交易都会过期。绝大多数交易确认问题都与 RPC 节点和验证器如何检测和处理过期交易有关。深入了解交易过期的工作原理应该有助于诊断大部分交易确认问题。

交易过期的工作原理

每笔交易都包含一个“最近的区块哈希”,用作 PoH 时钟时间戳,并在该区块哈希不再“足够新”时过期。

当每个块最终确定时(即达到最大刻度高度,到达“块边界”),该块的最终哈希值将被添加到 BlockhashQueue 中,该队列最多存储 300 个最新块哈希值。在交易处理期间,Solana 验证器将检查每笔交易的最新区块哈希值是否记录在最近 151 个存储的哈希值(也称为“最大处理期限”)内。如果交易的最近区块哈希值早于此最大处理期限,则不会处理该交易。

INFO

由于当前的最大处理组合数或“年龄”为 150,并且队列中的块哈希的“年龄”为 0 索引,因此实际上有 151 个块哈希被认为“足够新”且可用于处理。

交易过期示例

让我们看一个简单的例子:

  1. 验证者正在为当前槽生成新块。
  2. 验证器接收到来自用户的交易,其中包含最近的区块哈希 abcd...
  3. 验证器根据 BlockhashQueue 中最近的区块哈希列表检查此区块哈希 abcd...,发现它是在 151 个区块前创建的。
  4. 由于它刚好是 151 个区块哈希,足够新, 所以该交易尚未过期,仍然可以被处理!
  5. 但是等等:在实际处理交易之前,验证者完成了下一个块的创建并将其添加到 BlockhashQueue 中。然后验证者开始为下一个时隙生成块(验证器为 4 个连续时隙生成块)。
  6. 验证器再次检查同一笔交易,发现它现在已经有 152 个区块哈希值了,并因为太旧而拒绝它了:(

为什么交易会过期?

实际上,这样做的好处是,为了帮助验证者避免处理同一笔交易两次。

防止双重处理的一种简单的暴力方法可能是根据区块链的整个交易历史记录检查每笔新交易。但是,通过让交易在短时间内过期,验证器只需要检查新交易是否属于最近处理的相对较小的交易集中。

其他区块链

Solana 防止双重处理的方法与其他区块链有很大不同。例如,以太坊跟踪每个交易发送者的计数器(随机数),并且只会处理使用下一个有效随机数的交易。

以太坊的方法对于验证者来说实施起来很简单,但对于用户来说可能会出现问题。许多人都遇到过这样的情况:他们的以太坊交易长时间陷入待处理状态,并且所有后来使用较高随机数值的交易都被阻止处理。

Solana 的优势

Solana 的方法有以下优点:

  1. 单个费用支付者可以同时提交多笔交易,并且这些交易可以以任何顺序进行处理。如果用户同时使用多个应用程序,这种情况可能会发生。
  2. 如果交易没有提交到区块并且过期,用户可以再次尝试,因为他们知道之前的交易将永远不会被处理。

通过不使用计数器,Solana 钱包体验可能更容易让用户理解,因为他们可以迅速了解交易的成功、失败或过期状态,并避免繁琐的待处理状态。

Solana 的缺点

Solana 的缺点包括:

  1. 验证者必须主动跟踪一组所有已处理的交易 ID,以防止重复处理。
  2. 如果过期时间太短,用户可能无法在过期前提交交易。

这些缺点凸显了在交易到期设置中的权衡。如果增加交易的过期时间,验证者需要使用更多的内存来跟踪更多的交易。如果减少过期时间,用户就没有足够的时间提交交易。

目前,Solana 集群要求交易使用的区块哈希不超过 151 个。

INFO

这个 Github issue包含一些计算,估计主网测试版验证器需要大约 150MB 的内存来跟踪交易。如果需要的话,将来可以在不减少到期时间的情况下进行精简,就像该问题中详细介绍的那样。

交易确认提示

正如之前提到的,区块哈希在仅 151 个区块后就会过期。当在 400 毫秒的目标时间内处理时隙时,这个时间段最快可以过去一分钟。

考虑到客户端需要获取最近的区块哈希、等待用户签名,并最终希望广播的交易能够被接受,一分钟并不是很长的时间。下面让我们了解一些技巧,以帮助避免由于交易过期而导致确认失败!

获取具有适当承诺级别的块哈希

考虑到交易的过期时间很短,客户端和应用程序必须确保帮助用户获取尽可能新的区块哈希来创建交易。

目前推荐使用的 RPC API 是 getLatestBlockhash。默认情况下,该 API 使用最终确定的承诺级别返回最近确定的区块的区块哈希值。但是,也可以通过设置承诺参数为不同的承诺级别来更改此行为。

推荐

几乎始终应该使用 confirmed 已确认的承诺级别进行 RPC 请求,因为它通常只会落后于 processed 已处理的承诺几个槽,并且极少会遇到丢弃分叉的情况。

但也可以考虑其他选项:

  • 使用 processed 可以获取与其他承诺级别相比的最新区块哈希,这样可以提供更多时间来准备和处理交易。但由于 Solana 区块链中分叉的普遍存在,大约 5% 的区块最终未被集群确认,因此该交易可能会使用属于已丢弃分叉的区块哈希。在最终确认的区块链中,使用区块哈希的交易永远不会被认为是最近的交易。
  • 使用 finalized 最终确认的默认承诺级别可以消除选择的区块哈希可能属于已删除分叉的风险。不过,最近确认的区块和最近最终确认的区块之间通常至少有 32 个时隙的差距。这种权衡相当严重,有效地将交易的过期时间减少了约 13 秒,但在集群不稳定的情况下,这段时间可能会更长。

使用适当的预检承诺级别

如果在交易中使用一个 RPC 节点获取的区块哈希,然后又在另一个 RPC 节点发送或模拟该交易,可能会因为一个节点落后于另一个节点而遇到问题。

当 RPC 节点收到 sendTransaction 请求时,它们将尝试使用最新已完成的块或使用 preflightCommitment 参数选择的块来确定交易的过期块。一个非常常见的问题是,接收到的交易的区块哈希是在用于计算该交易到期的区块之后生成的。如果 RPC 节点无法确定交易何时过期,它只会转发交易一次,然后删除该交易。

同样,当 RPC 节点收到 simulateTransaction 请求时,它们将使用最新已完成的块或使用 preflightCommitment 参数选择的块来模拟交易。如果用于模拟的块比用于交易的块哈希的块旧,则模拟将失败,并出现可怕的“未找到块哈希”错误。

推荐

即使使用 skipPreflight,也始终将 preflightCommitment 参数设置为用于获取 sendTransactionsimulateTransaction 请求。

发送交易时要警惕落后的 RPC 节点

当应用程序同时使用 RPC 池服务或者在创建和发送交易时使用不同的 RPC 端点时,需要警惕一个 RPC 节点落后于另一个节点的情况。举例来说,如果从一个 RPC 节点获取交易区块哈希,然后将该交易发送到第二个 RPC 节点进行转发或模拟,则第二个 RPC 节点可能会落后于第一个节点。

推荐

对于 sendTransaction 请求,客户端应定期向 RPC 节点重新发送交易,这样,如果 RPC 节点稍微落后于集群,它最终会赶上并正确检测交易的过期情况。

对于 simulateTransaction 请求,客户端应使用 replaceRecentBlockhash 参数告诉 RPC 节点将模拟交易的 blockhash 替换为对模拟始终有效的 blockhash。

避免重复使用过时的区块哈希

即使应用程序获取了最近的区块哈希,也请确保它不会在交易中重复使用该区块哈希太久。理想的情况是在用户签署交易之前获取最近的区块哈希。

应用推荐

频繁轮询最近的新区块哈希,以确保每当用户触发创建交易的操作时,应用程序已经有一个准备就绪的新区块哈希。

钱包推荐

定期轮询新的最近的区块哈希,并在签署交易之前替换交易的最近的区块哈希,以确保区块哈希尽可能新。

获取区块哈希时使用健康的 RPC 节点

通过从 RPC 节点获取具有 confirm 已确认承诺级别的最新区块哈希,它将使用它所知道的最新已确认区块的区块哈希进行响应。 Solana 的区块传播协议会优先将区块发送到质押节点,因此 RPC 节点自然会落后于集群其他节点一个区块。他们还必须做更多的工作来处理应用程序请求,并且在用户流量大的情况下可能会滞后更多。

因此,滞后的 RPC 节点可以使用集群不久前确认的块哈希来响应 getLatestBlockhash 请求。默认情况下,落后的 RPC 节点检测到它落后于集群超过 150 个槽,将停止响应请求,但在达到该阈值之前,它们仍然可以返回即将过期的块哈希。

推荐

使用以下方法之一监控 RPC 节点的运行状况,以确保它们具有最新的集群状态视图:

  1. 使用具有 processed 承诺级别的 getSlot RPC API 获取 RPC 节点的最高已处理插槽,然后调用 getMaxShredInsertSlot RPC API 来获取 RPC 节点已收到块“碎片”的最高插槽。如果这些响应之间的差异非常大,则集群生成的块远远早于 RPC 节点处理的块。
  2. 在几个不同的 RPC API 节点上调用具有 confirmed 已确认承诺级别的 getLatestBlockhash RPC API,并使用为其上下文槽返回最高槽的节点中的块哈希。

等待足够长的时间过期

推荐

当调用 getLatestBlockhash RPC API 获取交易的最新区块哈希时,请记下响应中的 lastValidBlockHeight

然后,使用 confirmed 已确认的承诺级别轮询 getBlockHeight RPC API,直到返回的块高度大于之前返回的最后一个有效块高度。

考虑使用“持久”交易

有时交易过期问题确实很难避免(例如离线签名、集群不稳定)。如果前面的技巧仍然不足以满足用例,可以改用持久交易(它们只需要一些设置)。

要开始使用持久交易,用户首先需要提交一个交易,该交易调用创建特殊链上“随机数”帐户并在其中存储“持久区块哈希”的指令。在未来的任何时候(只要随机数账户尚未被使用),用户都可以通过遵循以下两条规则来创建持久交易:

  1. 指令列表必须以加载其链上随机数账户的“预先随机数”系统指令开始。
  2. 交易的区块哈希必须等于链上随机数账户存储的持久区块哈希。

以下是 Solana 运行时处理这些持久交易的方式:

  1. 如果交易的区块哈希不再是“最近的”,运行时会检查交易的指令列表是否以“提前随机数”系统指令开头。
  2. 如果是,则加载由“advance nonce”指令指定的随机数帐户。
  3. 然后它检查存储的持久区块哈希是否与交易的区块哈希相匹配。
  4. 最后,它确保将随机数帐户存储的区块哈希推进到最新的区块哈希,以确保同一交易永远不会被再次处理。

有关这些持久交易如何工作的更多详细信息,参见original proposal 并查看 Solana 文档中的示例