Durable Nonce Fan-out untuk SWQoS
Latar belakang dan skenario penggunaan
Dalam pengiriman transaksi Solana, progresi slot, penempatan leader, dan kemacetan jalur jaringan terus-menerus mengubah rute mana yang paling cepat mencapai leader. Hal ini tidak khusus pada penyedia RPC atau layanan pengiriman mana pun; ini adalah karakteristik struktural dari model eksekusi Solana.
SWQoS Endpoint milik ERPC menyediakan jalur pengiriman yang menyuntikkan transaksi ke jalur prioritas yang dialokasikan oleh Leader berdasarkan Stake-weighted Quality of Service (SWQoS). Bandwidth prioritas ini sekitar 5x dibandingkan jalur non-prioritas dan diterapkan sebelum evaluasi Priority-fee.
Karena itu, SWQoS Endpoint adalah opsi penting untuk pengiriman transaksi, tetapi di lingkungan produksi satu endpoint saja tidak selalu menjadi yang tercepat. Bahkan di dalam slot yang sama, perbedaan jalur yang bersifat sementara atau ketimpangan beban dapat membuat endpoint cepat lainnya unggul.
Mengingat karakteristik tersebut, alih-alih mengandalkan satu rute saja, pola operasional berupa pengiriman transaksi yang sama secara paralel ke beberapa jalur cepat dan menerima yang pertama kali diproses dapat menjadi efektif.
Namun, ketika transaksi yang sama dikirim ke beberapa endpoint, tanpa kontrol tambahan Anda tidak dapat menjamin bahwa transaksi tersebut hanya dieksekusi satu kali. Fan-out tanpa kontrol ini dapat menyebabkan eksekusi ganda yang tidak diinginkan atau kontrol retry yang rusak.
Solana menyediakan Durable Nonce sebagai mekanisme untuk hal ini. Dengan Durable Nonce, Anda dapat mengirim transaksi yang sama dan sudah ditandatangani melalui beberapa rute, sekaligus membatasi eksekusi on-chain hanya satu kali.
Halaman ini menjelaskan cara mengimplementasikan operasi fan-out yang menggabungkan SWQoS Endpoint milik ERPC dengan endpoint RPC cepat lainnya, dengan asumsi pengiriman transaksi dilakukan menggunakan Durable Nonce.
Cakupan dan prasyarat
Panduan ini mencakup pembuatan akun Durable Nonce dengan web3.js serta penggunaannya untuk pengiriman transaksi dan operasi fan-out.
Prasyarat yang perlu dipahami:
- Untuk transaksi Durable Nonce, gunakan nilai nonce sebagai
recentBlockhashdan tempatkannonceAdvancesebagai instruksi pertama - Setelah
nonceAdvancedieksekusi, nonce dapat terpakai meskipun instruksi berikutnya gagal. Anda tidak dapat mengirim ulang rawTx yang sama apa adanya. - Pembuatan akun nonce adalah penyiapan sekali jalan dan akun tersebut biasanya digunakan kembali
Langkah 1: Siapkan otoritas nonce dan koneksi
Otoritas nonce adalah Keypair yang dapat memajukan nonce tersebut.
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 */)- Otoritas nonce dapat mengeksekusi
nonceAdvance - Otoritas ini bisa menjadi fee payer, atau berupa Keypair terpisah
Langkah 2: Buat Keypair untuk akun nonce
Akun nonce adalah System Account.
typescript
const nonceAccount = Keypair.generate()const nonceAccount = Keypair.generate()Keypair ini digunakan untuk:
- Menyimpan nilai nonce (pengganti
recentBlockhash) - Menandatangani hanya pada saat pembuatan
- Penyimpanan yang aman setelahnya; Keypair ini tidak diperlukan untuk pengiriman harian
Langkah 3: Hitung saldo minimum rent-exempt
Akun nonce harus rent-exempt. Jangan menuliskan nilainya secara hardcode; ambil nilai tersebut dari RPC.
typescript
const lamports =
await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)const lamports =
await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)Langkah 4: Buat dan inisialisasi akun nonce
Buat akun nonce dengan createAccount + nonceInitialize dalam satu transaksi.
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',
)Gunakan
nonceAccount.publicKey ini untuk pengiriman berikutnya.Langkah 5: Ambil nonce sebelum setiap pengiriman
Untuk setiap transaksi, ambil nilai nonce saat ini.
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- Gunakan
noncesebagairecentBlockhash context.slotdigunakan dalam konfirmasi
Langkah 6: Bangun transaksi Durable Nonce
Transaksi Durable Nonce harus memenuhi kondisi berikut:
recentBlockhash = nonce- Instruksi pertama adalah
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()Catatan:
- Jika Anda menggunakan instruksi ComputeBudget, instruksi tersebut harus ditempatkan setelah
nonceAdvance - Jika
nonceAdvancetidak berada di urutan pertama, transaksi akan ditolak
Langkah 7: Fan-out ke beberapa RPC
Kirim rawTx yang sama secara bersamaan.
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.valueconst 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- Tanda tangan identik di seluruh endpoint
- Pengiriman yang berhasil bukan merupakan konfirmasi
Langkah 8: Konfirmasi dengan Durable Nonce
Dengan Durable Nonce, sertakan informasi nonce dalam konfirmasi.
typescript
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)Jika konfirmasi berhasil:
- Nonce akan maju
- Salinan yang dikirim ke RPC lain akan mengembalikan
InvalidNonce
Pengiriman berikutnya
- Setelah konfirmasi, ambil nonce yang baru
- Jangan gunakan kembali rawTx atau nilai nonce yang sama
- Untuk alur kerja paralel, gunakan akun nonce yang terpisah
Untuk transaksi berikutnya, ambil nonce yang telah diperbarui dan bangun transaksi menggunakan langkah-langkah yang sama.






