Durable Nonce Fan-out для SWQoS

Контекст и сценарий использования

При отправке Solana transactions продвижение slots, расположение leaders и перегрузка сетевых путей постоянно меняют то, какой маршрут быстрее всего достигает leader. Это не особенность какого-то одного RPC provider или sending service, а структурная характеристика модели исполнения Solana.
SWQoS endpoint от ERPC предоставляет путь отправки, который вводит transactions в priority lane, выделяемую leaders на основе Stake-weighted Quality of Service (SWQoS). Эта priority bandwidth примерно в 5 раз больше, чем у non-priority lane, и применяется еще до оценки priority fee.
Поэтому SWQoS endpoint - важная опция для отправки transactions, но в production один endpoint не всегда оказывается самым быстрым. Даже в пределах одного slot временные различия в path или перекос нагрузки могут привести к тому, что другой быстрый endpoint окажется впереди.
С учетом этого эффективным operational pattern может быть параллельная отправка одной и той же transaction по нескольким быстрым путям с принятием первого обработанного результата, а не зависимость от единственного маршрута.
Однако если отправить одну и ту же transaction на несколько endpoints, без дополнительного контроля нельзя гарантировать, что она исполнится только один раз. Fan-out без такого контроля может привести к нежелательному двойному исполнению или сломанной retry logic.
Для этого в Solana предусмотрен механизм Durable Nonce. Использование Durable Nonce позволяет отправлять одну и ту же подписанную transaction по нескольким маршрутам, при этом ограничивая on-chain execution одним исполнением.
На этой странице объясняется, как реализовать fan-out operations, которые сочетают SWQoS endpoint ERPC с другими быстрыми RPC endpoints, исходя из предположения, что отправка transaction выполняется с Durable Nonce.

Scope и prerequisites

Это руководство охватывает создание Durable Nonce account через web3.js и его использование для отправки transactions и fan-out operations.
Что нужно понимать заранее:
  • Для Durable Nonce transactions используйте nonce value в качестве recentBlockhash и ставьте nonceAdvance первой instruction
  • После выполнения nonceAdvance nonce может быть израсходован, даже если последующие instructions завершатся ошибкой. Повторно отправить тот же rawTx как есть уже нельзя.
  • Создание nonce account - это одноразовая настройка; обычно account затем переиспользуется

Шаг 1: Подготовьте nonce authority и connection

Nonce authority - это Keypair, который может продвигать 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 может выполнять nonceAdvance
  • Это может быть fee payer или отдельный keypair

Шаг 2: Сгенерируйте Keypair для nonce account

Nonce account - это System Account.
typescript
const nonceAccount = Keypair.generate()
Этот Keypair используется для:
  • хранения nonce value (замена recentBlockhash)
  • подписи только в момент создания
  • безопасного хранения после этого; в ежедневных отправках он уже не нужен

Шаг 3: Рассчитайте rent-exempt minimum

Nonce accounts должны быть rent-exempt. Не хардкодьте значения - получайте их из RPC.
typescript
const lamports =
  await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH,
  )

Шаг 4: Создайте и инициализируйте nonce account

Создайте nonce account через createAccount + nonceInitialize в одной transaction.
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',
)
Используйте этот nonceAccount.publicKey для последующих отправок.

Шаг 5: Получайте nonce перед каждой отправкой

Для каждой transaction получайте текущее nonce value.
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
  • Используйте nonce как recentBlockhash
  • context.slot используется при confirmation

Шаг 6: Постройте transaction с Durable Nonce

Transactions с Durable Nonce должны удовлетворять следующим условиям:
  • recentBlockhash = nonce
  • Первая instruction - это 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()
Примечания:
  • Если вы используете instructions ComputeBudget, они должны идти после nonceAdvance
  • Если nonceAdvance не стоит первым, transaction будет отклонена

Шаг 7: Выполните fan-out на несколько RPC

Отправьте один и тот же rawTx конкурентно.
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 будет одинаковой на всех endpoints
  • Успешная отправка не означает confirmation

Шаг 8: Выполните confirmation с Durable Nonce

При использовании Durable Nonce включайте информацию о nonce в confirmation.
typescript
await connection.confirmTransaction(
  {
    signature,
    nonceAccountPubkey: nonceAccount.publicKey,
    nonceValue: nonce,
    minContextSlot,
  },
  'confirmed',
)
Если confirmation успешно:
  • nonce продвигается
  • Копии, отправленные в другие RPC, вернут InvalidNonce

Следующие отправки

  • После confirmation получайте свежий nonce
  • Не переиспользуйте тот же rawTx или то же nonce value
  • Для параллельных workflows используйте отдельные nonce accounts
Для следующей transaction получите обновленный nonce и снова соберите transaction по тем же шагам.