新消息:Helius 收购 Light Protocol
如何在 Solana 上成功上链交易
博客/开发

如何在 Solana 上成功上链交易

开发者体验工程师X 上的 Anam AnsariLinkedIn 上的 Anam Ansari
阅读需 18 分钟

Solana 最近的交易量达到前所未有的水平,导致大量交易失败或被丢弃。

Solana 的每秒交易数 (TPS) 约为 1,000+ 笔非投票交易。Quinn(网络层协议 QUIC 的 Rust 实现)在高需求场景下难以有效处理垃圾流量,可能迫使区块领导者选择性地断开连接。在所有失败的交易中,约 8% 由真实用户发起,其余都是机器人发送的任意交易。

要处理失败交易,必须了解交易在 Solana 上的提交和处理方式。本文将深入探讨交易失败的可能原因,并提供提高交易吞吐量的最佳实践。本文假设你已基本了解 Solana 的编程模型,以及如何创建和发送交易。

交易

程序执行始于提交到集群的交易。一笔交易包含:

  • 交易计划读取或写入的所有账户组成的数组
  • 一条或多条指令(即最小执行单元)
  • 一个近期区块哈希
  • 一个或多个签名

运行时会按顺序以原子方式处理交易中的每条指令。如果某条指令的任何部分失败,整笔交易都会失败。

什么是区块哈希?

“区块哈希”是某个时隙最新的历史证明 (PoH) 哈希。由于 Solana 依赖 PoH 作为可信时钟,因此交易的近期区块哈希可视为时间戳。区块哈希可防止重复交易,并为交易设定有效期。如果交易的区块哈希过旧,交易就会被拒绝。区块哈希的最大有效期为 150 个区块,约为 ~1 分 19 秒。

交易如何提交?

Solana 由一组验证者维护,他们负责验证添加到账本中的交易。系统会从中选出一名领导验证者,负责将条目追加到账本。账本条目可以是一个 tick,也可以是一个交易条目。账本保存着包含客户端签名交易的条目列表。从概念上讲,创世区块可以追溯到账本起点。不过,实际验证者的账本可能只保留较新的区块以减少存储,因为按照设计,验证未来区块不需要旧区块。

领导验证者在每个时隙中只能生成一个区块,而区块哈希是用于标识每个区块的唯一标识符。它是区块中所有条目及前一个区块哈希共同计算出的哈希值。领导者日程会在每个纪元开始前确定,通常提前约两天,用来决定任意时刻由哪个验证者担任当前领导者。交易发起后,会被转发给当前和下一任领导验证者。

交易可以通过以下方式提交给领导者: 

  1. RPC 服务器:RPC 提供商可以通过 sendTransaction JSON-RPC 方法提交交易。接收交易的 RPC 节点会尝试每两秒将其作为 UDP 数据包发送给当前和下一任领导者,直到交易最终确定或交易的区块哈希过期(150 个区块后,约为 ~1 分 19 秒)。在此之前,除客户端和中继 RPC 节点所掌握的信息外,不会有其他交易记录。 
  2. TPU 客户端:TPU 客户端只负责提交交易。客户端软件需要自行处理重播和领导者转发。

要使用 sendTransaction 方法,需要传入编码为字符串的交易对象。其他可选参数包括:

  1. encoding:交易数据使用的编码为 base58 或 base64。 
  2. skipPreflight:预检包括验证交易签名,以及根据预检承诺级别指定的银行时隙模拟交易。如果预检失败,将返回错误。此功能默认为 false,即不会跳过预检。
  3. preflightCommitment:指定执行预检时使用的承诺级别。承诺级别默认为 finalized,但可以通过字符串进行更改。建议为承诺和预检承诺指定相同的级别,以避免出现难以理解的行为。
  4. maxRetries:maxRetries 参数决定 RPC 节点向领导者重试发送交易的最大次数。如果未提供此参数,RPC 节点会持续重试,直到交易最终确定或区块哈希过期。 
  5. minContextSlot:minContextSlot 参数指定执行交易预检的最小时隙。

交易如何处理?

验证者的交易处理单元 (TPU) 接收交易、验证签名、执行交易,并将其分享给网络中的其他验证者。

TPU 分五个不同阶段处理交易:

获取阶段

获取阶段负责接收交易。它会根据三个端口对传入交易进行分类:

  • tpu:处理代币转账、NFT 铸造和程序指令等常规交易
  • tpu_vote:仅处理投票交易
  • tpu_forwards:如果当前领导者无法处理所有交易,它会将未处理的数据包转发给下一任领导者

数据包以 128 个为一批,转发到签名验证阶段。

签名验证阶段

签名验证阶段验证数据包的签名,并剔除验证失败的数据包。投票数据包与常规数据包通过两条独立的流水线运行。从软件角度来看,接收的数据包包含一些元数据,但此时仍无法确定这些数据包是否为交易。

如果安装了 GPU,系统会使用它进行签名验证。此外,在流量较高、数据包过多时,系统会根据 IP 地址丢弃部分数据包。

银行阶段

此阶段负责筛选和处理交易。目前,它由 6 个独立工作线程组成,其中 2 个为投票线程,4 个为非投票线程。常规交易会被添加到非投票线程。每个线程都有一个本地缓冲区,可在优先级队列中容纳最多 64 笔互不冲突的交易。随后,借助 Sealevel,这些交易会被并行处理。你可以观看此视频,详细了解银行阶段。

历史证明服务

PoH 服务模块记录 tick 的推进。每个 tick 代表一个时间单位,每个时隙包含 64 个 tick。在从银行阶段收到记录前,系统会不断生成哈希:

next_hash = hash(prev_hash, hash(transaction_ids))

随后,这些记录会转换为条目,并通过广播阶段向网络广播。

广播阶段

PoH 服务生成的条目会被转换为 shred,即区块的最小单位,然后通过名为 Turbine 的区块传播技术发送到网络的其余部分。从宏观上看,Turbine 将区块分割成更小的片段,并通过分层节点结构分发。节点无需与所有其他节点保持联系,只需与选定的少数节点通信。你可以阅读这篇文章,进一步了解 Turbine 及其工作原理。 

交易为什么会失败?

除错误指令或自定义程序错误造成的失败外,交易失败还可能由以下原因导致:

网络丢包

网络层可能在领导者处理交易前就将其丢弃。UDP 数据包丢失是最简单的原因。另一个原因与 TPU 的 fetch stage 有关。当网络负载过高时,验证者可能因需要处理的交易过多而不堪重负。验证者可以将多余交易转发到下一名验证者的 tpu_forward 端口。但可转发的数据量有限,而且每次转发仅限验证者之间的一跳。这意味着在 tpu_forwards 端口收到的交易不会继续转发给其他验证者。如果待处理的重播队列超过 10,000 笔交易,新提交的交易将被丢弃。

过期/错误的区块哈希 

每笔交易都有一个“近期区块哈希”,用作历史证明 (PoH) 时钟的时间戳。区块哈希可帮助验证者避免重复处理同一笔交易,并记录交易处理的时间和顺序。处理过程中,验证者会拒绝区块哈希无效的交易。

区块哈希过期

当交易的区块哈希不再足够“新”时,它就会过期。处理交易时,Solana 验证者会在区块中查找对应区块哈希的时隙编号。如果验证者找不到该区块哈希的时隙编号,或查到的时隙编号比正在处理的区块低 151 个以上时隙,交易就会被拒绝。默认情况下,如果 Solana 交易未在指定时间内(约 ~1 分 19 秒)提交到区块中,就会过期。

滞后的 RPC 节点

通过 RPC 提交交易时,RPC 池的某部分可能领先于其他部分。当池内节点需要协作时,这可能引发问题。例如,如果交易的 recentBlockhash 从池中较领先的部分查询,却提交给较滞后的部分,节点将无法识别较新的区块哈希并拒绝交易。提交交易时,可以通过在 sendTransaction 上启用预检来发现此问题。

临时网络分叉

临时网络分叉也可能导致交易被丢弃。如果验证者在银行阶段重放区块的速度较慢,可能会形成少数派分叉。客户端构建交易时,交易可能引用仅存在于少数派分叉上的 recentBlockhash。提交交易后,集群可能在交易得到处理前离开该少数派分叉。在这种情况下,由于找不到区块哈希,交易会被丢弃。

如何让交易成功上链?

要诊断确认问题,了解交易过期机制非常重要。按照以下步骤操作,可提高交易成功率:

概要

  • 使用承诺级别“confirmed”或“finalized”获取最新区块哈希
  • 将 skipPreflight 设为 true
  • 优化请求的计算单元数量
  • 添加并动态计算优先费
  • 将 maxRetries 设为 0,并添加自定义交易发送重试逻辑。
  • 探索质押连接
  • 如果交易对时间不敏感,请使用持久 nonce

区块哈希

验证者处理交易的时间有限。如果验证者处理交易前,与交易关联的区块哈希已经过期,交易将被取消。为确保交易成功,应使用近期区块哈希发送交易。如果区块哈希在验证者处理交易前过期,可以使用新的区块哈希重新尝试,以确保交易得到成功处理。可以通过两种方式实现: 

1. 设置新的承诺级别:

获取最新区块哈希时,推荐使用的 RPC API 方法是 getLatestBlockhash。默认情况下,此方法使用 finalized 承诺级别,返回最近已最终确定区块的区块哈希。此承诺级别表示该区块之上至少又添加了 31 个已确认区块,因而可以消除使用已丢弃分叉中区块哈希的风险。不过,最近确认的区块与最终确定的区块之间通常至少相差 32 个时隙。这样的取舍会让交易有效期缩短约 13 秒,在集群不稳定时可能缩短更多。 

可以将承诺参数设为其他级别,覆盖区块哈希的承诺级别。建议对 RPC 请求使用 confirmed 承诺级别,因为它通常只比 processed 承诺级别落后几个时隙,且属于已丢弃分叉的概率很低。尽管与其他承诺级别相比,processed 承诺级别可以获取最新的区块哈希,但并不推荐使用。由于 Solana 协议中的分叉,约 5% 的区块不会被集群最终确定。如果交易使用了属于已丢弃分叉的区块哈希,那么最终确定的区块链中的任何区块都不会将其视为近期区块哈希。

2. 频繁轮询新的近期区块哈希:

添加脚本,频繁使用 getLatestBlockhash 方法(每 60 秒)获取并存储最新区块哈希。这样,每当用户触发交易时,应用都已有新鲜的区块哈希可用。钱包也应频繁轮询新区块哈希,并在签署交易前立即替换交易的近期区块哈希,确保它尽可能新。

跳过预检

提交交易前,系统会执行以下预检:

  • 验证交易签名。
  • 根据预检承诺指定的银行时隙模拟交易。如果失败,则返回错误。 

如果选择用于模拟的区块早于交易区块哈希所用的区块,模拟将失败,并返回令人头疼的“找不到区块哈希”错误。 

如果你确信交易签名已经验证且没有其他错误,可以跳过预检。即使使用 skipPreflight 参数,也务必针对 sendTransaction 和 simulateTransaction 请求,将 preflightCommitment 参数设为获取交易区块哈希时所用的相同承诺级别。

计算单元

交易在网络上得到确认时,会占用区块可用计算单元 (CU) 总量的一部分。目前,每个区块的计算总上限为 48M CU。开发者可以为交易指定计算单元预算。如果未设置预算,则使用默认值 200,000。许多交易不会用完全部 CU 预算,因为请求高于所需的预算不会受到惩罚。然而,预先请求过多计算单元会增加高效调度交易的难度,因为在执行交易前,调度器并不知道区块还剩多少计算资源。为避免这种情况,开发者应设置范围更合理、与交易需求相符的 CU 请求。你可以参考这份指南来优化计算单元预算。在即将发布的 Solana 客户端 v1.18 更新中,需要较少计算单元的交易将获得更高优先级。

优化计算单元 (CU) 使用量具有以下优势:

  • 较小的交易更有可能被纳入区块。
  • 成本更低的指令让程序具有更强的可组合性。
  • 降低区块整体使用量,使一个区块能够容纳更多交易。

实施优先费

可以在基础交易费之外添加优先费,让验证者优先处理交易。这些费用按每个计算单元多少微 lamport 计价(即少量 SOL)。将其添加到交易后,验证者节点会更有经济动力把交易纳入网络区块。

但需要注意,优先费的支付金额应有上限。支付高于常规水平的费用并不会提高交易成功的概率。因此,建议动态计算优先费,既支付适当金额以保持竞争力,又避免多付。集成过程很简单,你可以参考关于优先费的官方文档,也可以使用现成的 Helius API。

实现可靠的重试逻辑

遇到网络拥堵时,请在代码中实现自定义逻辑,以处理失败交易并手动重试。为此,通过 sendTransaction 提交交易时,将 maxRetries 参数设为 0。你可以使用以下不同方法重试交易:

  • 使用不同承诺级别轮询 transaction status,并持续使用同一笔已签名交易,直到交易得到确认。使用指数退避机制来避免产生垃圾流量。或者,也可以按固定时间间隔提交交易,直到超时。
  • 存储 getLatestBlockhash method 返回的 lastValidBlockHeight。然后轮询集群的区块高度,并在当前区块高度超过 lastValidBlockHeight 后手动重试交易。通过 getLatestBlockhash 轮询时,建议指定所需的承诺级别。将承诺级别设为 confirmed(已投票)或 finalized(confirmed 后约 ~30 个区块),可以避免轮询到少数派分叉中的区块哈希。

质押连接

领导者的网络带宽容量有限。为了有效利用带宽,必须使用质押加权机制,避免不考虑交易来源、盲目按照先到先得的方式接收交易。Solana 是一个权益证明网络,因此扩展质押加权的应用来提高交易服务质量十分自然。这意味着,拥有 0.5% 质押权重的节点至少可以向领导者发送 0.5% 的数据包。网络中的其余部分,无论以何种剩余质押权重组合,都无法将这些数据包完全挤出。此机制称为质押加权服务质量 (SWQoS)。

Helius 为付费计划提供质押连接。要了解更多信息,请查看我们的文档:在 Solana 上发送交易。

持久 Nonce

持久 nonce 允许创建并签署可在未来任意时点提交的交易。它们适用于托管服务等需要较长时间生成交易签名的场景。如果交易对时间不敏感,可以使用此方法绕过交易 recentBlockhash 有效期较短的限制。

要开始使用持久交易,需要提交一笔交易,调用指令在链上创建特殊的“nonce”账户,并在其中存储“持久区块哈希”。Nonce 账户存储 nonce 值。只要 nonce 账户尚未使用,就可以按照以下两条规则创建持久交易:

  • 指令列表必须以“推进 nonce”系统指令开头,该指令会加载你的链上 nonce 账户。
  • 交易的区块哈希必须等于链上 nonce 账户中存储的持久区块哈希。

参考这篇文章,了解如何通过 CLI 和 Web3.js 实现持久 nonce。

Helius 提交交易的方法

sendTransaction 请求会自动路由到离你最近的 RPC 节点。如果未指定 maxRetries,系统会每 2 秒重试一次交易,直到区块哈希过期。我们建议将 maxRetries 设为 0,并自行每 2 秒重新广播交易,直到交易得到确认。 

为应对当前的网络拥堵,我们正全天候努力提高用户的交易上链率。我们已降低 sendTransaction 请求的速率限制。此措施旨在管理拥堵,并防止向验证者发送垃圾流量。你可以在此处查看限制。

此外,我们正通过质押连接为付费计划路由高质量流量。我们利用自己的验证者提供这些质押连接。如果优先费总额至少为 10,000 Lamports(集群中位数),该流量就会被视为高质量流量。

我们要求总费用高于 10,000 Lamports,以确保向验证者提供高质量流量。验证者已开始限制发送低费用交易的流量来源,甚至直接将其屏蔽。

使用质押连接可以显著提高交易上链率。请参考这篇文章,了解创建交易时如何设置优先费。

我们的建议

初级/中级用户

建议将 skipPreflight 设为 false。预检包括验证交易签名,以及根据预检承诺指定的银行时隙模拟交易。如果预检失败,将返回错误。如果不执行预检,配置错误可能导致交易被丢弃。

高级用户

对于需要尽可能降低延迟的高级用户,应将 skipPreflight 设为 true。不过,你需要自行确保交易配置正确。 

总结

要在网络拥堵期间成功让交易在 Solana 网络上链,需要深入理解网络架构和交易处理机制。掌握区块哈希在确保交易唯一性和时效性方面的作用、通过 RPC 服务器或 TPU 客户端提交交易的流程,以及正确设置 skipPreflight、preflightCommitment 和 maxRetries 等参数的重要性,可以显著提升交易性能。实现自定义重试机制并利用质押连接,也有助于提高成功率。

此外,还必须了解网络当前的局限,以及 Anza 为解决这些问题正在开展的工作,例如即将发布的 v1.18 客户端。随着网络不断演进和扩展,及时掌握信息并保持灵活应变,是与网络高效交互的关键。 

如果需要任何帮助或支持,请随时通过 Discord 联系我们。请务必在下方输入你的电子邮件地址,以免错过 Solana 的任何最新动态。准备好深入探索了吗?立即阅读 Helius 博客上的最新文章,继续你的 Solana 之旅。

资源

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片