
Solana 交易:持久 nonce(2023)
简介
作为开发者在 Solana 上提交交易时,你需要通过 Solana RPC 获取最新的区块哈希。这一关键步骤有助于防范重放攻击,确保交易使用特定区块哈希签名并提交后,任何人都无法使用同一哈希重放或重新提交该交易。
假设你需要提交一笔交易,而它必须由离线冷存储或 Ledger 等硬件钱包签名。但区块哈希很快就会过期,可能导致交易失效。这正是持久 nonce 的用武之地,它可以实现安全的离线交易。
完成本指南后,你将了解:
- 什么是持久 nonce。
- 持久 nonce 的用途。
- 如何在交易中使用持久 nonce。
交易:前置要求
开始之前,请确保你已具备:
- JavaScript 基础知识。
- 已安装 NodeJS。
- 已安装 Solana CLI。
- 已安装 Git
环境设置
- 克隆包含现有工具函数的示例代码仓库:
git clone- 进入项目文件夹并安装 npm 依赖:
cd durable-noncenpm install- 进入 nonce 文件夹内的 wallets 文件夹。这里将存放用于测试的本地密钥:
cd wallets- 安装 Solana CLI 后,为支付密钥对创建一个钱包:
solana-keygen new -o ./wallet.json- 现在,在 CLI 中将其设为你的钱包,以便空投 Solana:
solana config set --keypair ./wallet.json- 现在运行以下命令,即可向此地址空投 Solana:
solana aidrop 2- 我们还需要为 nonce 权限账户创建另一个钱包并注入资金:
solana-keygen new -o ./nonceAuth.json- 对于生成的这个公钥,我们可以使用水龙头网站向其空投 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 账户的交易。
- 创建 nonce 账户之前,我们需要计算存储账户数据所需的租金,并获取最新区块哈希。
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- 现在,我们将构建一笔交易来创建 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,
})
);- 我们将使用权限密钥对签署交易,并将其发送到 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" });- 发送交易后,我们将确认其状态,确保 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);- 为了充分使用 nonce 账户,我们需要初始化它的值。我们将创建一笔新交易来执行
SystemProgram.nonceInitialize指令。
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- 与上一步类似,我们将签署交易并将其发送到网络,以初始化 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" });- 最后,我们将确认初始化交易的状态,确保 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 开发的最新动态,并在我们发布新内容时收到更新


