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 並使用相同步驟構建交易。