---
title: "SWQoS를 위한 Durable Nonce 팬아웃"
description: "web3.js를 사용한 Durable Nonce 계정 설정과 SWQoS 팬아웃 전송"
canonical: "https://erpc.global/ko/doc/rpc/durable-nonce-send-transaction/"
locale: "ko"
---

# SWQoS를 위한 Durable Nonce 팬아웃

## 배경 및 사용 시나리오

Solana의 트랜잭션 전송에서는 slot 진행, leader 배치, 네트워크 경로 혼잡에 따라 leader에 가장 빨리 도달하는 경로가 끊임없이 바뀝니다. 이는 특정 RPC 제공자나 전송 서비스에 국한된 현상이 아니라 Solana 실행 모델의 구조적 특성입니다.

ERPC의 SWQoS 엔드포인트는 leader가 Stake-weighted Quality of Service(SWQoS)에 따라 할당하는 우선 레인에 트랜잭션을 주입하는 전송 경로를 제공합니다. 이 우선 대역폭은 비우선 레인의 약 5배이며, priority fee 평가보다 먼저 적용됩니다.

따라서 SWQoS 엔드포인트는 트랜잭션 전송에서 중요한 선택지이지만, 운영 환경에서 단일 엔드포인트가 항상 가장 빠른 것은 아닙니다. 같은 slot 안에서도 일시적인 경로 차이나 부하 편중으로 다른 빠른 엔드포인트가 앞설 수 있습니다.

이러한 특성을 고려하면, 단일 경로에 의존하기보다 같은 트랜잭션을 여러 빠른 경로로 병렬 전송하고 가장 먼저 처리된 것을 채택하는 운영 패턴이 효과적일 수 있습니다.

하지만 같은 트랜잭션을 여러 엔드포인트로 보내면, 추가적인 제어 없이는 한 번만 실행된다고 보장할 수 없습니다. 이러한 제어가 없는 팬아웃은 의도하지 않은 이중 실행이나 재시도 제어의 붕괴로 이어질 수 있습니다.

Solana는 이를 위한 메커니즘으로 Durable Nonce를 제공합니다. Durable Nonce를 사용하면 같은 서명된 트랜잭션을 여러 경로로 보내면서도 온체인 실행은 한 번으로 제한할 수 있습니다.

이 페이지에서는 Durable Nonce를 사용한 트랜잭션 전송을 전제로, ERPC의 SWQoS 엔드포인트와 다른 빠른 RPC 엔드포인트를 조합한 팬아웃 운영을 구현하는 방법을 설명합니다.

---

## 범위 및 사전 요구 사항

이 가이드는 web3.js로 Durable Nonce 계정을 만들고, 이를 트랜잭션 전송과 팬아웃 운영에 사용하는 방법을 다룹니다.

먼저 이해해야 할 전제:

- Durable Nonce 트랜잭션에서는 nonce 값을 `recentBlockhash`로 사용하고 `nonceAdvance`를 첫 번째 instruction으로 배치합니다
- `nonceAdvance`가 실행되면 이후 instruction이 실패하더라도 nonce가 소비될 수 있습니다. 같은 rawTx를 그대로 다시 보낼 수는 없습니다.
- Nonce 계정 생성은 한 번만 하는 설정이며 일반적으로 재사용합니다

---

## 1단계: nonce authority와 connection 준비

nonce authority는 nonce를 진행(advance)시킬 수 있는 Keypair입니다.

```ts
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여도 되고, 별도의 keypair여도 됩니다

---

## 2단계: nonce 계정 Keypair 생성

nonce 계정은 **System Account**입니다.

```ts
const nonceAccount = Keypair.generate()
```

이 Keypair는 다음 용도로 사용됩니다:

- nonce 값 보관 (`recentBlockhash` 대체)
- 생성 시에만 서명
- 이후에는 안전하게 보관하며, 일상적인 전송에는 필요하지 않습니다

---

## 3단계: rent-exempt 최소 금액 계산

Nonce 계정은 rent-exempt여야 합니다. 값을 하드코딩하지 말고 RPC에서 가져오세요.

```ts
const lamports =
  await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH)
```

---

## 4단계: nonce 계정 생성 및 초기화

**createAccount + nonceInitialize**로 단일 트랜잭션에서 nonce 계정을 생성합니다.

```ts
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 가져오기

트랜잭션마다 현재 nonce 값을 가져옵니다.

```ts
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단계: Durable Nonce 트랜잭션 구성

Durable Nonce 트랜잭션은 다음 조건을 충족해야 합니다:

- `recentBlockhash = nonce`
- 첫 번째 instruction은 `nonceAdvance`

```ts
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()
```

참고:

- ComputeBudget instruction을 사용한다면 반드시 `nonceAdvance` **다음에** 와야 합니다
- `nonceAdvance`가 첫 번째가 아니면 트랜잭션이 거부됩니다

---

## 7단계: 여러 RPC로 팬아웃

같은 rawTx를 동시에 보냅니다.

```ts
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.value
```

- 서명은 모든 엔드포인트에서 동일합니다
- 전송에 성공했다고 해서 확인(confirmation)된 것은 **아닙니다**

---

## 8단계: Durable Nonce로 확인

Durable Nonce에서는 확인할 때 nonce 정보를 포함합니다.

```ts
await connection.confirmTransaction(
  {
    signature,
    nonceAccountPubkey: nonceAccount.publicKey,
    nonceValue: nonce,
    minContextSlot,
  },
  'confirmed',
)
```

확인에 성공하면:

- nonce가 진행(advance)됩니다
- 다른 RPC로 보낸 사본은 `InvalidNonce`를 반환합니다

---

## 다음 전송

- 확인 후 새 nonce를 가져옵니다
- 같은 rawTx나 nonce 값을 재사용하지 마세요
- 병렬 워크플로에는 별도의 nonce 계정을 사용하세요

다음 트랜잭션에서는 업데이트된 nonce를 가져와 같은 단계로 트랜잭션을 구성합니다.
