
Solana 트랜잭션: 영구 논스(2023)
소개
개발자가 Solana에서 트랜잭션을 제출하려면 Solana RPC를 통해 최신 블록해시를 가져와야 합니다. 이 핵심 단계는 재전송 공격을 방지합니다. 특정 블록해시로 트랜잭션을 서명하고 제출한 후에는 누구도 동일한 해시로 해당 트랜잭션을 재실행하거나 다시 제출할 수 없습니다.
Ledger 같은 오프라인 콜드 스토리지나 하드웨어 지갑의 서명이 필요한 트랜잭션을 제출해야 한다고 가정해 보겠습니다. 하지만 블록해시는 빠르게 만료되어 트랜잭션이 무효화될 수 있습니다. 이때 영구 논스를 사용하면 안전한 오프라인 트랜잭션이 가능합니다.
이 가이드를 마치면 다음 내용을 이해할 수 있습니다.
- 영구 논스란 무엇인가요?
- 영구 논스의 목적은 무엇인가요?
- 트랜잭션에서 영구 논스를 어떻게 사용하나요?
트랜잭션: 사전 요구 사항
시작하기 전에 다음 항목을 준비하세요.
- 기본적인 JavaScript 지식
- NodeJS 설치
- Solana CLI 설치
- Git 설치
환경 설정
- 기존 유틸리티가 포함된 예제 저장소를 복제합니다.
git clone- 프로젝트 폴더로 이동한 후 npm을 설치합니다.
cd durable-noncenpm install- nonce 폴더 안의 wallets 폴더로 이동합니다. 테스트용 로컬 키가 이곳에 저장됩니다.
cd wallets- 설치된 Solana CLI로 결제 키페어용 지갑을 하나 생성합니다.
solana-keygen new -o ./wallet.json- 이제 Solana를 에어드롭할 수 있도록 이 지갑을 CLI의 기본 지갑으로 설정합니다.
solana config set --keypair ./wallet.json- 다음 명령어를 실행하여 이 주소로 Solana를 에어드롭할 수 있습니다.
solana aidrop 2- 논스 권한용 지갑도 생성하고 자금을 충전해야 합니다.
solana-keygen new -o ./nonceAuth.json- 생성된 공개 키에는 여기의 Faucet 사이트를 사용하여 1 SOL을 에어드롭할 수 있습니다.
환경 설정을 마쳤으므로 다음 단계로 넘어가겠습니다.
영구 논스란 무엇인가요?
Solana의 영구 논스 계정은 안전 금고와 같습니다. 이 계정을 생성하면 Solana는 "영구 논스"라는 고유하고 안정적인 코드를 할당합니다. 트랜잭션마다 변경되는 일반적인 논스와 달리 이 값은 유지되며 일관된 참조값으로 사용됩니다.
이는 특히 "오프라인" 트랜잭션에 유용합니다. 트랜잭션을 구성할 때 계정의 이 논스를 참조합니다. Solana는 이를 저장된 값과 대조해 검증하며, 값이 일치하면 트랜잭션을 승인합니다. 따라서 영구 논스 계정은 저장 및 검증 메커니즘으로 작동합니다. Solana 네트워크의 빠른 속도와 오프라인 시나리오를 지원하면서 트랜잭션의 진위를 보장합니다.
영구 논스는 다음과 같은 다양한 사용 사례에 활용할 수 있습니다.
- 예약 트랜잭션: 미래의 특정 시점에 트랜잭션이 실행되도록 설정할 수 있습니다. 영구 논스는 예약된 트랜잭션이 안전하게 실행되도록 보장합니다.
- 다중 서명 지갑: 다중 서명 지갑에서 영구 논스는 여러 서명자 사이의 보안과 조정을 한층 강화합니다.
- 향후 상호작용이 필요한 프로그램: Solana의 일부 프로그램은 특정 주기마다 다른 프로그램이나 서비스와 상호작용해야 합니다. 영구 논스는 이러한 상호작용의 무결성을 유지하는 데 도움이 됩니다.
- 다른 블록체인과의 상호작용: Solana가 다른 블록체인과 상호작용할 때 영구 논스는 크로스체인 트랜잭션의 유효성을 보장하는 역할을 합니다.
이제 예제 구현을 시작하겠습니다.
Solana 트랜잭션: 구현 단계
1단계: 종속성과 상수 설정
이 단계에서는 필요한 모듈과 유틸리티를 가져오고 예제에 사용할 상수와 키페어를 정의합니다. 이러한 종속성과 상수는 트랜잭션 프로세스 전반에서 사용됩니다.
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { encodeAndWriteTransaction, loadWallet, readAndDecodeTransaction } from "./utils";
const nonceAuthKeypair = loadWallet('./wallets/nonceAuth.json');
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet('./wallets/wallet.json');
const connection = new Connection("https://devnet.helius-rpc.com/?api-key=");
const waitTime = 120000;
const TranferAmount = LAMPORTS_PER_SOL * 0.01;2단계: sendTransaction 함수 생성
sendTransaction 함수는 영구 논스를 사용해 트랜잭션을 전송하는 프로세스를 조율합니다. 이 함수는 논스 생성, 확인, 트랜잭션 실행을 처리합니다.
async function sendTransaction() {
console.log("Starting Nonce Transaction")
try {
const nonceCreationTxSig = await nonce();
const confirmationStatus = await connection.confirmTransaction(nonceCreationTxSig);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error("Nonce account creation transaction failed:", confirmationStatus.value.err);
}
} catch (error) {
console.error(error);
}
}3단계: nonce 함수 생성
nonce 함수는 영구 논스 계정을 생성하고 초기화합니다. 이 과정에는 계정에 필요한 임대료 계산, 최신 블록해시 가져오기, 논스 계정을 생성하고 초기화하는 트랜잭션 구성이 포함됩니다.
- 논스 계정을 생성하기 전에 계정 데이터 저장에 필요한 임대료를 계산하고 최신 블록해시를 가져와야 합니다.
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- 이제 논스 계정을 생성하는 트랜잭션을 구성합니다.
SystemProgram.createAccount명령어를 사용해 논스 계정에 공간을 할당합니다.
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);- 권한 키페어로 트랜잭션에 서명하고 Solana 네트워크로 전송합니다. 이 트랜잭션이 영구 논스 계정을 생성합니다.
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
const signature = await connection.sendRawTransaction(createNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- 트랜잭션을 전송한 후 상태를 확인하여 논스 계정이 성공적으로 생성되었는지 검증합니다.
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error("Nonce account creation transaction failed: " + confirmationStatus.value.err);
}
console.log("Nonce account created:", signature);- 논스 계정을 완전히 활용하려면 값을 초기화해야 합니다.
SystemProgram.nonceInitialize명령어를 실행하는 새 트랜잭션을 생성합니다.
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- 이전 단계와 마찬가지로 트랜잭션에 서명하고 네트워크로 전송하여 논스 계정을 초기화합니다.
const { blockhash: initBlockhash, lastValidBlockHeight: initLastValidBlockHeight } = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair);
const initSignature = await connection.sendRawTransaction(initializeNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- 마지막으로 초기화 트랜잭션의 상태를 확인하여 논스 계정이 올바르게 초기화되었는지 검증합니다.
const initConfirmationStatus = await connection.confirmTransaction(initSignature);
if (initConfirmationStatus.value.err) {
throw new Error("Nonce initialization transaction failed: " + initConfirmationStatus.value.err);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}전체 함수는 다음과 비슷합니다.
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}4단계: getNonce 함수 생성
생성된 논스 계정에서 논스 값을 가져오는 getNonce 함수를 정의합니다.
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}5단계: createTx 함수 생성
논스 진행 명령어와 전송 명령어를 모두 포함하는 샘플 트랜잭션을 생성하는 createTx 함수를 정의합니다. 앞서 가져온 논스를 사용하여 트랜잭션의 진위를 보장합니다.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}6단계: signOffline 함수 생성
트랜잭션을 오프라인에서 서명하는 signOffline 함수를 정의합니다. 오프라인 지연을 시뮬레이션한 후 송신자와 논스 권한 키페어로 트랜잭션에 서명합니다.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}7단계: executeTx 함수 생성
executeTx 함수는 서명된 트랜잭션을 실행할 수 있도록 Solana 네트워크로 전송합니다. 트랜잭션 프로세스의 마지막 단계로, 트랜잭션을 네트워크에 브로드캐스트합니다.
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}8단계: fetchNonceInfo 함수 생성
fetchNonceInfo 함수는 생성된 논스 계정에서 논스 정보를 가져오며, 필요한 경우 최대 세 번까지 재시도합니다. 이를 통해 트랜잭션에 사용되는 논스가 최신 상태이며 유효한지 확인할 수 있습니다.
async function fetchNonceInfo(retries = 3) {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(`Retry fetching nonce in 3 seconds. ${retries} retries left.`);
await new Promise(res => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}9단계: sendTransaction 함수 호출
마지막으로 sendTransaction 함수를 호출해 트랜잭션 프로세스를 시작합니다. 이 함수는 앞서 정의한 모든 단계를 결합하여 영구 논스를 사용하는 트랜잭션을 생성하고 서명한 후 실행합니다.
ts-node mainsendTransaction을 실행하면 성공한 트랜잭션의 서명이 생성됩니다. 이 서명은 Solana 네트워크에서 트랜잭션을 추적하고 검증하는 데 필요한 핵심 정보입니다.
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent: 64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U이제 영구 논스를 사용해 트랜잭션을 성공적으로 처리했습니다!
전체 코드
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
encodeAndWriteTransaction,
loadWallet,
readAndDecodeTransaction,
} from "./utils";
const TranferAmount = LAMPORTS_PER_SOL * 0.01;
const nonceAuthKeypair = loadWallet("./wallets/nonceAuth.json");
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet("./wallets/wallet.json");
const connection = new Connection(
"https://devnet.helius-rpc.com/?api-key="
);
const waitTime = 120000;
async function sendTransaction() {
try {
// Create nonce and get its creation transaction signature
const nonceCreationTxSig = await nonce();
// Ensure nonce account creation is confirmed before moving forward
const confirmationStatus = await connection.confirmTransaction(
nonceCreationTxSig
);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error(
"Nonce account creation transaction failed:",
confirmationStatus.value.err
);
}
} catch (error) {
console.error(error);
}
}
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}
async function createTx(nonce: string) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(
sampleTx,
"./unsigned.json",
false
);
return serialisedTx;
}
async function signOffline(waitTime = 120000): Promise {
await new Promise((resolve) => setTimeout(resolve, waitTime));
const unsignedTx = readAndDecodeTransaction("./unsigned.json");
unsignedTx.sign(senderKeypair, nonceAuthKeypair); // Sign with both keys
const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signed.json");
return serialisedTx;
}
async function executeTx() {
const signedTx = readAndDecodeTransaction("./signed.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log(" Tx sent: ", sig);
}
async function fetchNonceInfo(retries = 3): Promise {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(
`Retry fetching nonce in 3 seconds. ${retries} retries left.`
);
await new Promise((res) => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}
sendTransaction();Solana 트랜잭션: Helius RPC 사용
Helius는 Solana RPC와 상호작용하는 강력한 중개자 역할을 하며, 영구 논스에 필요한 블록해시 정보를 가져오는 과정을 간소화합니다. Helius를 사용하면 특히 오프라인 시나리오에서 Solana 트랜잭션의 수명 주기를 더욱 안정적으로 관리할 수 있습니다. 블록해시에 간편하게 접근할 수 있어 개발자는 트랜잭션 만료에 더 강한 애플리케이션을 구축할 수 있습니다.
요약하면 Solana 트랜잭션의 영구 논스는 오프라인 트랜잭션을 처리하고 트랜잭션의 진위를 보장하는 안전하고 신뢰할 수 있는 방법입니다. 이 가이드의 단계에 따라 개발자는 Solana 애플리케이션에 영구 논스를 구현하여 보안과 유연성을 높일 수 있습니다.
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


