Durable Nonce Fan-out cho SWQoS
Bối cảnh và kịch bản sử dụng
Trong việc gửi transaction Solana, tiến trình slot, vị trí leader, và tắc nghẽn đường mạng liên tục thay đổi tuyến đường nào đến leader nhanh nhất. Đây không phải là vấn đề riêng của bất kỳ nhà cung cấp RPC hay dịch vụ gửi nào; đó là đặc tính cấu trúc của mô hình thực thi Solana.
Endpoint SWQoS của ERPC cung cấp đường gửi đưa transaction vào làn ưu tiên được phân bổ bởi leader dựa trên Stake-weighted Quality of Service (SWQoS). Băng thông ưu tiên này gấp khoảng 5 lần so với làn không ưu tiên và được áp dụng trước khi đánh giá priority fee.
Vì lý do đó, endpoint SWQoS là một tùy chọn quan trọng cho việc gửi transaction, nhưng trong production một endpoint duy nhất không phải lúc nào cũng nhanh nhất. Ngay trong cùng một slot, sự khác biệt đường dẫn tạm thời hoặc lệch tải có thể cho phép endpoint nhanh khác dẫn đầu.
Với những đặc điểm này, pattern vận hành gửi cùng một transaction đến nhiều đường nhanh song song và chấp nhận cái đầu tiên được xử lý có thể hiệu quả, thay vì phụ thuộc vào một tuyến đường duy nhất.
Tuy nhiên, khi cùng một transaction được gửi đến nhiều endpoint, bạn không thể đảm bảo rằng nó sẽ chỉ thực thi một lần mà không có kiểm soát bổ sung. Fan-out không có kiểm soát này có thể dẫn đến thực thi kép ngoài ý muốn hoặc kiểm soát retry bị hỏng.
Solana cung cấp Durable Nonce là cơ chế cho điều này. Sử dụng Durable Nonce cho phép bạn gửi cùng một transaction đã ký qua nhiều tuyến đường trong khi giới hạn thực thi trên chain chỉ một lần duy nhất.
Trang này giải thích cách triển khai các thao tác fan-out kết hợp endpoint SWQoS của ERPC với các endpoint RPC nhanh khác, giả định gửi transaction với Durable Nonce.
Phạm vi và điều kiện tiên quyết
Hướng dẫn này bao gồm việc tạo Durable Nonce account với web3.js và sử dụng nó cho gửi transaction và thao tác fan-out.
Điều kiện cần hiểu:
- Đối với Durable Nonce transaction, sử dụng giá trị nonce làm
recentBlockhashvà đặtnonceAdvancelàm instruction đầu tiên - Khi
nonceAdvancethực thi, nonce có thể bị tiêu thụ ngay cả khi các instruction sau thất bại. Bạn không thể gửi lại cùng rawTx nguyên trạng. - Việc tạo nonce account là thiết lập một lần và thường được tái sử dụng
Bước 1: Chuẩn bị nonce authority và connection
Nonce authority là một Keypair có thể advance 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 */)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 */)- Nonce authority có thể thực thi
nonceAdvance - Nó có thể là fee payer, hoặc một keypair riêng biệt
Bước 2: Tạo Keypair cho nonce account
Nonce account là một System Account.
typescript
const nonceAccount = Keypair.generate()const nonceAccount = Keypair.generate()Keypair này được sử dụng cho:
- Lưu giữ giá trị nonce (thay thế cho
recentBlockhash) - Ký chỉ tại thời điểm tạo
- Lưu trữ an toàn sau đó; không cần cho việc gửi hàng ngày
Bước 3: Tính toán mức tối thiểu miễn rent
Nonce account phải được miễn rent. Không hardcode giá trị; hãy lấy từ RPC.
typescript
const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)const lamports =
await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH,
)Bước 4: Tạo và khởi tạo nonce account
Tạo nonce account với createAccount + nonceInitialize trong một transaction duy nhất.
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 là nonce authority
tx.feePayer = nonceAuthority.publicKey
// sử dụng blockhash bình thường cho khởi tạo
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed')
tx.recentBlockhash = blockhash
// ký với cả account mới và 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 là nonce authority
tx.feePayer = nonceAuthority.publicKey
// sử dụng blockhash bình thường cho khởi tạo
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed')
tx.recentBlockhash = blockhash
// ký với cả account mới và authority
tx.sign(nonceAccount, nonceAuthority)
const signature = await connection.sendRawTransaction(tx.serialize())
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
'confirmed',
)Sử dụng
nonceAccount.publicKey này cho các lần gửi tiếp theo.Bước 5: Lấy nonce trước mỗi lần gửi
Cho mỗi transaction, hãy lấy giá trị nonce hiện tại.
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- Sử dụng
noncelàmrecentBlockhash context.slotđược sử dụng trong xác nhận
Bước 6: Xây dựng Durable Nonce transaction
Durable Nonce transaction phải thỏa mãn các điều kiện sau:
recentBlockhash = nonce- Instruction đầu tiên là
nonceAdvance
typescript
import {
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js'
const instructions = [
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
// thêm instruction thực tế của bạn sau đây
]
const message = new TransactionMessage({
payerKey: nonceAuthority.publicKey,
recentBlockhash: nonce,
instructions,
}).compileToV0Message()
const tx = new VersionedTransaction(message)
tx.sign([nonceAuthority /* + các signer khác */])
const rawTx = tx.serialize()import {
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js'
const instructions = [
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: nonceAuthority.publicKey,
}),
// thêm instruction thực tế của bạn sau đây
]
const message = new TransactionMessage({
payerKey: nonceAuthority.publicKey,
recentBlockhash: nonce,
instructions,
}).compileToV0Message()
const tx = new VersionedTransaction(message)
tx.sign([nonceAuthority /* + các signer khác */])
const rawTx = tx.serialize()Lưu ý:
- Nếu bạn sử dụng ComputeBudget instruction, chúng phải đặt sau
nonceAdvance - Nếu
nonceAdvancekhông phải là đầu tiên, transaction sẽ bị từ chối
Bước 7: Fan out đến nhiều RPC
Gửi cùng rawTx đồng thời.
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- Signature giống nhau trên tất cả endpoint
- Gửi thành công không phải là xác nhận
Bước 8: Xác nhận với Durable Nonce
Với Durable Nonce, bao gồm thông tin nonce trong xác nhận.
typescript
await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)await connection.confirmTransaction(
{
signature,
nonceAccountPubkey: nonceAccount.publicKey,
nonceValue: nonce,
minContextSlot,
},
'confirmed',
)Nếu xác nhận thành công:
- Nonce được advance
- Bản sao gửi đến RPC khác trả về
InvalidNonce
Các lần gửi tiếp theo
- Sau khi xác nhận, lấy nonce mới
- Không tái sử dụng cùng rawTx hoặc giá trị nonce
- Cho các workflow song song, sử dụng các nonce account riêng biệt
Cho transaction tiếp theo, hãy lấy nonce đã cập nhật và xây dựng transaction sử dụng các bước tương tự.