すべてのSolanaトランザクションには、最近のブロックハッシュが含まれています。これは、トランザクションが「今」作成されたことを証明する最近のネットワーク状態への参照です。ネットワークは、約150ブロック(約60〜90秒)より古いブロックハッシュを持つトランザクションを拒否し、リプレイ攻撃や古い送信を防ぎます。これはリアルタイム決済には完璧に機能します。しかし、署名と送信の間にギャップが必要なワークフローでは機能しません。例えば:
| シナリオ | 標準トランザクションが失敗する理由 |
|---|---|
| 財務運用 | 東京のCFOが署名し、ニューヨークの経理担当者が承認—90秒では不十分 |
| コンプライアンスワークフロー | トランザクションは実行前に法務/コンプライアンスレビューが必要 |
| コールドストレージ署名 | エアギャップマシンは署名済みトランザクションの手動転送が必要 |
| バッチ準備 | 営業時間中に給与や支払いを準備し、夜間に実行 |
| マルチシグ調整 | タイムゾーンをまたぐ複数の承認者 |
| スケジュール支払い | 将来の日付に実行される支払いをスケジュール |
従来の金融では、署名された小切手は90秒で期限切れになりません。特定のブロックチェーン操作も同様であるべきです。永続的ノンスは、最近のブロックハッシュを保存された永続的な値に置き換えることでこれを解決します。この値は使用時にのみ進み、送信準備が整うまでトランザクションを有効に保ちます。
仕組み
最近のブロックハッシュ(約150ブロック有効)の代わりに、nonceアカウントを使用します。これは、ブロックハッシュの代わりに使用できる一意の値を保存する特別なアカウントです。このnonceを使用する各トランザクションは、最初のinstructionとしてそれを「進める」必要があります。各nonce値は1つのトランザクションにのみ使用できます。
nonceアカウントには、rent免除のために約0.0015 SOLが必要です。1つのnonceアカウント=一度に1つの保留中トランザクション。並列ワークフローの場合は、複数のnonceアカウントを作成してください。
nonceアカウントの作成
nonceアカウントの作成には、1つのトランザクション内で2つのinstructionsが必要です:
- System Programの
getCreateAccountInstructionを使用してアカウントを作成 getInitializeNonceAccountInstructionを使用してnonceとして初期化
keypairの生成
nonceアカウントアドレスとして使用する新しいkeypairを生成し、必要なスペースとrentを計算します。
const nonceKeypair = await generateKeyPairSigner();const nonceSpace = BigInt(getNonceSize());const nonceRent = await rpc.getMinimumBalanceForRentExemption(nonceSpace).send();
アカウント作成instruction
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});
nonce初期化instruction
アカウントをnonceアカウントとして初期化し、それを進めることができる権限を設定します。
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});
トランザクションの構築
両方のinstructionsを含むトランザクションを構築します。
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));
署名と送信
nonceアカウントを作成して初期化するトランザクションに署名して送信します。
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));const signedCreateNonceTx =await signTransactionMessageWithSigners(createNonceTx);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedCreateNonceTx,{ commitment: "confirmed" });
遅延トランザクションの構築
最近のブロックハッシュの代わりに、nonceアカウントのblockhashをトランザクションの有効期間として使用します。
nonceの取得
nonceアカウントからデータを取得します。nonceアカウントのblockhashをトランザクションの有効期間として使用します。
{version: 1,state: 1,authority: 'HgjaL8artMtmntaQDVM2UBk3gppsYYERS4PkUhiaLZD1',blockhash: '5U7seXqfgZx1uh5DFhdH1vyBhr7XGRrKxBAnJJTbbUa',lamportsPerSignature: 5000n}
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);
転送instructionの作成
支払いのためのinstructionを作成します。この例ではトークン転送を示しています。
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});
durable nonceを使用したトランザクションの構築
setTransactionMessageLifetimeUsingDurableNonceを使用します。これによりnonceがブロックハッシュとして設定され、advance
nonce instructionが自動的に先頭に追加されます。
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});const transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));
トランザクションへの署名
トランザクションに署名します。これで標準のブロックハッシュの代わりにdurable nonceが使用されます。
const { data: nonceData } = await fetchNonce(rpc, nonceKeypair.address);const transferInstruction = getTransferInstruction({source: senderAta,destination: recipientAta,authority: sender.address,amount: 250_000n});const transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) =>setTransactionMessageLifetimeUsingDurableNonce({nonce: nonceData.blockhash as Nonce,nonceAccountAddress: nonceKeypair.address,nonceAuthorityAddress: nonceData.authority},tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);
トランザクションの保存または送信
署名後、トランザクションを保存用にエンコードします。準備ができたら、ネットワークに送信します。
保存用にエンコード
署名済みトランザクションをbase64にエンコードします。この値をデータベースに保存します。
トランザクションの送信
準備ができたら署名済みトランザクションを送信します。トランザクションはnonceが進められるまで有効です。
デモ
// 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// =============================================================================
保留中のトランザクションの無効化
各nonceアカウント blockhash
は一度しか使用できません。保留中のトランザクションを無効化するか、nonceアカウントを再利用のために準備するには、手動で進めます。
import { getAdvanceNonceAccountInstruction } from "@solana-program/system";// Submit this instruction (with a regular blockhash) to invalidate any pending transactiongetAdvanceNonceAccountInstruction({nonceAccount: nonceAddress,nonceAuthority});
これにより新しいnonce値が生成され、古い値で署名されたトランザクションは永久に無効になります。
マルチパーティ承認ワークフロー
トランザクションをデシリアライズして追加の署名を加え、再度シリアライズして保存または送信します。
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?