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 */)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()const nonceAccount = Keypair.generate()此金鑰對用於:
- 儲存 nonce 值(替代
recentBlockhash) - 僅在建立時簽名
- 之後安全儲存;日常傳送不需要使用
步驟 3:計算免租最低餘額
Nonce 賬戶必須免租。不要硬編碼值;從 RPC 獲取。
typescript
const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)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',
)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.slotimport { 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()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.valueconst 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',
)await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)確認成功後:
- nonce 會推進
- 傳送到其他 RPC 的副本將返回
InvalidNonce
後續傳送
- 確認後,獲取新的 nonce
- 不要重用相同的 rawTx 或 nonce 值
- 對於並行工作流,使用獨立的 nonce 賬戶
對於下一筆交易,獲取更新後的 nonce 並使用相同步驟構建交易。