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.
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 */)
- 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.
const nonceAccount = Keypair.generate()
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.
const lamports =
await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)
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.
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',
)
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.
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
- 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
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()
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.
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
- 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.
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)
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.