Fan-out con Durable Nonce per SWQoS

Contesto e scenario di utilizzo

Nell'invio di transazioni Solana, la progressione degli slot, il posizionamento dei leader e la congestione dei percorsi di rete cambiano costantemente quale rotta raggiunge più velocemente il leader. Questo non è specifico di alcun provider RPC o servizio di invio; è una caratteristica strutturale del modello di esecuzione di Solana.
L'endpoint SWQoS di ERPC fornisce un percorso di invio che immette le transazioni nella corsia prioritaria allocata dai leader in base alla Stake-weighted Quality of Service (SWQoS). Questa banda prioritaria è circa 5 volte quella della corsia non prioritaria e viene applicata prima della valutazione della priority fee.
Per questo motivo, l'endpoint SWQoS è un'opzione importante per l'invio delle transazioni, ma in produzione un singolo endpoint non è sempre il più veloce. Anche all'interno dello stesso slot, differenze transitorie di percorso o squilibri di carico possono far sì che un altro endpoint veloce risulti in vantaggio.
Date queste caratteristiche, un pattern operativo che invia la stessa transazione a più percorsi veloci in parallelo e accetta il primo che viene elaborato può essere efficace, invece di affidarsi a un'unica rotta.
Tuttavia, quando la stessa transazione viene inviata a più endpoint, non puoi garantire che venga eseguita una sola volta senza un controllo aggiuntivo. Il fan-out senza questo controllo può portare a una doppia esecuzione indesiderata o a un controllo dei retry compromesso.
Solana fornisce il Durable Nonce come meccanismo per questo. Usare il Durable Nonce ti consente di inviare la stessa transazione firmata su più rotte limitando l'esecuzione on-chain a una sola volta.
Questa pagina spiega come implementare operazioni di fan-out che combinano l'endpoint SWQoS di ERPC con altri endpoint RPC veloci, assumendo l'invio di transazioni con Durable Nonce.

Ambito e prerequisiti

Questa guida copre la creazione di un account Durable Nonce con web3.js e il suo utilizzo per l'invio di transazioni e operazioni di fan-out.
Prerequisiti da comprendere:
  • Per le transazioni Durable Nonce, usa il valore del nonce come recentBlockhash e posiziona nonceAdvance come prima istruzione
  • Una volta eseguito nonceAdvance, il nonce può essere consumato anche se le istruzioni successive falliscono. Non puoi reinviare lo stesso rawTx così com'è.
  • La creazione dell'account nonce è una configurazione una tantum e viene tipicamente riutilizzata

Passo 1: Preparare la nonce authority e la connessione

La nonce authority è un Keypair che può far avanzare il 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 */)
  • La nonce authority può eseguire nonceAdvance
  • Può essere il fee payer, oppure un keypair separato

Passo 2: Generare un Keypair per l'account nonce

Un account nonce è un System Account.
typescript
const nonceAccount = Keypair.generate()
Questo Keypair viene utilizzato per:
  • Conservare il valore del nonce (sostituto di recentBlockhash)
  • Firmare solo al momento della creazione
  • Essere conservato in sicurezza in seguito; non è necessario per gli invii quotidiani

Passo 3: Calcolare il minimo rent-exempt

Gli account nonce devono essere rent-exempt. Non codificare i valori in modo statico; recuperali dall'RPC.
typescript
const lamports =
  await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)

Passo 4: Creare e inizializzare l'account nonce

Crea l'account nonce con createAccount + nonceInitialize in una singola transazione.
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',
)
Usa questo nonceAccount.publicKey per gli invii successivi.

Passo 5: Recuperare il nonce prima di ogni invio

Per ogni transazione, recupera il valore corrente del 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.slot
  • Usa nonce come recentBlockhash
  • context.slot viene utilizzato nella conferma

Passo 6: Costruire la transazione Durable Nonce

Le transazioni Durable Nonce devono soddisfare le seguenti condizioni:
  • recentBlockhash = nonce
  • La prima istruzione è 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()
Note:
  • Se usi istruzioni ComputeBudget, devono venire dopo nonceAdvance
  • Se nonceAdvance non è la prima, la transazione verrà rifiutata

Passo 7: Fan-out verso più RPC

Invia lo stesso rawTx in contemporanea.
typescript
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 firma è identica su tutti gli endpoint
  • Un invio riuscito non è una conferma

Passo 8: Confermare con il Durable Nonce

Con il Durable Nonce, includi le informazioni sul nonce nella conferma.
typescript
await connection.confirmTransaction(
  {
    signature,
    nonceAccountPubkey: nonceAccount.publicKey,
    nonceValue: nonce,
    minContextSlot,
  },
  'confirmed',
)
Se la conferma ha successo:
  • Il nonce avanza
  • Le copie inviate agli altri RPC restituiscono InvalidNonce

Invii successivi

  • Dopo la conferma, recupera un nuovo nonce
  • Non riutilizzare lo stesso rawTx o valore del nonce
  • Per flussi di lavoro paralleli, usa account nonce separati
Per la transazione successiva, recupera il nonce aggiornato e costruisci la transazione usando gli stessi passaggi.