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 recentBlockhash và đặt nonceAdvance làm instruction đầu tiên
  • Khi nonceAdvance thự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 */)
  • 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()
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,
  )

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',
)
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.slot
  • Sử dụng nonce làm recentBlockhash
  • 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()
Lưu ý:
  • Nếu bạn sử dụng ComputeBudget instruction, chúng phải đặt sau nonceAdvance
  • Nếu nonceAdvance khô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.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',
)
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ự.