Fan-out Nonce durável para SWQoS
Antecedentes e cenário de uso
No envio de transações Solana, progressão de slots, colocação de líder e congestionamento de caminho de rede mudam constantemente qual rota atinge o líder mais rápido. Isto não é específico para qualquer fornecedor de RPC ou serviço de envio; é uma característica estrutural do modelo de execução de Solana.
O endpoint SWQoS do ERPC fornece um caminho de envio que injeta transações na faixa de prioridade alocada pelos líderes com base na Qualidade de Serviço ponderada em Stake (SWQoS). Esta largura de banda de prioridade é de cerca de 5× a da faixa não prioritária e é aplicada antes da avaliação da taxa de prioridade.
Por essa razão, o endpoint SWQoS é uma opção importante para o envio de transações, mas na produção um único endpoint nem sempre é o mais rápido. Mesmo dentro do mesmo slot, diferenças transientes de caminho ou inclinação de carga podem permitir que outro endpoint rápido leve.
Dadas essas características, um padrão operacional de enviar a mesma transação para múltiplos caminhos rápidos em paralelo e aceitar o primeiro processado pode ser eficaz, em vez de confiar em uma única rota.
No entanto, quando a mesma transação é enviada para múltiplos endpoints, você não pode garantir que ela será executada apenas uma vez sem controle adicional. Fan-out sem este controle pode levar a execução dupla não intencional ou controle de repetição quebrado.
Solana fornece Nonce durável como o mecanismo para isso. Usar o Durable Nonce permite enviar a mesma transação assinada por várias rotas, enquanto limita a execução on-chain a uma única vez.
Esta página explica como implementar operações de fan-out que combinam o endpoint SWQoS do ERPC com outros endpoints rápidos do RPC, assumindo o envio de transações com o Durable Nonce.
Âmbito de aplicação e condições prévias
Este guia cobre a criação de uma conta Nonce durável com web3.js e usá-la para operações de envio de transações e fan-out.
Pré-requisitos para compreender:
- Para transações Nonce duráveis, use o valor nonce como
recentBlockhashe lugarnonceAdvancecomo a primeira instrução - Uma vez
nonceAdvanceexecuta, o nonce pode ser consumido mesmo se instruções posteriores falharem. Você não pode reenviar o mesmo rawTx que está. - A criação da conta Nonce é uma configuração única e é normalmente reutilizada
Etapa 1: Prepare a autoridade e a conexão do nonce
A autoridade nonce é um Keypair que pode avançar o nonce.
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 */)- A autoridade nonce pode executar
nonceAdvance - Pode ser o pagador de taxa, ou um keypair separado
Passo 2: Gerar uma conta nonce Keypair
Uma conta nonce é uma Conta do sistema.
typescript
const nonceAccount = Keypair.generate()const nonceAccount = Keypair.generate()Este Keypair é usado para:
- Detendo o valor nonce (substituição para
recentBlockhash) - Assinatura apenas na hora da criação
- Armazenamento seguro depois; não é necessário para envios diários
Etapa 3: Calcule o mínimo sem renda
As contas do Nonce devem ser isentas de renda. Não codificar os valores; obtê- los do RPC.
typescript
const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)Passo 4: Criar e inicializar a conta nonce
Crie a conta nonce com createAccount + nonceInitialize em uma única transação.
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',
)Usar isto
nonceAccount.publicKey para envios subsequentes.Passo 5: Busque o nonce antes de cada envio
Para cada transação, obtenha o valor atual do 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- Use
noncecomorecentBlockhash context.sloté utilizado na confirmação
Passo 6: Construir a transação Nonce durável
As operações de Nonce duráveis devem satisfazer as seguintes condições:
recentBlockhash = nonce- A primeira instrução é
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()Notas:
- Se você usar as instruções do ComputeBudget, elas devem vir após ``nonceAdvance`
- Se
nonceAdvancenão é primeiro, a transação será rejeitada
Passo 7: Espalhe-se para múltiplos RPCs
Envie o mesmo rawTx simultaneamente.
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- A assinatura é idêntica entre os parâmetros
- Um envio bem- sucedido é não a confirmation
Passo 8: Confirme com Nonce durável
Com Durable Nonce, inclua a informação nonce na confirmação.
typescript
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)Se a confirmação tiver sucesso:
- O nonce avança
- Cópias enviadas para outros RPC
InvalidNonce
Envios seguintes
- Após confirmação, buscar um nonce fresco
- Não reutilize o mesmo valor bruto Tx ou nonce
- Para fluxos de trabalho paralelos, use contas nonce separadas
Para a próxima transação, obter o nonce atualizado e compilar a transação usando os mesmos passos.