모든 솔라나 트랜잭션에는 최근 블록해시가 포함됩니다. 이는 최근 네트워크 상태에 대한 참조로, 트랜잭션이 "지금" 생성되었음을 증명합니다. 네트워크는 약 150블록(약 60-90초)보다 오래된 블록해시를 가진 트랜잭션을 거부하여 재생 공격과 오래된 제출을 방지합니다. 이는 실시간 결제에는 완벽하게 작동합니다. 하지만 다음과 같이 서명과 제출 사이에 시간 간격이 필요한 워크플로우에서는 문제가 발생합니다:
| 시나리오 | 표준 트랜잭션이 실패하는 이유 |
|---|---|
| 재무 운영 | 도쿄의 CFO가 서명하고 뉴욕의 컨트롤러가 승인—90초로는 충분하지 않음 |
| 컴플라이언스 워크플로우 | 트랜잭션 실행 전 법률/컴플라이언스 검토 필요 |
| 콜드 스토리지 서명 | 에어갭 머신은 서명된 트랜잭션의 수동 전송 필요 |
| 일괄 준비 | 업무 시간에 급여 또는 지급을 준비하고 야간에 실행 |
| 멀티시그 조정 | 여러 시간대에 걸친 다수의 승인자 |
| 예약 결제 | 미래 날짜에 실행될 결제 예약 |
전통적인 금융에서 서명된 수표는 90초 후에 만료되지 않습니다. 특정 블록체인 작업도 마찬가지여야 합니다. 영구 논스는 최근 블록해시를 저장된 영구 값으로 대체하여 이 문제를 해결합니다. 이 값은 사용할 때만 진행되므로 제출할 준비가 될 때까지 유효한 트랜잭션을 제공합니다.
작동 원리
최근 블록해시(약 150개 블록 동안 유효) 대신, 블록해시 대신 사용할 수 있는 고유한 값을 저장하는 특수 계정인 논스 계정을 사용합니다. 이 논스를 사용하는 각 트랜잭션은 첫 번째 명령어로 논스를 "진행"해야 합니다. 각 논스 값은 하나의 트랜잭션에만 사용할 수 있습니다.
논스 계정은 rent 면제를 위해 약 0.0015 SOL이 필요합니다. 하나의 논스 계정 = 한 번에 하나의 대기 중인 트랜잭션입니다. 병렬 워크플로우의 경우 여러 논스 계정을 생성하세요.
논스 계정 생성
논스 계정 생성은 단일 트랜잭션에서 두 개의 명령어가 필요합니다:
- System Program의
getCreateAccountInstruction를 사용하여 계정 생성 getInitializeNonceAccountInstruction를 사용하여 논스로 초기화
키페어 생성
논스 계정 주소로 사용할 새 keypair를 생성하고 필요한 공간과 rent를 계산합니다.
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
계정 생성 명령어
rent 면제를 위한 충분한 lamports로 System Program이 소유한 계정을 생성합니다.
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 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 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));
서명 및 전송
논스 계정을 생성하고 초기화하기 위해 트랜잭션에 서명하고 전송합니다.
지연 트랜잭션 구축
최근 블록해시 대신 논스 계정의 blockhash를 트랜잭션의 수명으로 사용합니다.
논스 가져오기
논스 계정에서 데이터를 가져옵니다. 논스 계정의 blockhash를 트랜잭션의 수명으로
사용합니다.
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
전송 명령어 생성
결제를 위한 명령어를 생성합니다. 이 예제는 토큰 전송을 보여줍니다.
지속 가능한 논스로 트랜잭션 구축
setTransactionMessageLifetimeUsingDurableNonce를 사용하여 논스를 블록해시로
설정하고 논스 진행 명령어를 자동으로 앞에 추가합니다.
트랜잭션 서명
트랜잭션에 서명합니다. 이제 표준 블록해시 대신 지속 가능한 논스를 사용합니다.
트랜잭션 저장 또는 전송
서명 후 저장을 위해 트랜잭션을 인코딩하세요. 준비가 되면 네트워크로 전송합니다.
저장을 위한 인코딩
서명된 트랜잭션을 base64로 인코딩하세요. 이 값을 데이터베이스에 저장합니다.
트랜잭션 전송
준비가 되면 서명된 트랜잭션을 전송하세요. 트랜잭션은 논스가 진행될 때까지 유효합니다.
데모
// Generate keypairs for sender and recipientconst 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 accountsconst { 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 nonceconst createNonceAccountIx = getCreateAccountInstruction({payer: sender,newAccount: nonceKeypair,lamports: nonceRent,space: nonceSpace,programAddress: SYSTEM_PROGRAM_ADDRESS});// Instruction to initialize the nonce accountconst initNonceIx = getInitializeNonceAccountInstruction({nonceAccount: nonceKeypair.address,nonceAuthority: sender.address});// Build and send nonce account creation transactionconst { 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 accountconst { 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 instructionconst 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 timeconst base64EncodedTransaction =getBase64EncodedWireTransaction(signedTransaction);console.log("\nBase64 Encoded Transaction:", base64EncodedTransaction);// Send the encoded transaction, blockhash does not expireawait 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// =============================================================================
대기 중인 트랜잭션 무효화
각 논스 계정 blockhash은 한 번만 사용할 수 있습니다. 대기 중인 트랜잭션을
무효화하거나 논스 계정을 재사용하기 위해 준비하려면 수동으로 진행하세요:
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
이렇게 하면 새로운 논스 값이 생성되어 이전 값으로 서명된 모든 트랜잭션이 영구적으로 무효화됩니다.
다자간 승인 워크플로우
트랜잭션을 역직렬화하여 추가 서명을 추가한 다음 저장 또는 제출을 위해 다시 직렬화하세요:
import {getBase64Decoder,getTransactionDecoder,getBase64EncodedWireTransaction,partiallySignTransaction} from "@solana/kit";// Deserialize the stored transactionconst txBytes = getBase64Decoder().decode(serializedString);const partiallySignedTx = getTransactionDecoder().decode(txBytes);// Each approver adds their signatureconst fullySignedTx = await partiallySignTransaction([newSigner],partiallySignedTx);// Serialize again for storage or submissionconst serialized = getBase64EncodedWireTransaction(fullySignedTx);
트랜잭션은 직렬화, 저장 및 승인자 간에 전달될 수 있습니다. 필요한 모든 서명이 수집되면 네트워크에 제출하세요.
프로덕션 고려사항
논스 계정 관리:
- 병렬 트랜잭션 준비를 위한 논스 계정 풀 생성
- 어떤 논스가 "사용 중"인지 추적(대기 중인 서명된 트랜잭션 보유)
- 트랜잭션이 제출되거나 중단된 후 논스 재활용 구현
보안:
- 논스 권한은 트랜잭션의 무효화 여부를 제어합니다. 추가적인 제어와 직무 분리를 위해 논스 권한을 트랜잭션 서명자와 분리하는 것을 고려하세요
- 직렬화된 트랜잭션 바이트를 가진 누구나 네트워크에 제출할 수 있습니다
관련 리소스
Is this page helpful?