Fan-out com Durable Nonce para SWQoS

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.
ts
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.
ts
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.
ts
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.
ts
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.
ts
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
ts
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.
ts
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

Passo 8: Confirmar com Durable Nonce

Com Durable Nonce, inclua as informações do nonce na confirmação.
ts
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.