SWQoS 的 Durable Nonce 扇出发送

背景和使用场景

在 Solana 交易发送中,slot 进展、Leader 分布和网络路径拥塞不断变化,决定着哪条路由最先到达 Leader。这并非特定于某个 RPC 提供商或发送服务;这是 Solana 执行模型的结构性特征。
ERPC 的 SWQoS 端点提供了一条发送路径,将交易注入到 Leader 基于质押加权服务质量(SWQoS)分配的优先通道中。此优先带宽约为非优先通道的 5 倍,且在 Priority fee 评估之前应用。
因此,SWQoS 端点是交易发送的重要选项,但在生产环境中,单一端点并非总是最快的。即使在同一个 slot 内,瞬时路径差异或负载偏移也可能使另一个快速端点领先。
鉴于这些特性,将同一交易并行发送到多个快速路径、接受最先处理的那个的操作模式可能比依赖单一路由更有效。
然而,当同一交易被发送到多个端点时,如果没有额外控制,无法保证它只执行一次。没有这种控制的扇出可能导致意外的重复执行或破坏重试控制。
Solana 为此提供了 Durable Nonce 机制。使用 Durable Nonce 可以让您将同一已签名交易发送到多条路由,同时将链上执行限制为一次。
本页面说明如何实现结合 ERPC 的 SWQoS 端点与其他快速 RPC 端点的扇出操作,假设使用 Durable Nonce 进行交易发送。

范围和前提条件

本指南涵盖使用 web3.js 创建 Durable Nonce 账户,并将其用于交易发送和扇出操作。
需要理解的前提条件:
  • 对于 Durable Nonce 交易,使用 nonce 值作为 recentBlockhash,并将 nonceAdvance 作为第一条指令
  • 一旦 nonceAdvance 执行,即使后续指令失败,nonce 也可能被消耗。您不能原样重新发送相同的 rawTx。
  • Nonce 账户创建是一次性设置,通常会重复使用

步骤 1:准备 nonce 授权和连接

nonce 授权是一个可以推进 nonce 的 Keypair。
typescript
import {
  Connection,
  Keypair,
  SystemProgram,
  NONCE_ACCOUNT_LENGTH,
} from '@solana/web3.js'

const connection = new Connection(
  'https://<primary-rpc-endpoint>',
  'confirmed',
)

const nonceAuthority = Keypair.fromSecretKey(/* secret key */)
  • nonce 授权可以执行 nonceAdvance
  • 它可以是手续费支付者,也可以是单独的密钥对

步骤 2:生成 nonce 账户密钥对

nonce 账户是一个系统账户
typescript
const nonceAccount = Keypair.generate()
此密钥对用于:
  • 保存 nonce 值(替代 recentBlockhash
  • 仅在创建时签名
  • 之后安全存储;日常发送不需要使用

步骤 3:计算免租最低余额

Nonce 账户必须免租。不要硬编码值;从 RPC 获取。
typescript
const lamports =
  await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH,
  )

步骤 4:创建并初始化 nonce 账户

在单个交易中使用 createAccount + nonceInitialize 创建 nonce 账户。
typescript
import { Transaction } from '@solana/web3.js'

const tx = new Transaction()

tx.add(
  SystemProgram.createAccount({
    fromPubkey: nonceAuthority.publicKey,
    newAccountPubkey: nonceAccount.publicKey,
    lamports,
    space: NONCE_ACCOUNT_LENGTH,
    programId: SystemProgram.programId,
  }),
  SystemProgram.nonceInitialize({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
)

// fee payer is the nonce authority
tx.feePayer = nonceAuthority.publicKey

// use a normal blockhash for initialization
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash('confirmed')

tx.recentBlockhash = blockhash

// sign with both the new account and the authority
tx.sign(nonceAccount, nonceAuthority)

const signature = await connection.sendRawTransaction(tx.serialize())
await connection.confirmTransaction(
  { signature, blockhash, lastValidBlockHeight },
  'confirmed',
)
后续发送使用此 nonceAccount.publicKey

步骤 5:每次发送前获取 nonce

每次交易都需要获取当前的 nonce 值。
typescript
import { NonceAccount } from '@solana/web3.js'

const { value, context } =
  await connection.getAccountInfoAndContext(
    nonceAccount.publicKey,
    'confirmed',
  )

if (!value) {
  throw new Error('Nonce account not found')
}

const nonce = NonceAccount.fromAccountData(value.data).nonce
const minContextSlot = context.slot
  • 使用 nonce 作为 recentBlockhash
  • context.slot 用于确认

步骤 6:构建 Durable Nonce 交易

Durable Nonce 交易必须满足以下条件:
  • recentBlockhash = nonce
  • 第一条指令是 nonceAdvance
typescript
import {
  TransactionMessage,
  VersionedTransaction,
} from '@solana/web3.js'

const instructions = [
  SystemProgram.nonceAdvance({
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: nonceAuthority.publicKey,
  }),
  // add your real instructions after this
]

const message = new TransactionMessage({
  payerKey: nonceAuthority.publicKey,
  recentBlockhash: nonce,
  instructions,
}).compileToV0Message()

const tx = new VersionedTransaction(message)
tx.sign([nonceAuthority /* + other signers */])

const rawTx = tx.serialize()
注意事项:
  • 如果使用 ComputeBudget 指令,必须放在 nonceAdvance 之后
  • 如果 nonceAdvance 不是第一条指令,交易将被拒绝

步骤 7:扇出到多个 RPC

将相同的 rawTx 并发发送。
typescript
const endpoints = [
  'https://<erpc-swqos-fra>',
  'https://<backup-rpc-1>',
  'https://<backup-rpc-2>',
]

const results = await Promise.allSettled(
  endpoints.map((url) =>
    new Connection(url, 'confirmed').sendRawTransaction(rawTx, {
      skipPreflight: true,
      minContextSlot,
    }),
  ),
)

const success = results.find(
  (r): r is PromiseFulfilledResult<string> =>
    r.status === 'fulfilled',
)

if (!success) {
  throw new Error('All sends failed')
}

const signature = success.value
  • 签名在所有端点中相同
  • 发送成功不等于确认

步骤 8:使用 Durable Nonce 进行确认

使用 Durable Nonce 时,在确认中包含 nonce 信息。
typescript
await connection.confirmTransaction(
  {
    signature,
    nonceAccountPubkey: nonceAccount.publicKey,
    nonceValue: nonce,
    minContextSlot,
  },
  'confirmed',
)
确认成功后:
  • nonce 会推进
  • 发送到其他 RPC 的副本将返回 InvalidNonce

后续发送

  • 确认后,获取新的 nonce
  • 不要重用相同的 rawTx 或 nonce 值
  • 对于并行工作流,使用独立的 nonce 账户
对于下一笔交易,获取更新后的 nonce 并使用相同步骤构建交易。