新着:HeliusがLight Protocolを買収
永続的 nonceを使用してSolanaトランザクションを送信する方法
ブログ/開発

Solanaトランザクション:永続的 nonce(2023年)

デベロッパーリレーションズ責任者LinkedInのHunter Davis
読了時間:6分

はじめに

開発者がSolanaでトランザクションを送信する際は、Solana RPCを介して最新のブロックハッシュを取得する必要があります。この重要な手順により、リプレイ攻撃を防止できます。特定のブロックハッシュを使用してトランザクションが署名、送信された後は、同じハッシュを使ってそのトランザクションを再実行または再送信できなくなります。

Ledgerのようなオフラインのコールドストレージやハードウェアウォレットからの署名が必要なトランザクションを送信する場合を考えてみましょう。しかし、ブロックハッシュは短時間で期限切れになり、トランザクションが無効になる可能性があります。そこで、安全なオフライントランザクションを可能にする永続的 nonceが役立ちます。

このガイドを最後まで読むと、以下を理解できます。

  1. 永続的 nonceとは何か。
  2. 永続的 nonceの目的。
  3. トランザクションで永続的 nonceを使用する方法。

トランザクション: 前提条件

始める前に、以下を準備してください。

  • JavaScriptの基礎知識。
  • NodeJSがインストールされていること。
  • Solana CLIがインストールされていること。
  • Gitがインストールされていること

環境のセットアップ

  1. 既存のユーティリティを含むサンプルリポジトリをクローンします。
コード
git clone
  1. プロジェクトフォルダに移動し、npmをインストールします。
コード
cd durable-noncenpm install
  1. nonceフォルダ内のwalletsフォルダに移動します。ここにはテスト用のローカルキーを保存します。
コード
cd wallets
  1. Solana CLIをインストールした状態で、支払い用キーペアのウォレットを1つ作成します。
コード
solana-keygen new -o ./wallet.json
  1. 次に、Solanaをエアドロップするため、このウォレットをCLIのウォレットとして設定します。
コード
solana config set --keypair ./wallet.json
  1. 以下を実行すると、このアドレスにSolanaをエアドロップできます。
コード
solana aidrop 2
  1. nonce authority用の別のウォレットも作成し、資金を入れる必要があります。
コード
solana-keygen new -o ./nonceAuth.json
  1. 生成された公開鍵には、こちらのfaucetサイトを使用して1 SOLをエアドロップできます。

これで環境のセットアップが完了したので、次の手順に進みます。

永続的 nonceとは?

Solanaの永続的 nonceアカウントは、貸金庫のようなものです。このアカウントを作成すると、Solanaによって「永続的 nonce」と呼ばれる一意で安定したコードが割り当てられます。トランザクションごとに変化する通常のnonceとは異なり、この値は変わらず、一貫した参照値として機能します。

これは特に「オフライン」トランザクションで役立ちます。トランザクションを作成するときに、アカウントのこのnonceを参照します。Solanaは保存された値と照合し、一致すればトランザクションを承認します。つまり、永続的 nonceアカウントは保存と検証の両方の仕組みとして機能し、Solanaネットワークの高速な処理やオフライン環境に対応しながら、トランザクションの真正性を確保します。

永続的 nonceは、次のようなさまざまなユースケースで使用できます。

  • 予約トランザクション: 将来の指定時刻に実行されるトランザクションを設定できます。永続的 nonceにより、これらの予約トランザクションを安全に実行できます。
  • マルチシグウォレット: マルチシグウォレットでは、永続的 nonceによってセキュリティが強化され、複数の署名者間の連携が容易になります。
  • 将来のインタラクションが必要なプログラム: Solana上の一部のプログラムでは、特定の間隔で他のプログラムやサービスとやり取りする必要があります。永続的 nonceは、これらのインタラクションの整合性を維持するのに役立ちます。
  • 他のブロックチェーンとの連携: Solanaが他のブロックチェーンと連携する際、永続的 nonceはクロスチェーントランザクションの有効性を確保する役割を果たします。

それでは、サンプルの構築を始めます。

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関数は、永続的 nonceを使用してトランザクションを送信するプロセスを統括します。この関数は、nonceの作成、確認、トランザクションの実行を処理します。

コード
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関数は、永続的 nonceアカウントの作成と初期化を担います。これには、アカウントに必要なrentの計算、最新ブロックハッシュの取得、nonceアカウントを作成および初期化するトランザクションの構築が含まれます。

  1. nonceアカウントを作成する前に、アカウントデータの保存に必要なrentを計算し、最新のブロックハッシュを取得します。
コード
async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. 次に、nonceアカウントを作成するトランザクションを構築します。SystemProgram.createAccount命令を使用して、nonceアカウント用の領域を割り当てます。
コード
const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. authorityのキーペアでトランザクションに署名し、Solanaネットワークに送信します。このトランザクションによって、永続的 nonceアカウントが作成されます。
コード
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" });
  1. トランザクションを送信したら、そのステータスを確認し、nonceアカウントが正常に作成されたことを確かめます。
コード
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);
  1. nonceアカウントを完全に利用するには、その値を初期化する必要があります。SystemProgram.nonceInitialize命令を実行する新しいトランザクションを作成します。
コード
// Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. 前のステップと同様に、トランザクションに署名してネットワークへ送信し、nonceアカウントを初期化します。
コード
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" });
  1. 最後に、初期化トランザクションのステータスを確認し、nonceアカウントが正しく初期化されたことを確かめます。
コード
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関数を作成する

作成したnonceアカウントからnonce値を取得するgetNonce関数を定義します。

コード
async function getNonce() {
  const nonceAccount = await fetchNonceInfo();
  return nonceAccount.nonce;
}

ステップ5:createTx関数を作成する

createTx関数を定義します。この関数は、nonceを進める命令と転送命令の両方を含むサンプルトランザクションを作成します。先ほど取得したnonceを使用して、トランザクションの真正性を確保します。

コード
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関数を定義します。この関数はオフラインでの遅延をシミュレートしてから、送信者とnonce authorityの両方のキーペアでトランザクションに署名します。

コード
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関数は、作成したnonceアカウントからnonce情報を取得し、必要に応じて最大3回再試行します。これにより、トランザクションで使用するnonceが最新かつ有効であることを確認できます。

コード
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関数を呼び出してトランザクション処理を開始します。この関数は、これまでに定義したすべてのステップを組み合わせ、永続的 nonceを使用したトランザクションの作成、署名、実行を行います。

コード
ts-node main

sendTransactionを実行すると、成功したトランザクションの署名が出力されます。この署名は、Solanaネットワーク上でトランザクションを追跡、検証するために不可欠な情報です。

コード
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

これで、永続的 nonceを使用したトランザクションが正常に完了しました。

完全なコード

コード
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とやり取りするための強力な仲介役となり、永続的 nonceに必要なブロックハッシュ情報の取得を簡素化します。Heliusを利用すると、特にオフライン環境において、Solanaトランザクションのライフサイクルをより確実に管理できます。ブロックハッシュへ効率的にアクセスできるため、開発者はトランザクションの期限切れに強いアプリケーションを構築できます。

まとめると、Solanaトランザクションの永続的 nonceは、オフライントランザクションを処理し、トランザクションの真正性を確保するための安全で信頼性の高い方法です。このガイドの手順に従うことで、開発者はSolanaアプリケーションに永続的 nonceを実装し、セキュリティと柔軟性を強化できます。

‍

‍

‍

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます