本文详细介绍了在Solana区块链上使用Durable Nonces(持久性随机数)的概念和实现方法,帮助开发者处理离线交易和确保交易的真实性。文章从环境搭建到代码实现,逐步讲解了如何创建和使用Durable Nonces,并提供了完整的代码示例。
6分钟阅读
2023年9月12日
作为开发者,在Solana上提交交易时,你需要通过Solana RPC获取最新的blockhash。这一步骤至关重要,有助于减轻重放攻击,确保一旦使用特定的blockhash签名和提交交易后,没有人可以使用相同的hash重新播放或重新提交该交易。
想象一下,你需要提交一笔需要从离线冷存储或硬件钱包(如Ledger)获取签名的交易。然而,blockhash的有效期很短,这可能会使你的交易失效。这时,durable nonce(耐久性随机数)就派上用场了,它能够支持安全的离线交易。
通过本指南的学习,你将理解:
在开始之前,请确保你具备:
代码
git clone
代码
cd durable-nonce
npm install
代码
cd wallets
代码
solana-keygen new -o ./wallet.json
代码
solana config set --keypair ./wallet.json
代码
solana airdrop 2
代码
solana-keygen new -o ./nonceAuth.json
现在,你的环境已设置好,我们可以进行下一步。
Solana上的durable nonce账户可以被视为一个保险箱。当你启动此账户时,Solana为其分配一个唯一且稳定的代码,称为“durable nonce”。与每次交易都会变化的典型随机数不同,这个随机数保持不变,作为一致的参考。
这在进行“离线”交易时特别有用。在构建交易时,你引用来自账户的此随机数。Solana会将其与存储的值进行验证,如果匹配,交易将获得批准。因此,durable nonce账户是一种存储和验证机制,确保交易的真实性,并适应Solana网络快速的节奏和离线场景。
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 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;
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);
}
}
nonce函数负责创建和初始化durable nonce账户。这涉及计算账户所需的租金,获取最新的blockhash,以及构建创建和初始化nonce账户的交易。
代码
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
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,
})
);
代码
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" });
代码
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);
SystemProgram.nonceInitialize
指令。代码
// 初始化账户内的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);
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 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;
}
}
定义getNonce
函数,该函数负责从创建的nonce账户中获取nonce值。
代码
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}
定义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;
}
定义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;
}
executeTx
函数负责将已签署的交易发送到Solana网络以供执行。这是交易过程的最后一步,在此过程中交易被广播到网络。
代码
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}
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");
}
最终,调用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();
Helius可以作为与Solana的RPC互动的强大中介,简化获取durable nonce所需blockhash信息的过程。通过Helius,你可以更可靠地管理Solana交易的生命周期,尤其是在离线场景下。它可以提供对blockhash的简化访问,帮助开发者让他们的应用在交易过期时更加稳健。
总而言之,Solana交易中的durable nonce提供了一种安全可靠的方法来处理离线交易,并确保交易的真实性。通过遵循本指南中概述的步骤,开发者可以在他们的Solana应用中实现durable nonce,从而增强安全性和灵活性。
- 原文链接: helius.dev/blog/solana-t...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!