Durable Nonce Fan-out voor SWQoS
Achtergrond en gebruiksscenario
Bij het verzenden van Solana-transacties veranderen slotvoortgang, leaderplaatsing en netwerk-padcongestie voortdurend welke route de leader het snelst bereikt. Dit is niet specifiek voor een RPC-provider of verzendservice; het is een structureel kenmerk van Solana's uitvoeringsmodel.
ERPC's SWQoS-endpoint biedt een verzendpad dat transacties injecteert in de prioriteitsbaan die door leaders wordt toegewezen op basis van Stake-weighted Quality of Service (SWQoS). Deze prioriteitsbandbreedte is ongeveer 5x die van de niet-prioriteitsbaan en wordt toegepast voor de evaluatie van prioriteitskosten.
Om die reden is het SWQoS-endpoint een belangrijke optie voor het verzenden van transacties, maar in productie is een enkel endpoint niet altijd het snelst. Zelfs binnen dezelfde slot kunnen tijdelijke padverschillen of belastingscheefheid een ander snel endpoint laten leiden.
Gezien deze kenmerken kan een operationeel patroon van het parallel verzenden van dezelfde transactie naar meerdere snelle paden en het accepteren van de eerste die wordt verwerkt effectief zijn, in plaats van te vertrouwen op een enkele route.
Echter, wanneer dezelfde transactie naar meerdere endpoints wordt verzonden, kunt u niet garanderen dat deze slechts eenmaal wordt uitgevoerd zonder aanvullende controle. Fan-out zonder deze controle kan leiden tot onbedoelde dubbele uitvoering of gebroken retry-controle.
Solana biedt Durable Nonce als mechanisme hiervoor. Het gebruik van Durable Nonce stelt u in staat dezelfde ondertekende transactie over meerdere routes te verzenden terwijl de on-chain uitvoering beperkt wordt tot een enkele keer.
Deze pagina legt uit hoe u fan-out-operaties implementeert die ERPC's SWQoS-endpoint combineren met andere snelle RPC-endpoints, uitgaande van transactieverzending met Durable Nonce.
Scope en vereisten
Deze handleiding behandelt het aanmaken van een Durable Nonce-account met web3.js en het gebruik ervan voor transactieverzending en fan-out-operaties.
Te begrijpen vereisten:
- Voor Durable Nonce-transacties gebruikt u de nonce-waarde als
recentBlockhashen plaatst unonceAdvanceals eerste instructie - Zodra
nonceAdvancewordt uitgevoerd, kan de nonce worden verbruikt zelfs als latere instructies falen. U kunt dezelfde rawTx niet opnieuw verzenden. - Het aanmaken van een nonce-account is eenmalig en wordt doorgaans hergebruikt
Stap 1: Bereid de nonce-autoriteit en verbinding voor
De nonce-autoriteit is een Keypair dat de nonce kan doorschuiven.
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 */)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 */)- De nonce-autoriteit kan
nonceAdvanceuitvoeren - Het kan de fee payer zijn, of een apart keypair
Stap 2: Genereer een nonce-account Keypair
Een nonce-account is een System Account.
typescript
const nonceAccount = Keypair.generate()const nonceAccount = Keypair.generate()Dit Keypair wordt gebruikt voor:
- Het bewaren van de nonce-waarde (vervanging voor
recentBlockhash) - Ondertekenen alleen bij aanmaak
- Veilige opslag daarna; het is niet nodig voor dagelijkse verzendingen
Stap 3: Bereken het huurvrijstellingsminimum
Nonce-accounts moeten huurvrijgesteld zijn. Hardcode geen waarden; haal ze op via de RPC.
typescript
const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)Stap 4: Maak en initialiseer het nonce-account
Maak het nonce-account aan met createAccount + nonceInitialize in een enkele transactie.
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',
)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',
)Gebruik deze
nonceAccount.publicKey voor volgende verzendingen.Stap 5: Haal de nonce op voor elke verzending
Haal voor elke transactie de huidige nonce-waarde op.
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.slotimport { 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- Gebruik
noncealsrecentBlockhash context.slotwordt gebruikt bij bevestiging
Stap 6: Bouw de Durable Nonce-transactie
Durable Nonce-transacties moeten aan de volgende voorwaarden voldoen:
recentBlockhash = nonce- De eerste instructie is
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()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()Opmerkingen:
- Als u ComputeBudget-instructies gebruikt, moeten deze na
nonceAdvancekomen - Als
nonceAdvanceniet als eerste staat, wordt de transactie afgewezen
Stap 7: Fan-out naar meerdere RPC's
Verzend dezelfde rawTx gelijktijdig.
typescript
const endpoints = [
'https://<erpc-swqos-fra>',
'https://<backup-rpc-1>',
'https://<backup-rpc-2>',
]
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.valueconst endpoints = [
'https://<erpc-swqos-fra>',
'https://<backup-rpc-1>',
'https://<backup-rpc-2>',
]
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- De handtekening is identiek voor alle endpoints
- Een succesvolle verzending is geen bevestiging
Stap 8: Bevestig met Durable Nonce
Bij Durable Nonce neemt u de nonce-info op in de bevestiging.
typescript
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)Als de bevestiging slaagt:
- De nonce schuift door
- Kopies verzonden naar andere RPC's retourneren
InvalidNonce
Volgende verzendingen
- Haal na bevestiging een nieuwe nonce op
- Hergebruik niet dezelfde rawTx of nonce-waarde
- Gebruik voor parallelle workflows aparte nonce-accounts
Haal voor de volgende transactie de bijgewerkte nonce op en bouw de transactie met dezelfde stappen.