Solana 交易:持久 Nonces(2023)

  • Helius
  • 发布于 2023-09-14 10:15
  • 阅读 15

本文详细介绍了在Solana区块链上使用Durable Nonces(持久性随机数)的概念和实现方法,帮助开发者处理离线交易和确保交易的真实性。文章从环境搭建到代码实现,逐步讲解了如何创建和使用Durable Nonces,并提供了完整的代码示例。

6分钟阅读

2023年9月12日

介绍

作为开发者,在Solana上提交交易时,你需要通过Solana RPC获取最新的blockhash。这一步骤至关重要,有助于减轻重放攻击,确保一旦使用特定的blockhash签名和提交交易后,没有人可以使用相同的hash重新播放或重新提交该交易。

想象一下,你需要提交一笔需要从离线冷存储或硬件钱包(如Ledger)获取签名的交易。然而,blockhash的有效期很短,这可能会使你的交易失效。这时,durable nonce(耐久性随机数)就派上用场了,它能够支持安全的离线交易。

通过本指南的学习,你将理解:

  1. 什么是durable nonce。
  2. durable nonce的目的。
  3. 如何在交易中使用durable nonce。

交易:前提条件

在开始之前,请确保你具备:

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

环境设置

  1. 克隆我们的示例存储库,里面包含现有的工具:

代码

git clone
  1. 进入项目文件夹并安装npm:

代码

cd durable-nonce
npm 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 airdrop 2
  1. 我们还需要为nonce权限创建并资助另一个钱包:

代码

solana-keygen new -o ./nonceAuth.json
  1. 对于此公钥,我们可以使用水龙头网站空投1 SOL, 这里

现在,你的环境已设置好,我们可以进行下一步。

什么是 Durable Nonce?

Solana上的durable nonce账户可以被视为一个保险箱。当你启动此账户时,Solana为其分配一个唯一且稳定的代码,称为“durable nonce”。与每次交易都会变化的典型随机数不同,这个随机数保持不变,作为一致的参考。

这在进行“离线”交易时特别有用。在构建交易时,你引用来自账户的此随机数。Solana会将其与存储的值进行验证,如果匹配,交易将获得批准。因此,durable nonce账户是一种存储和验证机制,确保交易的真实性,并适应Solana网络快速的节奏和离线场景。

durable nonce 可以用于多种用例,例如:

  • 定时交易: 你可以设置未来的特定时间进行交易。durable nonce确保这些定时交易安全执行。
  • 多重签名钱包: 在多签名钱包的上下文中,durable nonce提供额外的安全性和多个签字者之间的协调。
  • 需要未来交互的程序: Solana上的某些程序在特定间隔需要与其他程序或服务进行交互。durable nonce帮助维护这些交互的完整性。
  • 与其他区块链交互: 当Solana与其他区块链交互时,durable 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 TransferAmount = LAMPORTS_PER_SOL * 0.01;

步骤2:创建sendTransaction函数

sendTransaction函数协调使用durable 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函数负责创建和初始化durable nonce账户。这涉及计算账户所需的租金,获取最新的blockhash,以及构建创建和初始化nonce账户的交易。

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

代码

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网络。此交易创建durable 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指令。

代码

// 初始化账户内的nonce值
    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() {
  // 创建nonce账户
  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 {
    // 创建nonce账户
    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);

    // 现在,初始化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); // 仅用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函数,该函数创建一个包含advance 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: TransferAmount,
  });

  const sampleTx = new Transaction();
  sampleTx.add(advanceNonceIx, transferIx);
  sampleTx.recentBlockhash = nonce; // 使用之前获取的nonce
  sampleTx.feePayer = senderKeypair.publicKey;

  const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
  return serialisedTx;
}

步骤6:创建signOffline函数

定义signOffline函数,该函数负责离线签署交易。它模拟了签署交易之前的离线延迟,并使用发送者和nonce授权机构的密钥对进行签名。

代码

async function signOffline() {
  await new Promise(resolve => setTimeout(resolve, waitTime)); // 等待 time ms
  const unsignedTx = await readAndDecodeTransaction("./unsignedTxn.json");
  unsignedTx.sign(senderKeypair, nonceAuthKeypair); // 用两个密钥签署
  const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signedTxn.json");
  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)); // 等待3秒
    }
  }
  throw new Error("No account info found");
}

步骤9:调用sendTransaction函数

最终,调用sendTransaction函数以启动交易过程。此函数将所有先前定义的步骤整合在一起,以使用durable nonce创建、签署和执行交易。

代码

ts-node main

运行sendTransaction将生成成功交易的交易签名。此签名是用于在Solana网络上跟踪和验证交易的重要信息。

代码

Tx written to ./unsignedTxn.json
Tx written to ./signedTxn.json
Tx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

你现在已经在成功的交易中使用了durable nonce!

完整代码

代码

import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  NonceAccount,
  NONCE_ACCOUNT_LENGTH,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import {
  encodeAndWriteTransaction,
  loadWallet,
  readAndDecodeTransaction,
} from "./utils";

const TransferAmount = 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 {
    // 创建nonce并获取其创建交易签名
    const nonceCreationTxSig = await nonce();

    // 确保在继续之前确认了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);
  }
}

async function nonce() {
  // 创建nonce账户
  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 {
    // 创建nonce账户
    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);

    // 现在,初始化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); // 仅用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) {
  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: TransferAmount,
  });

  const sampleTx = new Transaction();
  sampleTx.add(advanceNonceIx, transferIx);
  sampleTx.recentBlockhash = nonce; // 使用之前获取的nonce
  sampleTx.feePayer = senderKeypair.publicKey;

  const serialisedTx = encodeAndWriteTransaction(
    sampleTx,
    "./unsigned.json",
    false
  );
  return serialisedTx;
}
async function signOffline(waitTime = 120000) {
  await new Promise((resolve) => setTimeout(resolve, waitTime));
  const unsignedTx = await readAndDecodeTransaction("./unsigned.json");
  unsignedTx.sign(senderKeypair, nonceAuthKeypair); // 用两个密钥签署
  const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signed.json");
  return serialisedTx;
}

async function executeTx() {
  const signedTx = await readAndDecodeTransaction("./signed.json");
  const sig = await connection.sendRawTransaction(signedTx.serialize());
  console.log("      Tx sent: ", sig);
}

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)); // 等待3秒
    }
  }
  throw new Error("No account info found");
}

sendTransaction();

Solana交易:使用Helius RPCs

Helius可以作为与Solana的RPC互动的强大中介,简化获取durable nonce所需blockhash信息的过程。通过Helius,你可以更可靠地管理Solana交易的生命周期,尤其是在离线场景下。它可以提供对blockhash的简化访问,帮助开发者让他们的应用在交易过期时更加稳健。

总而言之,Solana交易中的durable nonce提供了一种安全可靠的方法来处理离线交易,并确保交易的真实性。通过遵循本指南中概述的步骤,开发者可以在他们的Solana应用中实现durable nonce,从而增强安全性和灵活性。

  • 原文链接: helius.dev/blog/solana-t...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Helius
Helius
https://www.helius.dev/