跳到主要内容

Solana高级概念: 交易确认与过期

鱼雪

许多新开发者在构建应用程序时,常常会遇到与交易确认相关的问题。

本文旨在提升对Solana区块链交易确认机制的整体理解,并提供一些推荐的最佳实践。

交易的简要背景​

在深入了解Solana的交易确认和过期机制之前,

让我们简要了解以下几个方面:

  • 交易是什么
  • 交易的生命周期
  • 什么是区块哈希(blockhash)
  • 对历史证明(Proof of History, PoH)及其与区块哈希的关系有一个简要了解

什么是交易?​

交易由两部分组成:消息和签名列表。交易消息是关键,它由以下三部分组成:

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

本文将重点讨论交易的最近区块哈希,因为它在交易确认中起着重要作用。

交易生命周期回顾​

下面是交易生命周期的高层次视图。

本文将涉及除步骤1和4之外的所有内容。

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

什么是区块哈希?​

区块哈希指的是某个槽位(slot)的最后一个历史证明(PoH)哈希。

由于Solana使用PoH作为可信的时钟, 交易的最近区块哈希可以看作是一个时间戳。

历史证明回顾​

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

该名称中的"历史"部分来源于区块生产者将交易 ID 哈希到流中以记录在其区块中处理了哪些交易。

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

由于每个哈希必须按顺序生成, PoH可以用作可信的时钟。

每个生成的区块包含一个区块哈希和一组称为ticks的哈希检查点,

以便验证者可以并行验证完整的哈希链,并证明确实经过了一定的时间。

交易过期​

默认情况下,如果交易未在一定时间内提交到区块中,它们将会过期。 大多数交易确认问题与RPC节点和验证者如何检测和处理过期交易有关。 对交易过期工作机制的深入理解可以帮助你诊断大部分交易确认问题。

交易过期的工作原理​

每笔交易包含一个最近的区块哈希,该哈希用作PoH时钟时间戳, 当该区块哈希不再足够最近时,交易就会过期。

随着每个区块的最终化(即达到最大tick高度,达到区块边界), 区块的最终哈希会添加到BlockhashQueue中,该队列存储最多300个最近的区块哈希。

在交易处理中,Solana验证者将检查每笔交易的最近区块哈希 是否记录在最近的151个存储的哈希中(即最大处理年龄)。

如果交易的最近区块哈希比这个最大处理年龄更旧,则该交易不会被处理。

信息

由于当前的最大处理年龄为150,加上区块哈希在队列中的年龄从0开始索引, 实际上有151个区块哈希被认为是足够最近并且可以处理。

由于槽位(即验证者可以生成区块的时间段)配置为大约400毫秒, 但可能在400毫秒到600毫秒之间波动, 因此给定的区块哈希只能在大约60到90秒内用于交易, 之后它将被运行时视为过期。

交易过期示例​

让我们快速演示一个例子:

  1. 验证者正在为当前槽位积极生成新块。
  2. 验证者从用户那里收到一笔包含最近区块哈希abcd...的交易。
  3. 验证者将此区块哈希abcd...与BlockhashQueue中的最近区块哈希列表进行比较, 发现它是在151个区块之前创建的。
  4. 由于它正好是151个区块哈希老,因此交易尚未过期,可以处理。
  5. 但在实际处理交易之前,验证者完成了下一个区块的创建并将其添加到BlockhashQueue。 然后,验证者开始为下一个槽位生成区块(验证者可以连续生成4个槽位的区块)。
  6. 验证者再次检查同一笔交易,发现它现在有152个区块哈希老,并拒绝了它,因为它太老了:(

交易为什么会过期?​

实际上,这是为了帮助验证者避免重复处理同一笔交易。

一种简单的蛮力方法来防止重复处理是将每个新交易与区块链的整个交易历史记录进行比较。

但通过让交易在短时间后过期,验证者只需检查新交易是否在相对较小的最近处理交易集内即可。

其他区块链​

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

以太坊的方法对验证者来说实现简单,但对用户来说可能会有问题。

许多人遇到过以太坊交易长时间处于待处理状态,所有使用更高nonce值的后续交易都被阻止处理的情况。

Solana的优势​

Solana的方法有以下几个优势:

  • 单个费用支付者可以同时提交多个允许以任何顺序处理的交易。 如果你同时使用多个应用程序,这种情况可能会发生。
  • 如果交易未提交到区块并过期,用户可以重新尝试,知道他们之前的交易永远不会被处理。

由于不使用计数器,Solana的钱包体验对用户来说可能更容易理解, 因为他们可以快速达到成功、失败或过期状态,避免烦人的待处理状态。

Solana的劣势​

当然也有一些劣势:

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

这些劣势突显了交易过期配置中的权衡。

如果增加交易的过期时间,验证者需要使用更多内存来跟踪更多交易。

如果减少过期时间,用户可能没有足够的时间提交交易。

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

信息

这个 Github 问题包含了一些估算, 它们估计mainnet-beta验证者需要约 150MB 的内存来跟踪交易。 如果必要的话,未来可以通过简化这个过程,而不会降低交易的过期时间, 详细信息请参考该问题。

交易确认提示​

正如之前提到的,区块哈希在151个区块后过期, 当槽位在目标时间400ms内处理时,这可以快到一分钟。

考虑到客户端需要获取最近的区块哈希、等待用户签名,

并最终希望广播的交易被愿意接受的领导者接收,一分钟并不是很多时间。

让我们来看看一些帮助避免交易因过期导致确认失败的提示!

使用适当的承诺级别获取区块哈希​

鉴于短暂的过期时间框架, 客户端和应用程序必须帮助用户创建一个尽可能最近的区块哈希的交易。

获取区块哈希(blockhash)时,目前推荐的RPC API是getLatestBlockhash。

默认情况下,该API使用已最终(finalized)确定的承诺级别返回最近的最终区块的区块哈希(blockhash)。 然而,你可以通过设置承诺(commitment)参数为不同的承诺级别来覆盖此行为。

推荐​

几乎总是使用已确认(confirmed)的承诺级别进行RPC请求, 因为它通常仅比处理的(processed)承诺滞后几个槽位,并且几乎没有属于丢弃分叉的风险。

但可以考虑其他选项:

  • 选择处理过的(processed)承诺将让你获取与其他承诺级别相比最新的区块哈希, 因此为你提供最多的时间来准备和处理交易。 但由于Solana区块链中分叉的普遍性,大约5%的区块最终不会被集群确认, 所以你的交易有可能使用一个属于丢弃分叉的区块哈希。 使用被废弃区块哈希的交易永远不会被最终区块视为最近。
  • 使用默认的已最终(finalized)确定的承诺级别将消除你选择的区块哈希属于丢弃分叉的任何风险。 代价是通常最新确认区块与最新最终区块之间至少有32个槽位的差异。 这种权衡非常严重,有效地减少了你的交易的过期时间大约13秒,但在集群不稳定时,这可能会更多。

使用适当的预提交级别​

如果你的交易使用的区块哈希是从一个RPC节点获取的, 然后你发送或模拟该交易时使用了另一个RPC节点, 可能会由于一个节点落后于另一个节点而出现问题。

当RPC节点接收到sendTransaction请求时, 它们将尝试使用最近的最终区块或预检承诺(preflightCommitment)参数 选择的区块来确定你的交易的过期块。 一个非常常见的问题是,接收到的交易的区块哈希是在用于计算该交易过期时间的区块之后生成的。 如果RPC节点无法确定你的交易何时过期,它将只转发你的交易一次,然后将其丢弃。

类似地,当RPC节点接收到simulateTransaction请求时, 它们将使用最近的最终区块或预检承诺(preflightCommitment)参数选择的区块模拟你的交易。 如果选择用于模拟的区块比用于你的交易区块哈希的区块更旧, 模拟将失败,并出现找不到区块哈希的错误。

推荐​

即使你使用skipPreflight,始终将预检承诺(preflightCommitment)参数 设置为用于获取交易区块哈希的相同承诺级别, 用于sendTransaction和simulateTransaction请求。

发送交易时警惕滞后的RPC节点​

当你的应用程序使用RPC池服务或创建交易和发送交易时使用不同的RPC端点时, 你需要警惕一个RPC节点落后于另一个RPC节点的情况。

例如, 如果你从一个RPC节点获取交易区块哈希, 然后将该交易发送到第二个RPC节点进行转发或模拟, 第二个RPC节点可能落后于第一个。

推荐​

对于sendTransaction请求,客户端应频繁地将交易重新发送到RPC节点, 以便如果RPC节点略微落后于集群,它最终会赶上并正确检测你的交易过期时间。

对于simulateTransaction请求,客户端应使用replaceRecentBlockhash参数, 告诉RPC节点用一个始终有效的区块哈希替换模拟交易的区块哈希。

避免重用过期的区块哈希​

即使你的应用程序获取了一个非常新的区块哈希, 也要确保不会在交易中重用该区块哈希太久。

理想情况下,应在用户签署交易前立即获取最近的区块哈希。

对应用程序的建议​

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

对钱包的建议​

频繁轮询新的最近区块哈希,并在用户签署交易前立即替换交易的最近区块哈希,以确保区块哈希尽可能新。

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

通过从RPC节点使用已确认的承诺级别获取最新区块哈希,它会响应最近确认区块的区块哈希。 Solana的区块传播协议优先将区块发送到质押节点,因此RPC节点自然会落后于集群其他部分大约一个区块。 在处理应用程序请求时,它们还需要做更多的工作,并在用户流量繁忙时可能会落后更多。

滞后的RPC节点因此可能会响应getLatestBlockhash请求,返回集群确认的区块哈希, 但实际上该区块哈希即将过期。

默认情况下,如果滞后的RPC节点检测到它比集群落后超过150个槽位, 将停止响应请求,但在达到该阈值之前,它们仍可能返回一个即将过期的区块哈希。

推荐​

监控RPC节点的健康状况,以确保它们具有最新的集群状态视图,可以通过以下方法之一实现:

  1. 使用getSlot RPC API(使用处理过的承诺级别)获取RPC节点的最高处理槽位, 然后调用getMaxShredInsertSlot RPC API获取RPC节点接收到区块的碎片最高槽位。 如果这些响应之间的差异很大,说明集群生成的区块远远超过了RPC节点处理的区块。
  2. 使用已确认的承诺级别调用getLatestBlockhash RPC API, 在几个不同的RPC API节点上,使用返回的最高槽位的节点的区块哈希。

等待足够长的时间以确保过期​

推荐​

调用getLatestBlockhash RPC API以获取交易的最近区块哈希时, 记录响应中的lastValidBlockHeight。

然后,以已确认的承诺级别轮询getBlockHeight RPC API, 直到其返回的区块高度大于先前返回的最后有效区块高度。

考虑使用持久交易​

有时交易过期问题真的很难避免(例如,离线签名、集群不稳定)。 如果之前的提示仍然不足以满足你的用例,可以切换到使用持久交易(它们只需要一些设置)。

要开始使用持久交易, 用户首先需要提交一个调用指令的交易, 创建一个特殊的链上nonce账户,并在其中存储一个“持久区块哈希”。 在未来的任何时候(只要该nonce账户尚未使用),

用户可以通过以下两条规则创建持久交易:

  1. 指令列表必须以advance nonce系统指令开头,该指令会加载他们的链上nonce账户。
  2. 交易的区块哈希必须等于链上nonce账户存储的持久区块哈希。

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

  • 如果交易的区块哈希不再最近,运行时会检查交易的指令列表是否以advance nonce系统指令开头。
  • 如果是,则加载advance nonce指令指定的nonce账户。
  • 然后检查存储的持久区块哈希是否与交易的区块哈希匹配。
  • 最后,确保将nonce账户的存储区块哈希推进到最近的区块哈希,以确保同一交易永远不会被再次处理。

要点总结​

交易及其组成部分​

  • 交易:包含消息和签名列表。
  • 消息:由指令列表、账户列表和最近的区块哈希组成。
  • 区块哈希:是某个槽位的最后一个历史证明(PoH)哈希,作为交易的时间戳。

交易生命周期​

  1. 创建指令列表和账户列表。
  2. 获取最近的区块哈希。
  3. 模拟交易。
  4. 提示用户签名。
  5. 发送交易到RPC节点。
  6. 区块生产者验证并提交交易。
  7. 确认交易是否包含在区块中或是否已过期。

交易过期机制​

  • 默认过期时间:区块哈希在151个槽位(约60-90秒)内有效。
  • 区块哈希队列:最多存储300个最近的区块哈希。
  • 验证者检查:验证交易的区块哈希是否在最近的151个区块哈希中。
  • 目的:避免重复处理交易。

提示和建议​

  • 使用适当的承诺级别获取区块哈希:推荐使用已确认的承诺级别。
  • 确保预检承诺级别一致:使用获取区块哈希时相同的承诺级别进行交易发送和模拟。
  • 频繁发送交易:确保RPC节点最终检测到交易过期时间。
  • 避免重用过期的区块哈希:获取新的区块哈希并立即使用。
  • 监控RPC节点健康状况:确保其具有最新的集群状态视图。
  • 等待足够长的时间确认交易过期:记录lastValidBlockHeight并监控区块高度。

持久交易​

  • 设置:创建一个特殊的链上nonce账户。
  • 持久区块哈希:存储在nonce账户中,用于创建不易过期的交易。
  • 规则:交易需以advance nonce指令开头,区块哈希需匹配nonce账户存储的哈希。

通过理解和应用这些要点,可以有效处理Solana区块链上的交易确认与过期问题。