Contexto e cenário de uso
No envio de transações na Solana, o avanço dos slots, a posição dos líderes e o congestionamento dos caminhos de rede mudam constantemente qual rota chega mais rápido ao líder. Isso não é específico de nenhum provedor de RPC ou serviço de envio; é uma característica estrutural do modelo de execução da Solana.
O endpoint SWQoS da ERPC oferece um caminho de envio que injeta as transações na faixa de prioridade que os líderes alocam com base no Stake-weighted Quality of Service (SWQoS). Essa largura de banda prioritária é cerca de 5× a da faixa não prioritária e é aplicada antes da avaliação da taxa de prioridade.
Por isso, o endpoint SWQoS é uma opção importante para o envio de transações, mas em produção um único endpoint nem sempre é o mais rápido. Mesmo dentro do mesmo slot, diferenças momentâneas de caminho ou desequilíbrios de carga podem fazer outro endpoint rápido sair na frente.
Diante dessas características, em vez de depender de uma única rota, pode ser eficaz um padrão operacional que envia a mesma transação a vários caminhos rápidos em paralelo e aceita a primeira que for processada.
No entanto, quando a mesma transação é enviada a vários endpoints, sem um controle adicional não é possível garantir que ela seja executada apenas uma vez. Um fan-out sem esse controle pode levar a uma execução dupla não intencional ou a um controle de novas tentativas quebrado.
A Solana oferece o Durable Nonce como mecanismo para isso. Com o Durable Nonce, você pode enviar a mesma transação assinada por várias rotas e, ao mesmo tempo, limitar 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 da ERPC com outros endpoints RPC rápidos, partindo do envio de transações com Durable Nonce.
Escopo e pré-requisitos
Este guia aborda a criação de uma conta Durable Nonce com web3.js e o seu uso no envio de transações e em operações de fan-out.
Pré-requisitos que você precisa entender:
- Em transações com Durable Nonce, use o valor do nonce como
recentBlockhash e coloque nonceAdvance como a primeira instrução
- Depois que
nonceAdvance é executado, o nonce pode ser consumido mesmo que as instruções seguintes falhem. Não é possível reenviar o mesmo rawTx sem alterações.
- A criação da conta nonce é uma configuração feita uma única vez e, normalmente, a conta é reutilizada
Passo 1: Preparar a nonce authority e a conexão
A nonce authority é um Keypair que pode avançar o nonce.
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 nonce authority pode executar
nonceAdvance
- Ela pode ser o fee payer ou um keypair separado
Passo 2: Gerar um Keypair para a conta nonce
Uma conta nonce é uma System Account.
const nonceAccount = Keypair.generate()
const nonceAccount = Keypair.generate()
Este Keypair é usado para:
- Guardar o valor do nonce (que substitui o
recentBlockhash)
- Assinar apenas no momento da criação
- Ser guardado com segurança depois; ele não é necessário nos envios do dia a dia
Passo 3: Calcular o mínimo rent-exempt
As contas nonce precisam ser rent-exempt. Não fixe os valores no código; obtenha-os pelo RPC.
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.
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',
)
Use este nonceAccount.publicKey nos envios seguintes.
Passo 5: Obter o nonce antes de cada envio
Para cada transação, obtenha o valor atual do nonce.
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
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
- Use
nonce como recentBlockhash
context.slot é usado na confirmação
Passo 6: Montar a transação Durable Nonce
As transações Durable Nonce precisam atender às seguintes condições:
recentBlockhash = nonce
- A primeira instrução é
nonceAdvance
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()
Observações:
- Se você usar instruções ComputeBudget, elas devem vir depois de
nonceAdvance
- Se
nonceAdvance não for a primeira, a transação será rejeitada
Passo 7: Fazer fan-out para vários RPCs
Envie o mesmo rawTx em paralelo.
const endpoints = [
'https://swqos-fra2.erpc.global?api-key=YOUR_API_KEY',
'https://swqos-ams1.erpc.global?api-key=YOUR_API_KEY',
'https://<backup-rpc-1>',
]
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
const endpoints = [
'https://swqos-fra2.erpc.global?api-key=YOUR_API_KEY',
'https://swqos-ams1.erpc.global?api-key=YOUR_API_KEY',
'https://<backup-rpc-1>',
]
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 em todos os endpoints
- Um envio bem-sucedido não é uma confirmação
Com Durable Nonce, inclua as informações do nonce na confirmação.
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 for bem-sucedida:
- O nonce avança
- As cópias enviadas aos outros RPCs retornam
InvalidNonce
Próximos envios
- Depois da confirmação, obtenha um novo nonce
- Não reutilize o mesmo rawTx nem o mesmo valor de nonce
- Em fluxos de trabalho paralelos, use contas nonce separadas
Para a próxima transação, obtenha o nonce atualizado e monte a transação seguindo os mesmos passos.