Fan-out Durable Nonce pour SWQoS

Contexte et cas d'usage

Lors de l'envoi de transactions sur Solana, la progression des slots, le placement des leaders et la congestion des chemins réseau modifient sans cesse la route qui atteint le leader le plus rapidement. Ce phénomène n'est propre à aucun fournisseur RPC ni à aucun service d'envoi : c'est une caractéristique structurelle du modèle d'exécution de Solana.
L'endpoint SWQoS d'ERPC fournit un chemin d'envoi qui injecte les transactions dans la voie prioritaire que les leaders allouent selon le Stake-weighted Quality of Service (SWQoS). Cette bande passante prioritaire représente environ 5× celle de la voie non prioritaire et s'applique avant l'évaluation des frais de priorité.
C'est pourquoi l'endpoint SWQoS est une option importante pour l'envoi de transactions ; en production, toutefois, un endpoint unique n'est pas toujours le plus rapide. Même au sein d'un même slot, des écarts de chemin passagers ou un déséquilibre de charge peuvent permettre à un autre endpoint rapide de passer devant.
Compte tenu de ces caractéristiques, plutôt que de dépendre d'une route unique, il peut être efficace d'envoyer la même transaction en parallèle sur plusieurs chemins rapides et de retenir la première qui est traitée.
Cependant, lorsque la même transaction est envoyée à plusieurs endpoints, rien ne garantit, sans contrôle supplémentaire, qu'elle ne sera exécutée qu'une seule fois. Un fan-out sans ce contrôle peut entraîner une double exécution involontaire ou casser le contrôle des nouvelles tentatives.
Solana fournit pour cela le mécanisme Durable Nonce. Avec Durable Nonce, vous pouvez envoyer la même transaction signée par plusieurs routes tout en limitant son exécution on-chain à une seule fois.
Cette page explique comment mettre en œuvre des opérations de fan-out qui combinent l'endpoint SWQoS d'ERPC avec d'autres endpoints RPC rapides, pour un envoi de transactions avec Durable Nonce.

Portée et prérequis

Ce guide couvre la création d'un compte Durable Nonce avec web3.js et son utilisation pour l'envoi de transactions et les opérations de fan-out.
Prérequis à connaître :
  • Pour les transactions Durable Nonce, utilisez la valeur du nonce comme recentBlockhash et placez nonceAdvance en première instruction
  • Une fois nonceAdvance exécutée, le nonce peut être consommé même si les instructions suivantes échouent. Vous ne pouvez pas renvoyer le même rawTx tel quel.
  • La création du compte nonce est une configuration ponctuelle, et le compte est généralement réutilisé

Étape 1 : Préparer l'autorité du nonce et la connexion

L'autorité du nonce est un Keypair qui peut faire avancer le 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 */)
  • L'autorité du nonce peut exécuter nonceAdvance
  • Elle peut être le payeur des frais ou un keypair distinct

Étape 2 : Générer le Keypair du compte nonce

Un compte nonce est un compte système.
ts
const nonceAccount = Keypair.generate()
Ce Keypair sert à :
  • Conserver la valeur du nonce (qui remplace recentBlockhash)
  • Signer uniquement lors de la création
  • Être stocké en lieu sûr ensuite ; il n'est pas nécessaire pour les envois courants

Étape 3 : Calculer le minimum pour l'exonération de loyer

Les comptes nonce doivent être exonérés de loyer. Ne codez pas les valeurs en dur : récupérez-les depuis le RPC.
ts
const lamports =
  await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)

Étape 4 : Créer et initialiser le compte nonce

Créez le compte nonce avec createAccount + nonceInitialize dans une seule transaction.
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',
)
Utilisez ce nonceAccount.publicKey pour les envois suivants.

Étape 5 : Récupérer le nonce avant chaque envoi

Pour chaque transaction, récupérez la valeur actuelle du 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
  • Utilisez nonce comme recentBlockhash
  • context.slot sert à la confirmation

Étape 6 : Construire la transaction Durable Nonce

Les transactions Durable Nonce doivent remplir les conditions suivantes :
  • recentBlockhash = nonce
  • La première instruction est 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()
Remarques :
  • Si vous utilisez des instructions ComputeBudget, elles doivent venir après nonceAdvance
  • Si nonceAdvance n'est pas en première position, la transaction sera rejetée

Étape 7 : Fan-out vers plusieurs RPC

Envoyez le même rawTx simultanément.
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
  • La signature est identique d'un endpoint à l'autre
  • Un envoi réussi n'est pas une confirmation

Étape 8 : Confirmer avec Durable Nonce

Avec Durable Nonce, incluez les informations du nonce dans la confirmation.
ts
await connection.confirmTransaction(
  {
    signature,
    nonceAccountPubkey: nonceAccount.publicKey,
    nonceValue: nonce,
    minContextSlot,
  },
  'confirmed',
)
Si la confirmation réussit :
  • Le nonce avance
  • Les copies envoyées aux autres RPC renvoient InvalidNonce

Envois suivants

  • Après la confirmation, récupérez un nouveau nonce
  • Ne réutilisez ni le même rawTx ni la même valeur de nonce
  • Pour les workflows parallèles, utilisez des comptes nonce distincts
Pour la transaction suivante, récupérez le nonce mis à jour et construisez la transaction en suivant les mêmes étapes.