Solana 交易:持久随机数 (2023)

作为开发者在 Solana 上提交交易时,您需要通过 Solana RPC 获取最新的区块哈希。这一关键步骤有助于减轻重放攻击,确保一旦使用特定的区块哈希对交易进行签名和提交,没有人可以使用相同的哈希来重放或重新提交该交易。 读完本指南后,您将了解,多么持久的随机数啊,持久随机数的目的,如何在交易中使用持久随机数。

Solana 交易:持久随机数 (2023)

介绍

作为开发者在 Solana 上提交交易时,您需要通过 Solana RPC 获取最新的区块哈希。这一关键步骤有助于减轻重放攻击,确保一旦使用特定的区块哈希对交易进行签名和提交,没有人可以使用相同的哈希来重放或重新提交该交易。

想象一下需要提交一笔交易,该交易需要来自离线冷存储或 Ledger 等硬件钱包的签名。然而,区块哈希很快就会过期,可能会导致您的交易无效。这就是持久随机数的用武之地,它可以实现安全的离线交易。

读完本指南后,您将了解:

  1. 多么持久的随机数啊。
  2. 持久随机数的目的。
  3. 如何在交易中使用持久随机数。

交易:先决条件

在开始之前,请确保您拥有:

  • 基本的 JavaScript 知识。
  • NodeJS 已安装。
  • Solana CLI 已安装。
  • 已安装 Git

环境设置

  1. 使用现有实用程序克隆我们的示例存储库:

git clone 

  1. 导航到项目文件夹并安装 npm:

cd durable-noncenpm install

  1. 导航到 nonce 文件夹内的 wallets 文件夹,这将存放我们用于测试的本地密钥并导航到其中:

cd wallets

  1. 安装 Solana CLI 后,为支付密钥对创建一个钱包:

solana-keygen new -o ./wallet.json

  1. 现在将其设置为 CLI 上的钱包以空投 Solana:

solana config set --keypair ./wallet.json

  1. 现在您可以通过运行以下命令将 Solana 空投到该地址:

solana aidrop 2

  1. 我们还需要为随机数权威创建另一个钱包并为其提供资金:

solana-keygen new -o ./nonceAuth.json

  1. 对于生成的公钥,我们可以使用水龙头站点向其空投 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账户。这涉及计算帐户所需的租金、获取最新的区块哈希以及构建交易以创建和初始化随机数帐户。

  1. 在创建nonce账户之前,我们需要计算账户数据存储所需的租金并获取最新的区块哈希。

async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. 现在,我们将构建一个交易来创建随机数帐户。这涉及使用SystemProgram.createAccount指令为随机数帐户分配空间。

  const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. 我们将使用授权密钥对签署交易,并将其发送到 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" });
  1. 发送交易后,我们将确认其状态以确保随机数帐户创建成功。

   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. 为了充分利用随机数账户,我们需要初始化它的值。我们将创建一个新事务来执行SystemProgram.nonceInitialize指令。

      // Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. 与上一步类似,我们将签署交易并将其发送到网络以初始化随机数帐户。

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. 最后,我们将确认初始化交易的状态,以确保随机数帐户正确初始化。

    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函数,负责从创建的nonce账户中获取nonce值。


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函数从创建的随机数帐户中获取随机数信息,如有必要,最多可重试 3 次。这有助于确保交易中使用的随机数是最新且有效的。



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 main


运行 sendTransaction 将填充成功交易的交易签名。该签名是跟踪和验证 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 应用程序中实现持久随机数,从而增强安全性和灵活性。

💡
原文链接:Solana Transactions: Durable Nonces (2023)
本文由SlerfTools翻译,转载请注明出处。

SlerfTools专为Solana设计的工具箱,致力于简化区块链操作,提供无编程全可视化界面,使发币管理流动性无代码创建Dapp等复杂过程变得安全简单。