新消息: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 后,为支付密钥对创建一个钱包:
代码
solana-keygen new -o ./wallet.json
  1. 现在,在 CLI 中将其设为你的钱包,以便空投 Solana:
代码
solana config set --keypair ./wallet.json
  1. 现在运行以下命令,即可向此地址空投 Solana:
代码
solana aidrop 2
  1. 我们还需要为 nonce 权限账户创建另一个钱包并注入资金:
代码
solana-keygen new -o ./nonceAuth.json
  1. 对于生成的这个公钥,我们可以使用水龙头网站向其空投 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 账户。此过程包括计算账户所需的租金、获取最新区块哈希,以及构建用于创建和初始化 nonce 账户的交易。

  1. 创建 nonce 账户之前,我们需要计算存储账户数据所需的租金,并获取最新区块哈希。
代码
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. 我们将使用权限密钥对签署交易,并将其发送到 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 函数

定义 getNonce 函数,用于从已创建的 nonce 账户中获取 nonce 值。

代码
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 权限账户的密钥对签署交易。

代码
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 信息,并在必要时最多重试三次。这有助于确保交易使用的 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 开发的最新动态,并在我们发布新内容时收到更新