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 并使用相同步骤构建交易。