背景

离线交易

常规Solana交易都是典型短生命周期(大约1分钟),依赖recent_blockhash。如果想提前离线生成交易,等待较长时间再发送,则会过期,交易失败。
持久交易 nonce 是一种绕过交易的典型短生命周期的机制 recent_blockhash

顺序执行

持久交易 nonce可以让交易按照顺序执行

创建Nonce账户

账户地址可以通过create-nonce-account创建nonce账户,nonce账户用于存放下一个nonce值,nonce账户必须是免租的,所以需要持有最低的余额。

solana-keygen new -o nonce-keypair.json
solana create-nonce-account nonce-keypair.json 1

查询nonce

solana nonce nonce-keypair.json

输出

8GRipryfxcsxN8mAGjy8zbFo9ezaUsh47TsPzmZbuytU

提高存储的 Nonce

solana new-nonce nonce-keypair.json

显示nonce账户

solana nonce-account nonce-keypair.json

输出

balance: 0.5 SOL
minimum balance required: 0.00136416 SOL
nonce: DZar6t2EaCFQTbUP4DHKwZ1wT8gCPW2aRfkVWhydkBvS

从nonce账户提取资金

solana withdraw-from-nonce-account nonce-keypair.json ~/.config/solana/id.json 0.5

通过提取全部余额来关闭 nonce 账户

创建 nonce 账户后重新分配权限

solana authorize-nonce-account nonce-keypair.json nonce-authority.json

CLI 实测

这里我们演示了 Alice 使用持久随机数向 Bob 支付 1 个 SOL。对于所有支持持久随机数的子命令,该过程都是相同的

创建

首先,我们需要一些 Alice、Alice 的随机数和 Bob 的账户

solana-keygen new -o alice.json
solana-keygen new -o nonce.json
solana-keygen new -o bob.json

为Alice领取测试代币

Alice 需要一些资金来创建一个 nonce 账户并发送给 Bob。空投一些 SOL

solana airdrop -k alice.json 1
1 SOL

创建 Alice 的 nonce

现在 Alice 需要一个 nonce 账户。创建一个
这里没有使用 单独的nonce 权限alice.json,因此对 nonce 账户拥有完全的权限

solana create-nonce-account -k alice.json nonce.json 0.1
3KPZr96BTsL3hqera9up82KAU462Gz31xjqJ6eHUAjF935Yf8i1kmfEbo6SVbNaACKE5z6gySrNjVRvmS8DcPuwV

第一次向Bob转账

Alice 尝试向 Bob 付款,但签名时间过长。指定的区块哈希过期,交易失败

$ solana transfer -k alice.json --blockhash expiredDTaxfagttWjQweib42b6ZHADSx94Tw8gHx11 bob.json 0.01
[2025-03-06T18:48:28.462911000Z ERROR solana_cli::cli] Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" })
Error: Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" })

Nonce 来救援

Alice 重试交易,这次指定她的 nonce 账户和存储在那里的区块哈希
记住,在这个例子中,alice.json是nonce 权限

solana nonce-account nonce.json
balance: 0.1 SOL
minimum balance required: 0.00136416 SOL
nonce: F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7
$ solana transfer -k alice.json --blockhash F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 --nonce nonce.json bob.json 0.01
HR1368UKHVZyenmH7yVz5sBAijV6XAPeWbEiXEGVYQorRMcoijeNAbzZqEZiH8cDB8tk65ckqeegFjK8dHwNFgQ

成功

交易成功!Bob 从 Alice 处收到 0.01 SOL,Alice 存储的 nonce 值增加到新值

solana balance -k bob.json
0.01 SOL
solana nonce-account nonce.json
balance: 0.1 SOL
minimum balance required: 0.00136416 SOL
nonce: 6bjroqDcZgTv6Vavhqf81oBHTv3aMnX19UTB51YhAZnN

测试代码

const {
  Connection,
  Keypair,
  PublicKey,
  LAMPORTS_PER_SOL,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
  NonceAccount
} = require('@solana/web3.js');

async function main() {
  // 连接到 Solana 网络
  const connection = new Connection('https://api.devnet.solana.com', 'confirmed');

  // 创建一个新的 Keypair 作为 Nonce 账户的所有者
  const owner = Keypair.generate();

  // 创建一个新的 Keypair 作为 Nonce 账户
  const nonceAccount = Keypair.generate();

  // 计算 Nonce 账户所需的租金
  const nonceRent = await connection.getMinimumBalanceForRentExemption(NonceAccount.span);

  // 创建一个创建 Nonce 账户的交易
  const createNonceAccountTransaction = new Transaction().add(
    SystemProgram.createNonceAccount({
      fromPubkey: owner.publicKey,
      noncePubkey: nonceAccount.publicKey,
      lamports: nonceRent
    })
  );

  // 发送并确认创建 Nonce 账户的交易
  await sendAndConfirmTransaction(connection, createNonceAccountTransaction, [owner, nonceAccount]);

  // 获取 Nonce 账户的状态
  const nonceAccountState = await connection.getNonce(nonceAccount.publicKey);
  let nonce = nonceAccountState.nonce;

  // 离线签署 10 笔交易
  for (let i = 0; i < 10; i++) {
    // 创建一个新的交易
    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: owner.publicKey,
        toPubkey: new PublicKey('your_recipient_address'),
        lamports: LAMPORTS_PER_SOL * 0.01 // 示例金额
      })
    );

    // 设置 Nonce 信息
    transaction.recentBlockhash = nonce;
    transaction.feePayer = owner.publicKey;

    // 签署交易
    transaction.sign(owner);

    // 在这里你可以将交易序列化并保存,以便离线发送
    const serializedTransaction = transaction.serialize();

    console.log(`第 ${i + 1} 笔交易的 Nonce:`, nonce);

    // 推进 Nonce
    const advanceNonceTransaction = new Transaction().add(
      SystemProgram.advanceNonceAccount({
        noncePubkey: nonceAccount.publicKey,
        authorizedPubkey: owner.publicKey
      })
    );
    await sendAndConfirmTransaction(connection, advanceNonceTransaction, [owner]);

    // 获取新的 Nonce
    const newNonceAccountState = await connection.getNonce(nonceAccount.publicKey);
    nonce = newNonceAccountState.nonce;
  }
}

main().catch(console.error);

总结

  1. 如果想离线长期保存交易或者期望交易按顺序执行,可以使用nonce账户
  2. 实现过程,查询对应的nonce账户的nonce值,代替recent_blockhash
  3. CLI中每次交易后nonce账户的nonce值会自动推进更新,对于代码调用,需要发起SystemProgram.advanceNonceAccount交易主动推进
  4. nonce值无法本地离线计算推进,每次都需要链上发起更新

https://docs.anza.xyz/cli/examples/durable-nonce