결제고급 결제

지연 실행

모든 솔라나 트랜잭션에는 최근 블록해시가 포함됩니다. 이는 최근 네트워크 상태에 대한 참조로, 트랜잭션이 "지금" 생성되었음을 증명합니다. 네트워크는 약 150블록(약 60-90초)보다 오래된 블록해시를 가진 트랜잭션을 거부하여 재생 공격과 오래된 제출을 방지합니다. 이는 실시간 결제에는 완벽하게 작동합니다. 하지만 다음과 같이 서명과 제출 사이에 시간 간격이 필요한 워크플로우에서는 문제가 발생합니다:

시나리오표준 트랜잭션이 실패하는 이유
재무 운영도쿄의 CFO가 서명하고 뉴욕의 컨트롤러가 승인—90초로는 충분하지 않음
컴플라이언스 워크플로우트랜잭션 실행 전 법률/컴플라이언스 검토 필요
콜드 스토리지 서명에어갭 머신은 서명된 트랜잭션의 수동 전송 필요
일괄 준비업무 시간에 급여 또는 지급을 준비하고 야간에 실행
멀티시그 조정여러 시간대에 걸친 다수의 승인자
예약 결제미래 날짜에 실행될 결제 예약

전통적인 금융에서 서명된 수표는 90초 후에 만료되지 않습니다. 특정 블록체인 작업도 마찬가지여야 합니다. 영구 논스는 최근 블록해시를 저장된 영구 값으로 대체하여 이 문제를 해결합니다. 이 값은 사용할 때만 진행되므로 제출할 준비가 될 때까지 유효한 트랜잭션을 제공합니다.

작동 원리

최근 블록해시(약 150개 블록 동안 유효) 대신, 블록해시 대신 사용할 수 있는 고유한 값을 저장하는 특수 계정인 논스 계정을 사용합니다. 이 논스를 사용하는 각 트랜잭션은 첫 번째 명령어로 논스를 "진행"해야 합니다. 각 논스 값은 하나의 트랜잭션에만 사용할 수 있습니다.

Durable Nonce
Standard Blockhash

논스 계정은 rent 면제를 위해 약 0.0015 SOL이 필요합니다. 하나의 논스 계정 = 한 번에 하나의 대기 중인 트랜잭션입니다. 병렬 워크플로우의 경우 여러 논스 계정을 생성하세요.

논스 계정 생성

논스 계정 생성은 단일 트랜잭션에서 두 개의 명령어가 필요합니다:

  1. System Program의 getCreateAccountInstruction를 사용하여 계정 생성
  2. getInitializeNonceAccountInstruction를 사용하여 논스로 초기화

키페어 생성

논스 계정 주소로 사용할 새 keypair를 생성하고 필요한 공간과 rent를 계산합니다.

Create Nonce Account
const nonceKeypair = await generateKeyPairSigner();
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();

계정 생성 명령어

rent 면제를 위한 충분한 lamports로 System Program이 소유한 계정을 생성합니다.

Create Nonce Account
const nonceKeypair = await generateKeyPairSigner();
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();
const createNonceAccountIx = getCreateAccountInstruction({
payer: sender,
newAccount: nonceKeypair,
lamports: nonceRent,
space: nonceSpace,
programAddress: SYSTEM_PROGRAM_ADDRESS
});

논스 초기화 명령어

계정을 논스 계정으로 초기화하고, 이를 진행할 수 있는 권한을 설정합니다.

Create Nonce Account
const nonceKeypair = await generateKeyPairSigner();
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();
const createNonceAccountIx = getCreateAccountInstruction({
payer: sender,
newAccount: nonceKeypair,
lamports: nonceRent,
space: nonceSpace,
programAddress: SYSTEM_PROGRAM_ADDRESS
});
const initNonceIx = getInitializeNonceAccountInstruction({
nonceAccount: nonceKeypair.address,
nonceAuthority: sender.address
});

트랜잭션 구성

두 명령어를 포함한 트랜잭션을 구성합니다.

Create Nonce Account
const nonceKeypair = await generateKeyPairSigner();
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();
const createNonceAccountIx = getCreateAccountInstruction({
payer: sender,
newAccount: nonceKeypair,
lamports: nonceRent,
space: nonceSpace,
programAddress: SYSTEM_PROGRAM_ADDRESS
});
const initNonceIx = getInitializeNonceAccountInstruction({
nonceAccount: nonceKeypair.address,
nonceAuthority: sender.address
});
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const createNonceTx = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),
(tx) =>
appendTransactionMessageInstructions(
[createNonceAccountIx, initNonceIx],
tx
)
);

서명 및 전송

논스 계정을 생성하고 초기화하기 위해 트랜잭션에 서명하고 전송합니다.

키페어 생성

논스 계정 주소로 사용할 새 keypair를 생성하고 필요한 공간과 rent를 계산합니다.

계정 생성 명령어

rent 면제를 위한 충분한 lamports로 System Program이 소유한 계정을 생성합니다.

논스 초기화 명령어

계정을 논스 계정으로 초기화하고, 이를 진행할 수 있는 권한을 설정합니다.

트랜잭션 구성

두 명령어를 포함한 트랜잭션을 구성합니다.

서명 및 전송

논스 계정을 생성하고 초기화하기 위해 트랜잭션에 서명하고 전송합니다.

Create Nonce Account
const nonceKeypair = await generateKeyPairSigner();
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();

지연 트랜잭션 구축

최근 블록해시 대신 논스 계정의 blockhash를 트랜잭션의 수명으로 사용합니다.

논스 가져오기

논스 계정에서 데이터를 가져옵니다. 논스 계정의 blockhash를 트랜잭션의 수명으로 사용합니다.

Example Nonce Account Data
{
version: 1,
state: 1,
authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',
blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',
lamportsPerSignature: 5000n
}

전송 명령어 생성

결제를 위한 명령어를 생성합니다. 이 예제는 토큰 전송을 보여줍니다.

지속 가능한 논스로 트랜잭션 구축

setTransactionMessageLifetimeUsingDurableNonce를 사용하여 논스를 블록해시로 설정하고 논스 진행 명령어를 자동으로 앞에 추가합니다.

트랜잭션 서명

트랜잭션에 서명합니다. 이제 표준 블록해시 대신 지속 가능한 논스를 사용합니다.

논스 가져오기

논스 계정에서 데이터를 가져옵니다. 논스 계정의 blockhash를 트랜잭션의 수명으로 사용합니다.

Example Nonce Account Data
{
version: 1,
state: 1,
authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',
blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',
lamportsPerSignature: 5000n
}

전송 명령어 생성

결제를 위한 명령어를 생성합니다. 이 예제는 토큰 전송을 보여줍니다.

지속 가능한 논스로 트랜잭션 구축

setTransactionMessageLifetimeUsingDurableNonce를 사용하여 논스를 블록해시로 설정하고 논스 진행 명령어를 자동으로 앞에 추가합니다.

트랜잭션 서명

트랜잭션에 서명합니다. 이제 표준 블록해시 대신 지속 가능한 논스를 사용합니다.

Build Deferred Transaction
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);

트랜잭션 저장 또는 전송

서명 후 저장을 위해 트랜잭션을 인코딩하세요. 준비가 되면 네트워크로 전송합니다.

저장을 위한 인코딩

서명된 트랜잭션을 base64로 인코딩하세요. 이 값을 데이터베이스에 저장합니다.

트랜잭션 전송

준비가 되면 서명된 트랜잭션을 전송하세요. 트랜잭션은 논스가 진행될 때까지 유효합니다.

저장을 위한 인코딩

서명된 트랜잭션을 base64로 인코딩하세요. 이 값을 데이터베이스에 저장합니다.

트랜잭션 전송

준비가 되면 서명된 트랜잭션을 전송하세요. 트랜잭션은 논스가 진행될 때까지 유효합니다.

Store and Execute
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const base64EncodedTransaction =
getBase64EncodedWireTransaction(signedTransaction);
// Store base64EncodedTransaction in your database

데모

Demo
// Generate keypairs for sender and recipient
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
console.log("Sender Address:", sender.address);
console.log("Recipient Address:", recipient.address);
// Demo Setup: Create RPC connection, mint, and token accounts
const { rpc, rpcSubscriptions, mint } = await demoSetup(sender, recipient);
// =============================================================================
// Step 1: Create a Nonce Account
// =============================================================================
const nonceKeypair = await generateKeyPairSigner();
console.log("\nNonce Account Address:", nonceKeypair.address);
const nonceSpace = BigInt(getNonceSize());
const nonceRent = await rpc
.getMinimumBalanceForRentExemption(nonceSpace)
.send();
// Instruction to create new account for the nonce
const createNonceAccountIx = getCreateAccountInstruction({
payer: sender,
newAccount: nonceKeypair,
lamports: nonceRent,
space: nonceSpace,
programAddress: SYSTEM_PROGRAM_ADDRESS
});
// Instruction to initialize the nonce account
const initNonceIx = getInitializeNonceAccountInstruction({
nonceAccount: nonceKeypair.address,
nonceAuthority: sender.address
});
// Build and send nonce account creation transaction
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const createNonceTx = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),
(tx) =>
appendTransactionMessageInstructions(
[createNonceAccountIx, initNonceIx],
tx
)
);
const signedCreateNonceTx =
await signTransactionMessageWithSigners(createNonceTx);
assertIsTransactionWithBlockhashLifetime(signedCreateNonceTx);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedCreateNonceTx,
{ commitment: "confirmed" }
);
console.log("Nonce Account created.");
// =============================================================================
// Step 2: Token Payment with Durable Nonce
// =============================================================================
// Fetch current nonce value from the nonce account
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);
console.log("Nonce Account data:", nonceData);
const [senderAta] = await findAssociatedTokenPda({
mint: mint.address,
owner: sender.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [recipientAta] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipient.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
console.log("\nMint Address:", mint.address);
console.log("Sender Token Account:", senderAta);
console.log("Recipient Token Account:", recipientAta);
const transferInstruction = getTransferInstruction({
source: senderAta,
destination: recipientAta,
authority: sender.address,
amount: 250_000n // 0.25 tokens
});
// Create transaction message using durable nonce lifetime
// setTransactionMessageLifetimeUsingDurableNonce automatically prepends
// the AdvanceNonceAccount instruction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) =>
setTransactionMessageLifetimeUsingDurableNonce(
{
nonce: nonceData.blockhash as string as Nonce,
nonceAccountAddress: nonceKeypair.address,
nonceAuthorityAddress: nonceData.authority
},
tx
),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithDurableNonceLifetime(signedTransaction);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Encode the transaction to base64, optionally save and send at a later time
const base64EncodedTransaction =
getBase64EncodedWireTransaction(signedTransaction);
console.log("\nBase64 Encoded Transaction:", base64EncodedTransaction);
// Send the encoded transaction, blockhash does not expire
await rpc
.sendTransaction(base64EncodedTransaction, {
encoding: "base64",
skipPreflight: true
})
.send();
console.log("\n=== Token Payment with Durable Nonce Complete ===");
console.log("Transaction Signature:", transactionSignature);
// =============================================================================
// Demo Setup Helper Function
// =============================================================================
Console
Click to execute the code.

대기 중인 트랜잭션 무효화

각 논스 계정 blockhash은 한 번만 사용할 수 있습니다. 대기 중인 트랜잭션을 무효화하거나 논스 계정을 재사용하기 위해 준비하려면 수동으로 진행하세요:

import { getAdvanceNonceAccountInstruction } from "@solana-program/system";
// Submit this instruction (with a regular blockhash) to invalidate any pending transaction
getAdvanceNonceAccountInstruction({
nonceAccount: nonceAddress,
nonceAuthority
});

이렇게 하면 새로운 논스 값이 생성되어 이전 값으로 서명된 모든 트랜잭션이 영구적으로 무효화됩니다.

다자간 승인 워크플로우

트랜잭션을 역직렬화하여 추가 서명을 추가한 다음 저장 또는 제출을 위해 다시 직렬화하세요:

import {
getBase64Decoder,
getTransactionDecoder,
getBase64EncodedWireTransaction,
partiallySignTransaction
} from "@solana/kit";
// Deserialize the stored transaction
const txBytes = getBase64Decoder().decode(serializedString);
const partiallySignedTx = getTransactionDecoder().decode(txBytes);
// Each approver adds their signature
const fullySignedTx = await partiallySignTransaction(
[newSigner],
partiallySignedTx
);
// Serialize again for storage or submission
const serialized = getBase64EncodedWireTransaction(fullySignedTx);

트랜잭션은 직렬화, 저장 및 승인자 간에 전달될 수 있습니다. 필요한 모든 서명이 수집되면 네트워크에 제출하세요.

프로덕션 고려사항

논스 계정 관리:

  • 병렬 트랜잭션 준비를 위한 논스 계정 풀 생성
  • 어떤 논스가 "사용 중"인지 추적(대기 중인 서명된 트랜잭션 보유)
  • 트랜잭션이 제출되거나 중단된 후 논스 재활용 구현

보안:

  • 논스 권한은 트랜잭션의 무효화 여부를 제어합니다. 추가적인 제어와 직무 분리를 위해 논스 권한을 트랜잭션 서명자와 분리하는 것을 고려하세요
  • 직렬화된 트랜잭션 바이트를 가진 누구나 네트워크에 제출할 수 있습니다

관련 리소스

Is this page helpful?

목차

페이지 편집

관리자

© 2026 솔라나 재단.
모든 권리 보유.
연결하기