Skip to main content

18 posts tagged with "Solana"

View All Tags

Solana 的 Rust 库发布在 crates.io 上, 并可以在 docs.rs 上以 solana- 前缀找到。

info

你好,世界:开始 Solana 开发

要快速开始 Solana 开发并构建您的第一个 Rust 程序,

请查看以下详细的快速入门指南:

  • 使用浏览器构建并部署您的第一个 Solana 程序。无需安装。
  • 设置本地环境并使用本地测试验证器。

Rust 库

以下是 Solana 开发中最重要和常用的 Rust 库:

  • solana-program — 由在 Solana 上运行的程序导入,编译为 SBF。 此库包含许多基本数据类型,并从 solana-sdk 重新导出,solana-sdk 不能从 Solana 程序中导入。
  • solana-sdk — 基本的链下 SDK,它重新导出 solana-program 并在其上添加更多 API。 大多数不在链上运行的 Solana 程序将导入此库。
  • solana-client — 用于通过 JSON RPC API 与 Solana 节点交互。
  • solana-cli-config — 加载和保存 Solana CLI 配置文件。
  • solana-clap-utils — 使用 clap 设置 CLI 的例程,如主要的 Solana CLI 所用。 包括加载 CLI 支持的所有类型签名者的功能。
鱼雪

什么是 Solana-Web3.js?

Solana-Web3.js 库旨在提供对 Solana 的全面覆盖。

该库建立在 Solana 的 JSON RPC API 之上

可以在 @solana/web3.js 文档 找到完整文档。

常用术语

  • Program无状态的可执行代码,用于解释指令。程序能够根据提供的指令执行操作
  • Instruction客户端可以包含在交易中的程序的最小单位。 在其处理代码中,一条指令可能包含一个或多个跨程序调用。
  • Transaction:由客户端使用一个或多个密钥对签名的一个或多个指令,以原子方式执行,仅有两种可能结果:成功或失败。

更多术语请参见 Solana 术语表.

入门

安装

使用 yarn:

yarn add @solana/web3.js

使用 npm:

npm install --save @solana/web3.js

浏览器捆绑包:

<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.js"></script>
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>

用法

JavaScript:

const solanaWeb3 = require("@solana/web3.js");
console.log(solanaWeb3);

ES6:

import * as solanaWeb3 from "@solana/web3.js";
console.log(solanaWeb3);

浏览器捆绑包:

<script>
// solanaWeb3 是由捆绑包脚本提供的全局命名空间
console.log(solanaWeb3);
</script>

快速开始

连接到钱包

要让用户使用您的 dApp 或 Solana 应用,他们需要获取他们的密钥对

密钥对是一个包含匹配的公钥和私钥的实体,用于签署交易

获取密钥对的两种方式

  1. 生成新的密钥对
  2. 使用密钥(secret key)获得密钥对

生成新的密钥对:

const { Keypair } = require("@solana/web3.js");

let keypair = Keypair.generate();

通过密钥获得密钥对:

const { Keypair } = require("@solana/web3.js");

let secretKey = Uint8Array.from([
202, 171, 192, 129, 150, 189, 204, 241, 142, 71, 205, 2, 81, 97, 2, 176, 48,
81, 45, 1, 96, 138, 220, 132, 231, 131, 120, 77, 66, 40, 97, 172, 91, 245, 84,
221, 157, 190, 9, 145, 176, 130, 25, 43, 72, 107, 190, 229, 75, 88, 191, 136,
7, 167, 109, 91, 170, 164, 186, 15, 142, 36, 12, 23,
]);

let keypair = Keypair.fromSecretKey(secretKey);

目前许多钱包允许用户通过各种扩展或网络钱包带入他们的密钥对。

一般建议使用钱包而不是密钥对来签署交易

钱包在 dApp 和密钥对之间创建了一个分离层,确保 dApp 永远不会访问密钥

可以通过 wallet-adapter 库找到连接外部钱包的方法。

创建和发送交易

要与 Solana 上的程序交互,需要创建、签署并发送交易到网络

交易是带有签名的指令集合

指令在交易中的顺序决定了它们的执行顺序

使用 Transaction 对象创建交易,并添加所需的消息、地址或指令

以下是一个转账交易的示例:

const {
Keypair,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
} = require("@solana/web3.js");

let fromKeypair = Keypair.generate();
let toKeypair = Keypair.generate();
let transaction = new Transaction();

transaction.add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: toKeypair.publicKey,
lamports: LAMPORTS_PER_SOL,
}),
);

上述代码创建了一个准备签名并广播到网络的交易。

SystemProgram.transfer 指令被添加到交易中, 包含要发送的 lamports 数量,以及 tofrom 的公钥。

签署交易并发送到网络

const {
sendAndConfirmTransaction,
clusterApiUrl,
Connection,
} = require("@solana/web3.js");

let keypair = Keypair.generate();
let connection = new Connection(clusterApiUrl("testnet"));

sendAndConfirmTransaction(connection, transaction, [keypair]);

上述代码使用 SystemProgramTransactionInstruction 创建交易,并将其发送到网络。

使用 Connection 定义连接到的 Solana 网络,主要是 mainnet-betatestnetdevnet

与自定义程序交互

在 Solana 上,所有操作都与不同的程序交互,包括转账交易。

Solana 上的程序目前用 Rust 或 C 编写。

例如,SystemProgramallocate 方法签名如下:

pub fn allocate(
pubkey: &Pubkey,
space: u64
) -> Instruction

要与程序交互,必须首先知道所有将要交互的账户

必须提供程序将交互的每个账户

并指定账户是否是签名者(`isSigner`)或可写(`isWritable`)

在上面的allocate方法中,需要一个单独的帐户公钥,以及要分配的空间量

我们知道allocate方法通过在帐户内分配空间来写入,因此需要isWritable

当您指定运行指令的帐户时,需要isSigner

在这种情况下,签署者是调用在其内部分配空间的帐户

让我们看看如何使用solana-web3.js调用此指令:

调用 allocate 方法示例:

let keypair = web3.Keypair.generate();
let payer = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl("testnet"));

let airdropSignature = await connection.requestAirdrop(
payer.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

首先,我们设置账户 Keypair 和连接,以便在测试网上进行分配。

我们还创建一个支付方 Keypair 并进行空投一些 Sol,以便支付分配交易。

let allocateTransaction = new web3.Transaction({
feePayer: payer.publicKey,
});
let keys = [{ pubkey: keypair.publicKey, isSigner: true, isWritable: true }];
let params = { space: 100 };

我们创建了交易allocateTransactionkeysparams 对象。

在创建交易时,feePayer 是一个可选字段,用于指定谁支付交易费,默认为交易中第一个签名者的公钥。

keys 代表程序的allocate函数将与之交互的所有账户。

由于allocate函数还需要空间,我们创建了params,在稍后调用allocate函数时使用。

let allocateStruct = {
index: 8,
layout: struct([u32("instruction"), ns64("space")]),
};

上述内容是使用 @solana/buffer-layoutu32ns64 创建的,以便促进有效载荷的创建。

分配函数接受参数空间。为了与函数交互,我们必须将数据提供为缓冲区格式。

buffer-layout 库有助于为 Solana 上的 Rust 程序正确分配缓冲区并对其进行编码解释。

让我们来分解这个结构。

{
index: 8, /* <-- */
layout: struct([
u32('instruction'),
ns64('space'),
])
}

index设置为 8,因为分配函数位于 SystemProgram 的指令枚举中的第 8 个位置。

/* https://github.com/solana-labs/solana/blob/21bc43ed58c63c827ba4db30426965ef3e807180/sdk/program/src/system_instruction.rs#L142-L305 */
pub enum SystemInstruction {
/** 0 **/CreateAccount {/**/},
/** 1 **/Assign {/**/},
/** 2 **/Transfer {/**/},
/** 3 **/CreateAccountWithSeed {/**/},
/** 4 **/AdvanceNonceAccount,
/** 5 **/WithdrawNonceAccount(u64),
/** 6 **/InitializeNonceAccount(Pubkey),
/** 7 **/AuthorizeNonceAccount(Pubkey),
/** 8 **/Allocate {/**/},
/** 9 **/AllocateWithSeed {/**/},
/** 10 **/AssignWithSeed {/**/},
/** 11 **/TransferWithSeed {/**/},
/** 12 **/UpgradeNonceAccount,
}

接下来是u32('instruction')

{
index: 8,
layout: struct([
u32('instruction'), /* <-- */
ns64('space'),
])
}

在使用它调用指令时,分配结构中的布局必须始终首先具有 u32('instruction')

{
index: 8,
layout: struct([
u32('instruction'),
ns64('space'), /* <-- */
])
}

ns64('space')allocate 函数的参数。 您可以在 Rust 中的原始 allocate 函数中看到,space 的类型是 u64u64 是无符号 64 位整数。

JavaScript 默认只提供最多 53 位整数。

ns64 来自 @solana/buffer-layout,用于帮助在 Rust 和 JavaScript 之间进行类型转换。

您可以在 solana-labs/buffer-layout 中找到更多关于 Rust 和 JavaScript 之间的类型转换。

let data = Buffer.alloc(allocateStruct.layout.span);
let layoutFields = Object.assign({ instruction: allocateStruct.index }, params);
allocateStruct.layout.encode(layoutFields, data);

使用先前创建的缓冲布局,我们可以分配数据缓冲区

然后,我们分配我们的参数 { space: 100 },以便它正确映射到布局,并将其编码到数据缓冲区中。

现在数据已准备好发送到程序。

allocateTransaction.add(
new web3.TransactionInstruction({
keys,
programId: web3.SystemProgram.programId,
data,
}),
);

await web3.sendAndConfirmTransaction(connection, allocateTransaction, [
payer,
keypair,
]);

最后,我们将包含所有账户密钥、付款人、数据和程序ID的交易指令添加到交易中,并将交易广播到网络。

完整代码:

const { struct, u32, ns64 } = require("@solana/buffer-layout");
const { Buffer } = require("buffer");
const web3 = require("@solana/web3.js");

let keypair = web3.Keypair.generate();
let payer = web3.Keypair.generate();

let connection = new web3.Connection(web3.clusterApiUrl("testnet"));

let airdropSignature = await connection.requestAirdrop(
payer.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

let allocateTransaction = new web3.Transaction({
feePayer: payer.publicKey,
});
let keys = [{ pubkey: keypair.publicKey, isSigner: true, isWritable: true }];
let params = { space: 100 };

let allocateStruct = {
index: 8,
layout: struct([u32("instruction"), ns64("space")]),
};

let data = Buffer.alloc(allocateStruct.layout.span);
let layoutFields = Object.assign({ instruction: allocateStruct.index }, params);
allocateStruct.layout.encode(layoutFields, data);

allocateTransaction.add(
new web3.TransactionInstruction({
keys,
programId: web3.SystemProgram.programId,
data,
}),
);

await web3.sendAndConfirmTransaction(connection, allocateTransaction, [
payer,
keypair,
]);

总结

鱼雪

Web3 API参考指南

@solana/web3.js库是一个涵盖Solana JSON RPC API的软件包。

您可以在这里找到@solana/web3.js库的完整文档。

一般

连接

连接用于与Solana JSON RPC进行交互。 您可以使用连接来确认交易,获取账户信息等。

您可以通过定义JSON RPC集群终点和所需的承诺来创建连接。

一旦完成此操作,您可以使用此连接对象与任何Solana JSON RPC API 进行交互。

示例用法

const web3 = require("@solana/web3.js");

let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let slot = await connection.getSlot();
console.log(slot);
// 93186439

let blockTime = await connection.getBlockTime(slot);
console.log(blockTime);
// 1630747045

let block = await connection.getBlock(slot);
console.log(block);

/*
{
blockHeight: null,
blockTime: 1630747045,
blockhash: 'AsFv1aV5DGip9YJHHqVjrGg6EKk55xuyxn2HeiN9xQyn',
parentSlot: 93186438,
previousBlockhash: '11111111111111111111111111111111',
rewards: [],
transactions: []
}
*/

let slotLeader = await connection.getSlotLeader();
console.log(slotLeader);
//49AqLYbpJYc2DrzGUAH1fhWJy62yxBxpLEkfJwjKy2jr

上面的示例仅显示了 Connection 上的一些方法。请参阅生成的文档以获取完整列表。

交易

交易用于与Solana区块链上的程序进行交互。

这些交易使用TransactionInstructions构建, 其中包含所有可能进行交互的账户,以及任何所需的数据或程序地址。

每个TransactionInstruction 包括键、数据和程序ID。

您可以在单个交易中执行多个指令,同时与多个程序进行交互。

示例用法

const web3 = require("@solana/web3.js");
const nacl = require("tweetnacl");

// Airdrop SOL for paying transactions
let payer = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let airdropSignature = await connection.requestAirdrop(
payer.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

let toAccount = web3.Keypair.generate();

// Create Simple Transaction
let transaction = new web3.Transaction();

// Add an instruction to execute
transaction.add(
web3.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount.publicKey,
lamports: 1000,
}),
);

// Send and confirm transaction
// Note: feePayer is by default the first signer, or payer, if the parameter is not set
await web3.sendAndConfirmTransaction(connection, transaction, [payer]);

// Alternatively, manually construct the transaction
let recentBlockhash = await connection.getRecentBlockhash();
let manualTransaction = new web3.Transaction({
recentBlockhash: recentBlockhash.blockhash,
feePayer: payer.publicKey,
});
manualTransaction.add(
web3.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount.publicKey,
lamports: 1000,
}),
);

let transactionBuffer = manualTransaction.serializeMessage();
let signature = nacl.sign.detached(transactionBuffer, payer.secretKey);

manualTransaction.addSignature(payer.publicKey, signature);

let isVerifiedSignature = manualTransaction.verifySignatures();
console.log(`The signatures were verified: ${isVerifiedSignature}`);

// The signatures were verified: true

let rawTransaction = manualTransaction.serialize();

await web3.sendAndConfirmRawTransaction(connection, rawTransaction);

密钥对

密钥对用于在Solana内创建具有公钥和秘钥的帐户。

您可以生成密钥对,从种子生成,或者从秘钥创建。

示例用法

const { Keypair } = require("@solana/web3.js");

let account = Keypair.generate();

console.log(account.publicKey.toBase58());
console.log(account.secretKey);

// 2DVaHtcdTf7cm18Zm9VV8rKK4oSnjmTkKE6MiXe18Qsb
// Uint8Array(64) [
// 152, 43, 116, 211, 207, 41, 220, 33, 193, 168, 118,
// 24, 176, 83, 206, 132, 47, 194, 2, 203, 186, 131,
// 197, 228, 156, 170, 154, 41, 56, 76, 159, 124, 18,
// 14, 247, 32, 210, 51, 102, 41, 43, 21, 12, 170,
// 166, 210, 195, 188, 60, 220, 210, 96, 136, 158, 6,
// 205, 189, 165, 112, 32, 200, 116, 164, 234
// ]

let seed = Uint8Array.from([
70, 60, 102, 100, 70, 60, 102, 100, 70, 60, 102, 100, 70, 60, 102, 100, 70,
60, 102, 100, 70, 60, 102, 100, 70, 60, 102, 100, 70, 60, 102, 100,
]);
let accountFromSeed = Keypair.fromSeed(seed);

console.log(accountFromSeed.publicKey.toBase58());
console.log(accountFromSeed.secretKey);

// 3LDverZtSC9Duw2wyGC1C38atMG49toPNW9jtGJiw9Ar
// Uint8Array(64) [
// 70, 60, 102, 100, 70, 60, 102, 100, 70, 60, 102,
// 100, 70, 60, 102, 100, 70, 60, 102, 100, 70, 60,
// 102, 100, 70, 60, 102, 100, 70, 60, 102, 100, 34,
// 164, 6, 12, 9, 193, 196, 30, 148, 122, 175, 11,
// 28, 243, 209, 82, 240, 184, 30, 31, 56, 223, 236,
// 227, 60, 72, 215, 47, 208, 209, 162, 59
// ]

let accountFromSecret = Keypair.fromSecretKey(account.secretKey);

console.log(accountFromSecret.publicKey.toBase58());
console.log(accountFromSecret.secretKey);

// 2DVaHtcdTf7cm18Zm9VV8rKK4oSnjmTkKE6MiXe18Qsb
// Uint8Array(64) [
// 152, 43, 116, 211, 207, 41, 220, 33, 193, 168, 118,
// 24, 176, 83, 206, 132, 47, 194, 2, 203, 186, 131,
// 197, 228, 156, 170, 154, 41, 56, 76, 159, 124, 18,
// 14, 247, 32, 210, 51, 102, 41, 43, 21, 12, 170,
// 166, 210, 195, 188, 60, 220, 210, 96, 136, 158, 6,
// 205, 189, 165, 112, 32, 200, 116, 164, 234
// ]

使用 generate 生成一个随机的 Keypair 以在 Solana 上用作账户。

使用 fromSeed,您可以使用确定性构造函数生成一个 Keypair

fromSecret 从一个秘密的 Uint8array 创建一个 Keypair

您可以看到 generate KeypairfromSecret KeypairpublicKey 是相同的, 因为 generate Keypair 中的秘密被用于 fromSecret

warning

除非您使用高熵生成种子,否则不要使用 fromSeed。不要分享您的种子。将种子视为私钥对待。

PublicKey

PublicKey@solana/web3.js 中被用于交易、密钥对和程序。

在交易中列出每个账户和作为 Solana 上的通用标识符时,您需要公钥。

公钥可以使用 base58 编码的字符串、缓冲区、Uint8Array、数字和数字数组进行创建。

示例用法

const { Buffer } = require("buffer");
const web3 = require("@solana/web3.js");
const crypto = require("crypto");

// Create a PublicKey with a base58 encoded string
let base58publicKey = new web3.PublicKey(
"5xot9PVkphiX2adznghwrAuxGs2zeWisNSxMW6hU6Hkj",
);
console.log(base58publicKey.toBase58());

// 5xot9PVkphiX2adznghwrAuxGs2zeWisNSxMW6hU6Hkj

// Create a Program Address
let highEntropyBuffer = crypto.randomBytes(31);
let programAddressFromKey = await web3.PublicKey.createProgramAddress(
[highEntropyBuffer.slice(0, 31)],
base58publicKey,
);
console.log(`Generated Program Address: ${programAddressFromKey.toBase58()}`);

// Generated Program Address: 3thxPEEz4EDWHNxo1LpEpsAxZryPAHyvNVXJEJWgBgwJ

// Find Program address given a PublicKey
let validProgramAddress = await web3.PublicKey.findProgramAddress(
[Buffer.from("", "utf8")],
programAddressFromKey,
);
console.log(`Valid Program Address: ${validProgramAddress}`);

// Valid Program Address: C14Gs3oyeXbASzwUpqSymCKpEyccfEuSe8VRar9vJQRE,253

系统程序

SystemProgram 允许创建帐户、分配帐户数据、将帐户分配给程序、 处理 nonce 帐户以及传输 lamports

您可以使用 SystemInstruction 类来帮助解码阅读单个指令

示例用法

const web3 = require("@solana/web3.js");

// Airdrop SOL for paying transactions
let payer = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let airdropSignature = await connection.requestAirdrop(
payer.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

// Allocate Account Data
let allocatedAccount = web3.Keypair.generate();
let allocateInstruction = web3.SystemProgram.allocate({
accountPubkey: allocatedAccount.publicKey,
space: 100,
});
let transaction = new web3.Transaction().add(allocateInstruction);

await web3.sendAndConfirmTransaction(connection, transaction, [
payer,
allocatedAccount,
]);

// Create Nonce Account
let nonceAccount = web3.Keypair.generate();
let minimumAmountForNonceAccount =
await connection.getMinimumBalanceForRentExemption(web3.NONCE_ACCOUNT_LENGTH);
let createNonceAccountTransaction = new web3.Transaction().add(
web3.SystemProgram.createNonceAccount({
fromPubkey: payer.publicKey,
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: payer.publicKey,
lamports: minimumAmountForNonceAccount,
}),
);

await web3.sendAndConfirmTransaction(
connection,
createNonceAccountTransaction,
[payer, nonceAccount],
);

// Advance nonce - Used to create transactions as an account custodian
let advanceNonceTransaction = new web3.Transaction().add(
web3.SystemProgram.nonceAdvance({
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: payer.publicKey,
}),
);

await web3.sendAndConfirmTransaction(connection, advanceNonceTransaction, [
payer,
]);

// Transfer lamports between accounts
let toAccount = web3.Keypair.generate();

let transferTransaction = new web3.Transaction().add(
web3.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount.publicKey,
lamports: 1000,
}),
);
await web3.sendAndConfirmTransaction(connection, transferTransaction, [payer]);

// Assign a new account to a program
let programId = web3.Keypair.generate();
let assignedAccount = web3.Keypair.generate();

let assignTransaction = new web3.Transaction().add(
web3.SystemProgram.assign({
accountPubkey: assignedAccount.publicKey,
programId: programId.publicKey,
}),
);

await web3.sendAndConfirmTransaction(connection, assignTransaction, [
payer,
assignedAccount,
]);

Secp256k1Program

Secp256k1Program 用于验证 Secp256k1 签名,该签名被比特币和以太坊使用。

示例用法

const { keccak_256 } = require("js-sha3");
const web3 = require("@solana/web3.js");
const secp256k1 = require("secp256k1");

// Create a Ethereum Address from secp256k1
let secp256k1PrivateKey;
do {
secp256k1PrivateKey = web3.Keypair.generate().secretKey.slice(0, 32);
} while (!secp256k1.privateKeyVerify(secp256k1PrivateKey));

let secp256k1PublicKey = secp256k1
.publicKeyCreate(secp256k1PrivateKey, false)
.slice(1);

let ethAddress =
web3.Secp256k1Program.publicKeyToEthAddress(secp256k1PublicKey);
console.log(`Ethereum Address: 0x${ethAddress.toString("hex")}`);

// Ethereum Address: 0xadbf43eec40694eacf36e34bb5337fba6a2aa8ee

// Fund a keypair to create instructions
let fromPublicKey = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let airdropSignature = await connection.requestAirdrop(
fromPublicKey.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

// Sign Message with Ethereum Key
let plaintext = Buffer.from("string address");
let plaintextHash = Buffer.from(keccak_256.update(plaintext).digest());
let { signature, recid: recoveryId } = secp256k1.ecdsaSign(
plaintextHash,
secp256k1PrivateKey,
);

// Create transaction to verify the signature
let transaction = new Transaction().add(
web3.Secp256k1Program.createInstructionWithEthAddress({
ethAddress: ethAddress.toString("hex"),
plaintext,
signature,
recoveryId,
}),
);

// Transaction will succeed if the message is verified to be signed by the address
await web3.sendAndConfirmTransaction(connection, transaction, [fromPublicKey]);

消息

消息用作构造交易的另一种方式。

您可以使用交易中的账户、头部、指令和最近的区块哈希来构造消息

交易是消息和执行交易所需的所需签名列表的总和

示例用法

const { Buffer } = require("buffer");
const bs58 = require("bs58");
const web3 = require("@solana/web3.js");

let toPublicKey = web3.Keypair.generate().publicKey;
let fromPublicKey = web3.Keypair.generate();

let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let airdropSignature = await connection.requestAirdrop(
fromPublicKey.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

let type = web3.SYSTEM_INSTRUCTION_LAYOUTS.Transfer;
let data = Buffer.alloc(type.layout.span);
let layoutFields = Object.assign({ instruction: type.index });
type.layout.encode(layoutFields, data);

let recentBlockhash = await connection.getRecentBlockhash();

let messageParams = {
accountKeys: [
fromPublicKey.publicKey.toString(),
toPublicKey.toString(),
web3.SystemProgram.programId.toString(),
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 1,
numRequiredSignatures: 1,
},
instructions: [
{
accounts: [0, 1],
data: bs58.encode(data),
programIdIndex: 2,
},
],
recentBlockhash,
};

let message = new web3.Message(messageParams);

let transaction = web3.Transaction.populate(message, [
fromPublicKey.publicKey.toString(),
]);

await web3.sendAndConfirmTransaction(connection, transaction, [fromPublicKey]);

结构

结构类用于在 JavaScript 中创建与 Rust 兼容的结构体。

该类仅与 Borsh 编码的 Rust 结构体兼容。

示例用法

Rust中的结构体:

pub struct Fee {
pub denominator: u64,
pub numerator: u64,
}

使用web3:

import BN from "bn.js";
import { Struct } from "@solana/web3.js";

export class Fee extends Struct {
denominator: BN;
numerator: BN;
}

枚举

枚举类用于在 JavaScript 中表示与 Rust 兼容的枚举。

如果记录,该枚举将只是一个字符串表示,但在与 Struct 结合使用时,可以正常编码/解码。

该类仅与 Borsh 编码的 Rust 枚举兼容。

示例用法

Rust中的枚举:

pub enum AccountType {
Uninitialized,
StakePool,
ValidatorList,
}

使用web3:

import { Enum } from "@solana/web3.js";

export class AccountType extends Enum {}

NonceAccount

通常情况下,如果交易的 recentBlockhash 字段过时,交易将被拒绝。

为了提供某些托管服务,使用 Nonce 账户

通过 Nonce 账户在链上捕获 recentBlockhash 的交易不会过期,

只要 Nonce 账户未被推进。

您可以先创建一个普通账户, 然后使用 SystemProgram 使账户成为 Nonce 账户来创建一个 nonce 账户

示例用法

const web3 = require("@solana/web3.js");

// Create connection
let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

// Generate accounts
let account = web3.Keypair.generate();
let nonceAccount = web3.Keypair.generate();

// Fund account
let airdropSignature = await connection.requestAirdrop(
account.publicKey,
web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

// Get Minimum amount for rent exemption
let minimumAmount = await connection.getMinimumBalanceForRentExemption(
web3.NONCE_ACCOUNT_LENGTH,
);

// Form CreateNonceAccount transaction
let transaction = new web3.Transaction().add(
web3.SystemProgram.createNonceAccount({
fromPubkey: account.publicKey,
noncePubkey: nonceAccount.publicKey,
authorizedPubkey: account.publicKey,
lamports: minimumAmount,
}),
);
// Create Nonce Account
await web3.sendAndConfirmTransaction(connection, transaction, [
account,
nonceAccount,
]);

let nonceAccountData = await connection.getNonce(
nonceAccount.publicKey,
"confirmed",
);

console.log(nonceAccountData);
// NonceAccount {
// authorizedPubkey: PublicKey {
// _bn: <BN: 919981a5497e8f85c805547439ae59f607ea625b86b1138ea6e41a68ab8ee038>
// },
// nonce: '93zGZbhMmReyz4YHXjt2gHsvu5tjARsyukxD4xnaWaBq',
// feeCalculator: { lamportsPerSignature: 5000 }
// }

let nonceAccountInfo = await connection.getAccountInfo(
nonceAccount.publicKey,
"confirmed",
);

let nonceAccountFromInfo = web3.NonceAccount.fromAccountData(
nonceAccountInfo.data,
);

console.log(nonceAccountFromInfo);
// NonceAccount {
// authorizedPubkey: PublicKey {
// _bn: <BN: 919981a5497e8f85c805547439ae59f607ea625b86b1138ea6e41a68ab8ee038>
// },
// nonce: '93zGZbhMmReyz4YHXjt2gHsvu5tjARsyukxD4xnaWaBq',
// feeCalculator: { lamportsPerSignature: 5000 }
// }

上述示例展示了如何使用 SystemProgram.createNonceAccount 创建 NonceAccount, 以及如何从 accountInfo 中检索 NonceAccount

使用 nonce,您可以创建将 nonce 替代最近的 blockhash 的离线交易。

VoteAccount

投票账户是一个对象,它授予从网络上的本地投票账户程序解码投票账户的能力。

示例用法

const web3 = require("@solana/web3.js");

let voteAccountInfo = await connection.getProgramAccounts(web3.VOTE_PROGRAM_ID);
let voteAccountFromData = web3.VoteAccount.fromAccountData(
voteAccountInfo[0].account.data,
);
console.log(voteAccountFromData);
/*
VoteAccount {
nodePubkey: PublicKey {
_bn: <BN: cf1c635246d4a2ebce7b96bf9f44cacd7feed5552be3c714d8813c46c7e5ec02>
},
authorizedWithdrawer: PublicKey {
_bn: <BN: b76ae0caa56f2b9906a37f1b2d4f8c9d2a74c1420cd9eebe99920b364d5cde54>
},
commission: 10,
rootSlot: 104570885,
votes: [
{ slot: 104570886, confirmationCount: 31 },
{ slot: 104570887, confirmationCount: 30 },
{ slot: 104570888, confirmationCount: 29 },
{ slot: 104570889, confirmationCount: 28 },
{ slot: 104570890, confirmationCount: 27 },
{ slot: 104570891, confirmationCount: 26 },
{ slot: 104570892, confirmationCount: 25 },
{ slot: 104570893, confirmationCount: 24 },
{ slot: 104570894, confirmationCount: 23 },
...
],
authorizedVoters: [ { epoch: 242, authorizedVoter: [PublicKey] } ],
priorVoters: [
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object], [Object],
[Object], [Object]
],
epochCredits: [
{ epoch: 179, credits: 33723163, prevCredits: 33431259 },
{ epoch: 180, credits: 34022643, prevCredits: 33723163 },
{ epoch: 181, credits: 34331103, prevCredits: 34022643 },
{ epoch: 182, credits: 34619348, prevCredits: 34331103 },
{ epoch: 183, credits: 34880375, prevCredits: 34619348 },
{ epoch: 184, credits: 35074055, prevCredits: 34880375 },
{ epoch: 185, credits: 35254965, prevCredits: 35074055 },
{ epoch: 186, credits: 35437863, prevCredits: 35254965 },
{ epoch: 187, credits: 35672671, prevCredits: 35437863 },
{ epoch: 188, credits: 35950286, prevCredits: 35672671 },
{ epoch: 189, credits: 36228439, prevCredits: 35950286 },
...
],
lastTimestamp: { slot: 104570916, timestamp: 1635730116 }
}
*/

质押

StakeProgram

StakeProgram 便于质押 SOL 并将它们委托给网络上的任何验证器。

您可以使用 StakeProgram 创建一个质押账户,质押一些 SOL,授权账户用于提取质押金, 停用您的质押,并提取您的资金。

StakeInstruction 类用于解码和阅读通过调用StakeProgram 的交易中的更多指令

示例用法

const web3 = require("@solana/web3.js");

// Fund a key to create transactions
let fromPublicKey = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");

let airdropSignature = await connection.requestAirdrop(
fromPublicKey.publicKey,
web3.LAMPORTS_PER_SOL,
);
await connection.confirmTransaction({ signature: airdropSignature });

// Create Account
let stakeAccount = web3.Keypair.generate();
let authorizedAccount = web3.Keypair.generate();
/* Note: This is the minimum amount for a stake account -- Add additional Lamports for staking
For example, we add 50 lamports as part of the stake */
let lamportsForStakeAccount =
(await connection.getMinimumBalanceForRentExemption(
web3.StakeProgram.space,
)) + 50;

let createAccountTransaction = web3.StakeProgram.createAccount({
fromPubkey: fromPublicKey.publicKey,
authorized: new web3.Authorized(
authorizedAccount.publicKey,
authorizedAccount.publicKey,
),
lamports: lamportsForStakeAccount,
lockup: new web3.Lockup(0, 0, fromPublicKey.publicKey),
stakePubkey: stakeAccount.publicKey,
});
await web3.sendAndConfirmTransaction(connection, createAccountTransaction, [
fromPublicKey,
stakeAccount,
]);

// Check that stake is available
let stakeBalance = await connection.getBalance(stakeAccount.publicKey);
console.log(`Stake balance: ${stakeBalance}`);
// Stake balance: 2282930

// We can verify the state of our stake. This may take some time to become active
let stakeState = await connection.getStakeActivation(stakeAccount.publicKey);
console.log(`Stake state: ${stakeState.state}`);
// Stake state: inactive

// To delegate our stake, we get the current vote accounts and choose the first
let voteAccounts = await connection.getVoteAccounts();
let voteAccount = voteAccounts.current.concat(voteAccounts.delinquent)[0];
let votePubkey = new web3.PublicKey(voteAccount.votePubkey);

// We can then delegate our stake to the voteAccount
let delegateTransaction = web3.StakeProgram.delegate({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: authorizedAccount.publicKey,
votePubkey: votePubkey,
});
await web3.sendAndConfirmTransaction(connection, delegateTransaction, [
fromPublicKey,
authorizedAccount,
]);

// To withdraw our funds, we first have to deactivate the stake
let deactivateTransaction = web3.StakeProgram.deactivate({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: authorizedAccount.publicKey,
});
await web3.sendAndConfirmTransaction(connection, deactivateTransaction, [
fromPublicKey,
authorizedAccount,
]);

// Once deactivated, we can withdraw our funds
let withdrawTransaction = web3.StakeProgram.withdraw({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: authorizedAccount.publicKey,
toPubkey: fromPublicKey.publicKey,
lamports: stakeBalance,
});

await web3.sendAndConfirmTransaction(connection, withdrawTransaction, [
fromPublicKey,
authorizedAccount,
]);

授权

授权是在 Solana 中创建用于参与质押的授权账户时使用的对象。

您可以分别指定质押者和提取者,允许提取者与质押者不同的账户

您可以在 StakeProgram 下找到对授权对象的更多用法。

锁定

锁定与StakeProgram一起使用,用于创建账户。

锁定用于确定抵押将被锁定多长时间,或无法检索。

如果锁定对纪元和Unix时间戳都设置为 0,则该抵押账户的锁定将被禁用。

示例用法

const {
Authorized,
Keypair,
Lockup,
StakeProgram,
} = require("@solana/web3.js");

let account = Keypair.generate();
let stakeAccount = Keypair.generate();
let authorized = new Authorized(account.publicKey, account.publicKey);
let lockup = new Lockup(0, 0, account.publicKey);

let createStakeAccountInstruction = StakeProgram.createAccount({
fromPubkey: account.publicKey,
authorized: authorized,
lamports: 1000,
lockup: lockup,
stakePubkey: stakeAccount.publicKey,
});

上述代码创建了一个 createStakeAccountInstruction, 用于创建一个与 StakeProgram 配合使用的账户。

锁定期限已设置为 0,包括时代和 Unix 时间戳,在账户中禁用锁定。

查看 StakeProgram 以获取更多信息。

鱼雪

概述

地址查找表ALTs允许开发者创建相关地址的集合以便在单个交易中高效加载更多地址

Solana区块链的每个交易需要列出所有交互地址,默认限制为每个交易32个地址。 使用ALTs,可以将此限制提升到256个地址。

地址压缩

在地址查找表中整理完所需的所有地址后, 每个地址都可以通过表中的1字节索引(而不是完整的 32 字节地址)在交易中进行引用。 这种查找方法有效地将 32 字节地址“压缩”为 1 字节索引值。

这种压缩方式使得可以在单个查找表中存储多达256个地址,以供在任何给定交易内使用。

版本化交易

为了在交易中使用地址查找表,开发者必须使用新的版本化交易格式中的v0交易

创建地址查找表

使用 @solana/web3.js 库创建新的查找表类似于旧版遗留事务,但存在一些区别。

使用 @solana/web3.js 库,您可以使用 createLookupTable 函数来构建创建新查找表所需的指令, 以及确定其地址:

const web3 = require("@solana/web3.js");

// 连接到集群并获取当前`slot`
const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
const slot = await connection.getSlot();

// 假设:`payer`是一个有效的`Keypair`,有足够的SOL支付执行费用
const [lookupTableInst, lookupTableAddress] =
web3.AddressLookupTableProgram.createLookupTable({
authority: payer.publicKey,
payer: payer.publicKey,
recentSlot: slot,
});

console.log("查找表地址:", lookupTableAddress.toBase58());

// 通过交易发送`lookupTableInst`指令以在链上创建地址查找表
note

地址查找表可以使用 v0 交易或遗留交易创建。 但 Solana 运行时只能在使用 v0 版本事务时检索和处理查找表中的额外地址。

向查找表添加地址

地址添加到查找表称为扩展

使用 @solana/web3.js 库, 您可以使用 extendLookupTable 方法创建一个新的扩展指令

const extendInstruction = web3.AddressLookupTableProgram.extendLookupTable({
payer: payer.publicKey,
authority: payer.publicKey,
lookupTable: lookupTableAddress,
addresses: [
payer.publicKey,
web3.SystemProgram.programId,
// 在此处列出更多`publicKey`地址
],
});

// 通过交易将此`extendInstruction`发送到集群,以将`addresses`列表插入查找表
note

注意:由于传统交易的相同内存限制,用于扩展地址查找表的任何交易在一次性添加的地址数量上也受限。 因此,您需要使用多个交易来扩展任何包含更多地址(~20)的表,这些地址可以适应单个交易内存限制。

一旦这些地址已被插入到表中,并存储在链上,您将能够在未来交易中利用地址查找表

在未来交易中最多可以启用 256 个地址。

获取地址查找表

就像从集群请求另一个帐户(或PDA)一样,

您可以使用 getAddressLookupTable 方法获取完整的地址查找表:

const lookupTableAddress = new web3.PublicKey("");
const lookupTableAccount = (
await connection.getAddressLookupTable(lookupTableAddress)
).value;

console.log("集群中的查找表地址:", lookupTableAccount.key.toBase58());

我们的lookupTableAccount变量现在将是一个AddressLookupTableAccount对象,

我们可以解析它以读取存储在查找表中的所有地址的列表:

// 解析并读取表中存储的所有地址
for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) {
const address = lookupTableAccount.state.addresses[i];
console.log(i, address.toBase58());
}

在交易中使用地址查找表

创建了查找表,并将所需的地址存储在链上(通过扩展查找表)后, 您可以创建一个v0交易来利用链上查找功能。

就像旧版的传统(legacy)交易一样,您可以在链上创建您的交易将执行的所有指令。

然后,您可以将这些指令数组提供给在 v0 交易中使用的 Message。

info

v0 交易中使用的指令可以使用过去用于创建指令的相同方法和函数来构建。

与涉及地址查找表的指令无需更改。

// 假设:
// - `arrayOfInstructions` 已创建为 `TransactionInstruction` 的 `数组`
// - 我们正在使用上面获取的 `lookupTableAccount`

const messageV0 = new web3.TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: arrayOfInstructions, // 这是一个指令数组
}).compileToV0Message([lookupTableAccount]);

// 从`v0`消息创建一个`v0`交易
const transactionV0 = new web3.VersionedTransaction(messageV0);
// 使用我们创建的名为`payer`的文件系统钱包签署`v0`交易。
transactionV0.sign([payer]);
// 发送并确认交易
//(注意:这里没有签名者数组;请参见下面的说明...)
const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);

console.log(`Transaction: https://explorer.solana.com/tx/${txid}?cluster=devnet`);

总结

功能与优势

  • 扩展地址数量限制:默认情况下,每个交易最多只能包含32个地址。使用地址查找表,这一限制可以扩展到256个地址。
  • 地址压缩:地址查找表将32字节的地址压缩为1字节的索引值,从而提高了交易中地址引用的效率。

创建与使用

  1. 创建地址查找表:
  • 使用@solana/web3.js库中的createLookupTable函数。
  • 指定支付者和授权者的公钥,并获取当前的slot
  • 构建创建查找表的指令并发送交易以在链上创建查找表。
  1. 添加地址到查找表:
  • 使用extendLookupTable方法创建扩展指令。
  • 将所需的地址列表添加到查找表中。
  • 由于内存限制,可能需要多次交易来扩展查找表。
  1. 获取地址查找表:
  • 使用getAddressLookupTable方法从集群中获取完整的地址查找表。
    • 解析并读取表中存储的所有地址。
  1. 在交易中使用地址查找表:
  • 创建包含所需指令的v0交易消息。
  • 将查找表账户包含在v0交易消息中。
  • 签署并发送交易。

版本化交易的必要性

  • 使用地址查找表需要使用v0版本化交易格式。
  • v0交易支持在交易中引用查找表中的地址。
鱼雪

许多新开发者在构建应用程序时,常常会遇到与交易确认相关的问题。

本文旨在提升对Solana区块链交易确认机制的整体理解,并提供一些推荐的最佳实践。

交易的简要背景

在深入了解Solana的交易确认和过期机制之前,

让我们简要了解以下几个方面:

  • 交易是什么
  • 交易的生命周期
  • 什么是区块哈希(blockhash
  • 对历史证明(Proof of History, PoH)及其与区块哈希的关系有一个简要了解

什么是交易?

交易由两部分组成:消息和签名列表交易消息是关键,它由以下三部分组成

  • 要调用的指令列表
  • 要加载的账户列表
  • 一个最近的区块哈希

本文将重点讨论交易的最近区块哈希,因为它在交易确认中起着重要作用

交易生命周期回顾

下面是交易生命周期的高层次视图

本文将涉及除步骤1和4之外的所有内容。

  1. 创建指令列表以及指令需要读取和写入的账户列表
  2. 获取最近的区块哈希并使用它来准备交易消息
  3. 模拟交易以确保其行为符合预期
  4. 提示用户使用其私钥签署准备好的交易消息
  5. 将交易发送到RPC节点,该节点尝试将其转发给当前的区块生产者
  6. 希望区块生产者验证并将交易提交到其生成的区块中
  7. 确认交易是否已包含在区块中或检测其是否已过期

什么是区块哈希?

区块哈希指的是某个槽位slot)的最后一个历史证明(PoH)哈希。

由于Solana使用PoH作为可信的时钟交易的最近区块哈希可以看作是一个时间戳

历史证明回顾

Solana的历史证明机制使用非常长的递归SHA-256哈希链来构建可信钟。

该名称中的"历史"部分来源于区块生产者将交易 ID 哈希到流中以记录在其区块中处理了哪些交易。

PoH哈希计算公式:next_hash = hash(prev_hash, hash(transaction_ids))

由于每个哈希必须按顺序生成PoH可以用作可信的时钟

每个生成的区块包含一个区块哈希和一组称为ticks的哈希检查点,

以便验证者可以并行验证完整的哈希链,并证明确实经过了一定的时间。

交易过期

默认情况下,如果交易未在一定时间内提交到区块中,它们将会过期。 大多数交易确认问题与RPC节点和验证者如何检测和处理过期交易有关。 对交易过期工作机制的深入理解可以帮助你诊断大部分交易确认问题。

交易过期的工作原理

每笔交易包含一个最近的区块哈希,该哈希用作PoH时钟时间戳, 当该区块哈希不再足够最近时,交易就会过期

随着每个区块的最终化(即达到最大tick高度,达到区块边界), 区块的最终哈希会添加到BlockhashQueue中,该队列存储最多300个最近的区块哈希。

在交易处理中,Solana验证者将检查每笔交易的最近区块哈希 是否记录在最近的151个存储的哈希中(即最大处理年龄)。

如果交易的最近区块哈希比这个最大处理年龄更旧,则该交易不会被处理。

info

由于当前的最大处理年龄为150,加上区块哈希在队列中的年龄0开始索引, 实际上有151个区块哈希被认为是足够最近并且可以处理。

由于槽位(即验证者可以生成区块的时间段)配置为大约400毫秒, 但可能在400毫秒到600毫秒之间波动, 因此给定的区块哈希只能在大约6090秒内用于交易之后它将被运行时视为过期

交易过期示例

让我们快速演示一个例子:

  1. 验证者正在为当前槽位积极生成新块。
  2. 验证者从用户那里收到一笔包含最近区块哈希abcd...的交易。
  3. 验证者将此区块哈希abcd...BlockhashQueue中的最近区块哈希列表进行比较, 发现它是在151个区块之前创建的。
  4. 由于它正好是151个区块哈希老,因此交易尚未过期,可以处理。
  5. 但在实际处理交易之前,验证者完成了下一个区块的创建并将其添加到BlockhashQueue。 然后,验证者开始为下一个槽位生成区块(验证者可以连续生成4个槽位的区块)。
  6. 验证者再次检查同一笔交易,发现它现在有152个区块哈希老,并拒绝了它,因为它太老了:(

交易为什么会过期?

实际上,这是为了帮助验证者避免重复处理同一笔交易

一种简单的蛮力方法来防止重复处理是将每个新交易与区块链的整个交易历史记录进行比较

但通过让交易在短时间后过期,验证者只需检查新交易是否在相对较小的最近处理交易集内即可。

其他区块链

Solana防止重复处理的方法与其他区块链不同。 例如,以太坊会跟踪每个交易发送者的计数器(nonce),并且只处理使用下一个有效nonce的交易。

以太坊的方法对验证者来说实现简单,但对用户来说可能会有问题。

许多人遇到过以太坊交易长时间处于待处理状态,所有使用更高nonce值的后续交易都被阻止处理的情况。

Solana的优势

Solana的方法有以下几个优势

  • 单个费用支付者可以同时提交多个允许以任何顺序处理的交易。 如果你同时使用多个应用程序,这种情况可能会发生。
  • 如果交易未提交到区块并过期,用户可以重新尝试,知道他们之前的交易永远不会被处理。

由于不使用计数器,Solana的钱包体验对用户来说可能更容易理解, 因为他们可以快速达到成功、失败或过期状态,避免烦人的待处理状态。

Solana的劣势

当然也有一些劣势:

  • 验证者必须主动跟踪所有已处理交易ID的集合,以防止重复处理
  • 如果过期时间太短,用户可能无法在交易过期前提交交易。

这些劣势突显了交易过期配置中的权衡。

如果增加交易的过期时间验证者需要使用更多内存来跟踪更多交易。

如果减少过期时间,用户可能没有足够的时间提交交易。

目前,Solana集群要求交易使用的区块哈希不能超过151个区块。

info

这个 Github 问题包含了一些估算, 它们估计mainnet-beta验证者需要约 150MB 的内存来跟踪交易。 如果必要的话,未来可以通过简化这个过程,而不会降低交易的过期时间, 详细信息请参考该问题。

交易确认提示

正如之前提到的,区块哈希在151个区块后过期, 当槽位在目标时间400ms内处理时,这可以快到一分钟。

考虑到客户端需要获取最近的区块哈希等待用户签名

并最终希望广播的交易被愿意接受的领导者接收,一分钟并不是很多时间。

让我们来看看一些帮助避免交易因过期导致确认失败的提示!

使用适当的承诺级别获取区块哈希

鉴于短暂的过期时间框架客户端和应用程序必须帮助用户创建一个尽可能最近的区块哈希的交易

获取区块哈希(blockhash)时,目前推荐的RPC APIgetLatestBlockhash

默认情况下,该API使用已最终(finalized)确定的承诺级别返回最近的最终区块的区块哈希(blockhash)。 然而,你可以通过设置承诺(commitment)参数为不同的承诺级别来覆盖此行为。

推荐

几乎总是使用已确认(confirmed)的承诺级别进行RPC请求, 因为它通常仅比处理的(processed)承诺滞后几个槽位,并且几乎没有属于丢弃分叉的风险

但可以考虑其他选项:

  • 选择处理过的(processed)承诺将让你获取与其他承诺级别相比最新的区块哈希, 因此为你提供最多的时间来准备和处理交易。 但由于Solana区块链中分叉的普遍性,大约5%的区块最终不会被集群确认, 所以你的交易有可能使用一个属于丢弃分叉的区块哈希。 使用被废弃区块哈希的交易永远不会被最终区块视为最近。
  • 使用默认的已最终(finalized)确定的承诺级别将消除你选择的区块哈希属于丢弃分叉的任何风险。 代价是通常最新确认区块与最新最终区块之间至少有32个槽位的差异。 这种权衡非常严重,有效地减少了你的交易的过期时间大约13秒,但在集群不稳定时,这可能会更多。

使用适当的预提交级别

如果你的交易使用的区块哈希是从一个RPC节点获取的, 然后你发送或模拟该交易时使用了另一个RPC节点, 可能会由于一个节点落后于另一个节点而出现问题。

当RPC节点接收到sendTransaction请求时, 它们将尝试使用最近的最终区块或预检承诺(preflightCommitment)参数 选择的区块来确定你的交易的过期块。 一个非常常见的问题是,接收到的交易的区块哈希是在用于计算该交易过期时间的区块之后生成的。 如果RPC节点无法确定你的交易何时过期,它将只转发你的交易一次,然后将其丢弃。

类似地,当RPC节点接收到simulateTransaction请求时, 它们将使用最近的最终区块或预检承诺(preflightCommitment)参数选择的区块模拟你的交易。 如果选择用于模拟的区块比用于你的交易区块哈希的区块更旧, 模拟将失败,并出现找不到区块哈希的错误。

推荐

即使你使用skipPreflight,始终将预检承诺(preflightCommitment)参数 设置为用于获取交易区块哈希的相同承诺级别, 用于sendTransactionsimulateTransaction请求。

发送交易时警惕滞后的RPC节点

当你的应用程序使用RPC池服务或创建交易和发送交易时使用不同的RPC端点时, 你需要警惕一个RPC节点落后于另一个RPC节点的情况

例如, 如果你从一个RPC节点获取交易区块哈希, 然后将该交易发送到第二个RPC节点进行转发或模拟, 第二个RPC节点可能落后于第一个。

推荐

对于sendTransaction请求,客户端应频繁地将交易重新发送到RPC节点, 以便如果RPC节点略微落后于集群,它最终会赶上并正确检测你的交易过期时间。

对于simulateTransaction请求,客户端应使用replaceRecentBlockhash参数, 告诉RPC节点用一个始终有效的区块哈希替换模拟交易的区块哈希

避免重用过期的区块哈希

即使你的应用程序获取了一个非常新的区块哈希, 也要确保不会在交易中重用该区块哈希太久。

理想情况下,应在用户签署交易前立即获取最近的区块哈希

对应用程序的建议

频繁轮询新的最近区块哈希,以确保每当用户触发创建交易的操作时, 你的应用程序已经有一个准备好的新鲜区块哈希。

对钱包的建议

频繁轮询新的最近区块哈希,并在用户签署交易前立即替换交易的最近区块哈希,以确保区块哈希尽可能新。

使用健康的RPC节点获取区块哈希

通过从RPC节点使用已确认的承诺级别获取最新区块哈希,它会响应最近确认区块的区块哈希。 Solana的区块传播协议优先将区块发送到质押节点,因此RPC节点自然会落后于集群其他部分大约一个区块。 在处理应用程序请求时,它们还需要做更多的工作,并在用户流量繁忙时可能会落后更多。

滞后的RPC节点因此可能会响应getLatestBlockhash请求,返回集群确认的区块哈希, 但实际上该区块哈希即将过期。

默认情况下,如果滞后的RPC节点检测到它比集群落后超过150个槽位, 将停止响应请求,但在达到该阈值之前,它们仍可能返回一个即将过期的区块哈希

推荐

监控RPC节点的健康状况,以确保它们具有最新的集群状态视图,可以通过以下方法之一实现:

  1. 使用getSlot RPC API(使用处理过的承诺级别)获取RPC节点的最高处理槽位, 然后调用getMaxShredInsertSlot RPC API获取RPC节点接收到区块的碎片最高槽位。 如果这些响应之间的差异很大,说明集群生成的区块远远超过了RPC节点处理的区块。
  2. 使用已确认的承诺级别调用getLatestBlockhash RPC API, 在几个不同的RPC API节点上,使用返回的最高槽位的节点的区块哈希。

等待足够长的时间以确保过期

推荐

调用getLatestBlockhash RPC API获取交易的最近区块哈希时, 记录响应中的lastValidBlockHeight

然后,以已确认的承诺级别轮询getBlockHeight RPC API直到其返回的区块高度大于先前返回的最后有效区块高度

考虑使用持久交易

有时交易过期问题真的很难避免(例如,离线签名、集群不稳定)。 如果之前的提示仍然不足以满足你的用例,可以切换到使用持久交易(它们只需要一些设置)。

要开始使用持久交易, 用户首先需要提交一个调用指令的交易, 创建一个特殊的链上nonce账户,并在其中存储一个“持久区块哈希”。 在未来的任何时候(只要该nonce账户尚未使用),

用户可以通过以下两条规则创建持久交易

  1. 指令列表必须以advance nonce系统指令开头,该指令会加载他们的链上nonce账户。
  2. 交易的区块哈希必须等于链上nonce账户存储的持久区块哈希。

以下是Solana运行时如何处理这些持久交易的:

  • 如果交易的区块哈希不再最近,运行时会检查交易的指令列表是否以advance nonce系统指令开头。
  • 如果是,则加载advance nonce指令指定的nonce账户
  • 然后检查存储的持久区块哈希是否与交易的区块哈希匹配。
  • 最后,确保将nonce账户的存储区块哈希推进到最近的区块哈希,以确保同一交易永远不会被再次处理。

要点总结

交易及其组成部分

  • 交易:包含消息和签名列表。
  • 消息:由指令列表、账户列表和最近的区块哈希组成。
  • 区块哈希:是某个槽位的最后一个历史证明(PoH)哈希,作为交易的时间戳。

交易生命周期

  1. 创建指令列表和账户列表。
  2. 获取最近的区块哈希。
  3. 模拟交易。
  4. 提示用户签名。
  5. 发送交易到RPC节点。
  6. 区块生产者验证并提交交易。
  7. 确认交易是否包含在区块中或是否已过期。

交易过期机制

  • 默认过期时间:区块哈希在151个槽位(约60-90秒)内有效。
  • 区块哈希队列:最多存储300个最近的区块哈希。
  • 验证者检查:验证交易的区块哈希是否在最近的151个区块哈希中。
  • 目的:避免重复处理交易。

提示和建议

  • 使用适当的承诺级别获取区块哈希:推荐使用已确认的承诺级别。
  • 确保预检承诺级别一致:使用获取区块哈希时相同的承诺级别进行交易发送和模拟。
  • 频繁发送交易:确保RPC节点最终检测到交易过期时间。
  • 避免重用过期的区块哈希:获取新的区块哈希并立即使用。
  • 监控RPC节点健康状况:确保其具有最新的集群状态视图。
  • 等待足够长的时间确认交易过期:记录lastValidBlockHeight并监控区块高度。

持久交易

  • 设置:创建一个特殊的链上nonce账户
  • 持久区块哈希:存储在nonce账户中,用于创建不易过期的交易。
  • 规则:交易需以advance nonce指令开头,区块哈希需匹配nonce账户存储的哈希。

通过理解和应用这些要点,可以有效处理Solana区块链上的交易确认与过期问题

鱼雪

重试交易

有时,一个看似有效的交易可能在被包含在区块之前被丢弃。 这通常发生在网络拥堵期间,当RPC节点未能将交易重新广播给领导者时。

对于终端用户来说,这可能看起来像他们的交易完全消失了。 虽然RPC节点配备了通用的重新广播算法,但应用程序开发人员也可以开发自己的自定义重新广播逻辑。

要点概述(TLDR)

  • RPC节点将尝试使用通用算法重新广播交易。
  • 应用程序开发人员可以实现自己的自定义重新广播逻辑。
  • 开发人员应利用sendTransaction JSON-RPC方法中的maxRetries参数。
  • 开发人员应启用预检检查,以在提交交易前引发错误。
  • 重新签署任何交易之前,确保初始交易的区块哈希已过期非常重要。

交易的旅程

客户端如何提交交易

在Solana中,没有mempool的概念。

所有交易,无论是通过编程方式还是由终端用户发起的, 都会有效地路由到领导者,以便处理并纳入区块。

有两种主要方式可以将交易发送给领导者

  1. 通过RPC服务器和sendTransaction JSON-RPC方法代理发送。
  2. 直接通过TPU客户端发送给领导者。

绝大多数终端用户将通过RPC服务器提交交易。

当客户端提交交易时,接收的RPC节点将尝试将交易广播给当前和下一个领导者。

在交易被领导者处理之前,除了客户端和中继RPC节点所知道的之外,没有任何交易记录。

对于TPU客户端,重新广播和领导者转发完全由客户端软件处理。

交易历程概览,从客户端到领袖

RPC节点如何广播交易

RPC节点通过sendTransaction接收到交易后, 会将交易转换为UDP数据包, 然后再转发给相关的领导者。

UDP允许验证者快速互相通信,但不保证交易传递。

由于Solana的领导者时间表在每个时期(epoch)(大约2天)之前就已知, 因此RPC节点会将交易直接广播给当前和下一个领导者。 这与其他如以太坊的gossip协议不同,后者随机且广泛地传播交易。 默认情况下,RPC节点将尝试每两秒钟将交易转发给领导者, 直到交易被最终处理或交易的区块哈希过期(150个区块或约119秒)。 如果未处理的重新广播队列大小超过10,000笔交易,新提交的交易将被丢弃。 RPC操作员可以通过命令行参数调整此重试逻辑的默认行为。

当RPC节点广播交易时, 它将尝试将交易转发给领导者的交易处理单元TPU)。

TPU在五个不同阶段处理交易

  1. Fetch阶段
  2. SigVerify阶段
  3. Banking阶段
  4. 历史证明(PoH)服务
  5. 广播阶段

交易处理阶段

在这五个阶段中,Fetch阶段负责接收交易

在Fetch阶段验证者会根据三个端口对传入交易进行分类:

  • tpu处理常规交易,如代币转移、NFT铸造和程序指令。
  • tpu_vote专门处理投票交易。
  • tpu_forwards在当前领导者无法处理所有交易时,将未处理的数据包转发给下一个领导者。

有关TPU的更多信息,请参阅Jito Labs的优秀文章。

交易如何被丢弃

在交易的旅程中,有几种情况下交易可能会无意中从网络中丢失。

在交易处理之前

如果网络丢弃交易,很可能在交易被领导者处理之前就发生。 UDP数据包丢失是最简单的原因。

在网络负载过高时,验证者也可能因需要处理的大量交易而不堪重负。 虽然验证者配备了通过tpu_forwards转发多余交易的功能,但可转发的数据量有限。

此外,每次转发仅限于验证者之间的一跳。 也就是说,通过tpu_forwards端口接收的交易不会进一步转发给其他验证者。

还有两个不太为人知的原因也可能导致交易在处理之前被丢弃。

  • 第一种情况涉及通过RPC池提交的交易。 有时,RPC池的一部分可能显著领先于其余部分。 当池中的节点需要协同工作时,这可能会导致问题。 在此示例中,交易的recentBlockhash从池的先进部分(后端A)查询。 当交易提交到池的滞后部分(后端B)时,节点将无法识别高级的区块哈希并丢弃交易。 如果开发人员在sendTransaction上启用预检检查,可以在交易提交时检测到这一点。 通过 RPC 池丢失的交易
  • 临时的网络分叉也可能导致交易丢弃。 如果验证者在银行阶段慢于重放其区块,它可能会创建一个少数分叉。 当客户端构建交易时,交易可能引用仅存在于少数分叉中的recentBlockhash。 在交易提交后,集群可能会在交易处理前切换离开其少数分叉。 在这种情况下,由于找不到区块哈希,交易将被丢弃。 由于少数分支导致交易丢失(在处理之前)

交易处理后且在最终确认之前

如果交易引用了少数分叉的recentBlockhash,交易仍可能被处理。 然而,此时交易将由少数分叉的领导者处理。 当该领导者尝试将其处理的交易与网络其他部分共享时,它将无法与不识别少数分叉的大多数验证者达成共识。 在此时,交易将在最终确认之前被丢弃。 由于少数分叉(处理后)而丢弃的交易

处理丢弃的交易

虽然RPC节点将尝试重新广播交易,但他们使用的算法是通用的,通常不适合特定应用的需求。 为应对网络拥堵,应用程序开发人员应定制自己的重新广播逻辑。

深入了解sendTransaction

提交交易时,sendTransaction RPC方法是开发人员可用的主要工具。 sendTransaction仅负责将交易从客户端中继到RPC节点。 如果节点接收到交易,sendTransaction将返回可用于跟踪交易的交易ID。 成功响应并不表示交易将被集群处理或最终确认。

请求参数

  • transaction: string - 完全签名的交易,以编码字符串形式
  • (可选)configuration object: object
  • skipPreflight: boolean - 如果为true,跳过预检交易检查(默认值:false
  • (可选)preflightCommitment: string - 用于预检模拟的承诺级别,针对银行槽(默认值:"finalized")。
  • (可选)encoding: string - 用于交易数据的编码。可以是"base58"(慢)或"base64"(默认值:"base58")。
  • (可选)maxRetries: usize - RPC节点重试将交易发送给领导者的最大次数。 如果未提供此参数,RPC节点将重试交易直到交易被最终处理或区块哈希过期。

响应

  • transaction id: string - 交易中嵌入的第一个交易签名,以base-58编码字符串形式。 可以使用此交易ID通过getSignatureStatuses轮询状态更新。

自定义重新广播逻辑

为了开发自己的重新广播逻辑,开发人员应利用sendTransactionmaxRetries参数。

如果提供,maxRetries将覆盖RPC节点的默认重试逻辑,允许开发人员在合理范围内手动控制重试过程。

手动重试交易的常见模式涉及临时存储从getLatestBlockhash获取的lastValidBlockHeight。 一旦存储,应用程序可以轮询集群的区块高度,并在适当的间隔手动重试交易。 在网络拥堵时,设置maxRetries0并通过自定义算法手动重新广播是有利的。 虽然一些应用程序可能使用指数退避算法, 但其他如Mango的应用程序选择在某个超时发生前持续以恒定间隔重新提交交易。

示例代码展示了如何实现这一过程:

import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";

const sleep = async (ms: number) => {
return new Promise(r => setTimeout(r, ms));
};

(async () => {
const payer = Keypair.generate();
const toAccount = Keypair.generate().publicKey;

const connection = new Connection("http://127.0.0.1:8899", "confirmed");

const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);

await connection.confirmTransaction({ signature: airdropSignature });

const blockhashResponse = await connection.getLatestBlockhashAndContext();
const lastValidBlockHeight = blockhashResponse.context.slot + 150;

const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.value.blockhash,
lastValidBlockHeight: lastValidBlockHeight,
}).add(


SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount,
lamports: 1000000,
}),
);
const message = transaction.serializeMessage();
const signature = nacl.sign.detached(message, payer.secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
const rawTransaction = transaction.serialize();
let blockheight = await connection.getBlockHeight();

while (blockheight < lastValidBlockHeight) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
});
await sleep(500);
blockheight = await connection.getBlockHeight();
}
})();

在通过getLatestBlockhash轮询时,应用程序应指定其预期的承诺级别。

通过将其承诺级别设置为confirmed(已投票)或finalized(在confirmed之后约30个区块), 应用程序可以避免轮询少数分叉的区块哈希。

如果应用程序可以访问负载均衡器后的RPC节点,它还可以选择将工作负载分配给特定节点。 处理数据密集型请求(如getProgramAccounts)的RPC节点可能会落后,并且可能不适合同时转发交易。 对于处理时间敏感交易的应用程序,可能需要有专门的节点仅处理sendTransaction

跳过预检的成本

默认情况下,sendTransaction将在提交交易之前执行三个预检检查。

具体而言,sendTransaction将:

  1. 验证所有签名是否有效。
  2. 检查引用的区块哈希是否在最近150个区块内。
  3. 在预检承诺级别指定的银行槽上模拟交易。

如果这些预检检查中的任何一项失败,sendTransaction将在提交交易之前引发错误。

预检检查通常是防止交易丢失和允许客户端优雅处理错误之间的区别。

为了确保考虑到这些常见错误,建议开发人员将skipPreflight保持为false。

何时重新签署交易

尽管尝试重新广播,但有时客户端可能需要重新签署交易。 在重新签署任何交易之前,确保初始交易的区块哈希已过期非常重要。 如果初始区块哈希仍然有效,可能会导致网络接受两个交易。 对于终端用户来说,这似乎是他们无意中发送了相同的交易两次。

在Solana中,一旦交易引用的区块哈希比从getLatestBlockhash获得的lastValidBlockHeight更旧, 丢弃的交易就可以安全地被丢弃。

开发人员应通过查询getEpochInfo并与响应中的blockHeight进行比较来跟踪此lastValidBlockHeight。 一旦区块哈希失效,客户端可以重新签署一个新查询的区块哈希。

总结

重试交易要点总结

  1. 交易丢失原因
  • 网络拥堵时,RPC节点可能未能重新广播交易。
  • UDP数据包丢失或验证者因过载未能处理所有交易。
  • 临时网络分叉或RPC池部分节点滞后导致交易丢失。
  1. RPC节点重试逻辑
  • RPC节点使用通用算法每两秒重新广播交易,直到交易被处理或区块哈希过期(约119秒)。
  • 如果重试队列超过10,000笔交易,新交易将被丢弃。
  • RPC节点可以通过命令行参数调整重试逻辑。
  1. 自定义重试逻辑
  • 开发人员可以利用sendTransaction方法中的maxRetries参数,手动控制重试过程。
  • 使用自定义算法在网络拥堵时手动重新广播交易,避免重复提交。
  1. 预检检查
  • sendTransaction默认执行预检检查,包括签名验证、区块哈希检查和交易模拟。
  • 保持skipPreflightfalse,以防止常见错误和交易丢失。
  1. 重新签署交易
  • 在重新签署交易之前,确保初始交易的区块哈希已过期,避免重复交易。
  • 通过查询getEpochInfo并比较响应中的区块高度,跟踪lastValidBlockHeight
  1. 交易处理过程
  • 交易通过RPC服务器或TPU客户端提交。
  • RPC节点将交易转换为UDP数据包并广播给当前和下一个领导者。
  • 交易在TPU的五个阶段处理:FetchSigVerifyBanking、Proof of History ServiceBroadcast
  1. 示例代码实现
    • 使用JavaScript和Solana web3.js库实现手动重试交易逻辑,确保在区块哈希过期前持续重试。

通过这些要点,开发人员可以更好地理解和处理交易重试,确保在网络拥堵和其他异常情况下交易的可靠性。

鱼雪

在Solana上,状态压缩是一种创建链下数据指纹(fingerprint)(或哈希值) 并将此指纹存储在链上以进行安全验证的方法

有效地利用Solana分类账的安全性来安全验证链下数据,确保其未被篡改。

这种压缩方法允许Solana程序dApps使用廉价的区块链分类账空间, 而不是更昂贵的账户空间来安全存储数据

这是通过使用一种特殊的二叉树结构(称为并发默克尔树)来实现的,

该结构为每个数据片段(称为叶子)创建一个哈希值将它们一起哈希,并仅将最终哈希值存储在链上

什么是状态压缩?

简单来说,状态压缩使用“树”结构 以确定性方式将链下数据加密哈希在一起计算出一个单一的最终哈希值并将其存储在链上

这些树是在这种“确定性”过程中创建的:

  1. 取任意数据片段
  2. 创建该数据的哈希值
  3. 将此哈希值作为树底部的叶子(leaf)存储
  4. 然后将每对叶子(leaf)哈希在一起,创建一个分支(branch)
  5. 然后将每个分支(branch)哈希在一起
  6. 不断向上攀登树并将相邻的分支哈希在一起
  7. 一旦到达树顶,生成最终的根哈希(root hash)

这个根哈希(root hash)然后存储在链上,作为所有叶子数据的可验证证明。 允许任何人加密验证树内的所有链下数据,同时实际上只在链上存储最少量的数据

因此,由于这种状态压缩显著降低了存储/证明大量数据的成本

默克尔树和并发默克尔树

Solana的状态压缩使用了一种特殊类型的默克尔树

允许对任何给定树进行多次更改

同时仍保持树的完整性和有效性

这种特殊的树称为并发默克尔树它有效地在链上保留了树的“变更日志”允许对同一棵树进行多次快速更改(即,在同一个区块中),在证明无效之前。

什么是默克尔树?

默克尔树,有时称为哈希树,是一种基于哈希的二叉树结构

其中每个叶节点表示其内部数据的加密哈希值

而每个非叶节点(称为分支)表示其子叶哈希的哈希值

然后每个分支也被哈希在一起,攀登树,直到最终只剩下一个单一的哈希值

这个最终的哈希值称为根哈希或“根”, 可以与证明路径结合使用来验证存储在叶节点中的任何数据片段

一旦计算出最终的根哈希(root hash), 存储在叶节点中的任何数据片段都可以通过重新哈希特定叶的数据和攀登树的每个相邻分支的 哈希标签(称为证明或证明路径)来验证。

将这个重新哈希与根哈希进行比较就是对基础叶数据的验证

如果它们匹配,则数据被验证为准确。

如果不匹配,则叶数据已更改。

只要需要, 原始叶数据可以通过简单地对新叶数据进行散列并以与原始根相同的方式重新计算根散列来更改

然后,使用此新根哈希来验证任何数据,并有效地使先前的根哈希和先前的证明失效。

因此,对这些传统 Merkle 树进行的每次更改都必须按顺序执行。

info

当使用默克尔树时,更改叶数据并计算新的根哈希的过程可能是非常常见的事情!

虽然这是树的设计重点之一,但也可能导致其中一个最显著的缺点:快速变化

什么是并发默克尔树?

在高吞吐量应用中,例如在Solana运行时环境中, 验证者可能会相对快速地接收到更改链上传统默克尔树的请求(例如,在同一个插槽中)。

每次叶数据更改仍需按顺序执行

导致每个后续更改请求失败,因为根哈希和证明已被插槽中的先前更改请求无效化。

引入并发默克尔树

并发默克尔树在链上的每棵树特定账户存储了最近更改其根哈希及其派生证明安全变更日志。 这种变更日志缓冲区具有最大数量的变更日志“记录”(即最大缓冲区大小)。

验证者同一插槽中接收到多个叶数据更改请求时, 链上的并发默克尔树可以使用这个“变更日志缓冲区”作为更可接受证明的来源。

有效地允许同一棵树在同一插槽中进行最多最大缓冲区大小次更改。显著提高吞吐量

并发默克尔树大小调整

创建这些链上树时, 有3个值将决定您的树的大小、创建成本和对您的树的并发更改数量

  • 最大深度
  • 最大缓冲区大小
  • 树冠深度

最大深度

树的最大深度是从任何数据叶到树根的最大跳数。

由于默克尔树是二叉树,每个叶子仅连接到另一个叶子;作为一个叶子对存在。

因此,最大深度用于确定使用简单计算存储在树中的最大节点数(即数据片段或叶子):

nodes_count = 2 ^ maxDepth

由于必须在创建树时设置深度,因此您必须决定希望您的树存储多少数据片段

然后使用上述简单计算,您可以确定存储数据所需的最低最大深度(maxDepth)。

示例1:铸造100NFT

如果您希望创建一棵树来存储100个压缩NFT,我们需要至少100个叶子100个节点

// maxDepth=6 -> 64 nodes
2^6 = 64

// maxDepth=7 -> 128 nodes
2^7 = 128

我们必须使用最大深度(maxDepth)7以确保我们可以存储所有数据。

示例2:铸造15000NFT

如果您希望创建一棵树来存储15000个压缩NFT,我们需要至少15000个叶子15000个节点

// maxDepth=13 -> 8192 nodes
2^13 = 8192

// maxDepth=14 -> 16384 nodes
2^14 = 16384

我们必须使用最大深度14以确保我们可以存储所有数据。

更高的最大深度意味着更高的成本

最大深度将是创建树时成本的主要驱动因素之一,因为您将在创建时支付此成本。

最大深度越高,可以存储的数据指纹(即哈希)越多,成本越高

最大缓冲区大小

最大缓冲区大小实际上是指在根哈希仍然有效的情况下,树上可以发生的最大更改次数

由于根哈希实际上是所有叶数据的单一哈希, 更改任何单个叶会使常规树所有后续尝试更改任何叶所需的证明无效。

但对于并发树,有效地存在这些证明更新的变更日志。 此变更日志缓冲区在创建时通过此maxBufferSize值设置和调整大小

树冠深度

树冠深度,有时称为树冠大小是为任何给定证明路径缓存/存储在链上的证明节点数量

在对叶子(leaf)执行更新操作时,如转移所有权(例如销售压缩NFT), 必须使用完整证明路径来验证叶子的原始所有权,从而允许更新操作。

此验证通过使用完整证明路径正确计算当前根哈希

(或通过链上的并发缓冲区缓存的任何根哈希)来执行。

树的最大深度越大,执行此验证所需的证明节点越多。

例如,如果您的最大深度为14,则需要14个总证明节点来进行验证。

随着树变大,完整证明路径变长

通常,每个这些证明节点都需要包含在每次更新交易中。

由于每个证明节点值占用32字节交易空间(类似于提供公钥), 较大的树很快就会超过最大交易大小限制

引入了树冠

树冠允许在链上存储一定数量的证明节点(对于任何给定证明路径)。

从而减少每次更新交易中需要包含的证明节点数量,因此保持整体交易大小低于限制。

例如,最大深度为14的树需要14个总证明节点。 具有10个树冠,仅需4个证明节点提交每次更新交易。

树冠深度值越大,成本越高

canopyDepth 值也是创建树时成本的主要因素, 因为您将在树创建时支付此成本。

顶层深度越高,在链上存储的数据证明节点就越多,成本也越高

较小的树冠限制组合性

尽管树木的创建成本随着树冠高度的提高而增加,但较低的树冠深度将要求在每个更新事务中包含更多的证明节点。

要求提交更多节点,事务大小就越大,因此很容易超出事务大小限制。

这也将适用于任何其他尝试与您的树/叶子进行交互的 Solana 程序或 dApp。 如果您的树需要太多的证明节点(因为树的深度较低),那么任何其他的链上程序可能提供的额外操作, 将受到其特定指令大小加上您的证明节点列表大小的限制。

这将限制可组合性,并为您的特定树提供潜在的附加效用。

例如,如果您的树用于压缩NFT并且具有非常低的树冠深度, NFT市场可能只能支持简单的NFT转移,而不能支持链上竞标系统。

创建树的成本

创建并发 Merkle 树成本取决于树的大小参数

  • maxDepth
  • maxBufferSize
  • canopyDepth

这些值都用于计算在链上存储(以字节为单位)所需的树存在的空间

一旦计算出所需的空间(以字节为单位)

并使用 getMinimumBalanceForRentExemption RPC 方法

请求分配这些字节所需的费用(以 lamports 为单位)到链上。

在JavaScript中计算树成本

开发人员可以使用@solana/spl-account-compression软件包中的 getConcurrentMerkleTreeAccountSize函数来计算给定树大小参数所需的空间

然后使用getMinimumBalanceForRentExemption函数 来获取在链上为树分配所需空间的最终成本(以lamports计算)。

然后确定用多少`lamports`成本使得这个大小的帐户免除租金,类似于任何其他帐户的创建。

// 计算树所需的空间
const requiredSpace = getConcurrentMerkleTreeAccountSize(
maxDepth,
maxBufferSize,
canopyDepth,
);

// 获取在链上存储树的成本(以 `lamports` 计)
const storageCost =
await connection.getMinimumBalanceForRentExemption(requiredSpace);

示例成本

下面列出了几个示例成本,涵盖不同树大小,以及每个树可能有多少叶节点:

示例 #1:16,384个节点,费用为0.222 SOL

  • 最大深度14,最大缓冲区大小为64
  • 最大叶节点数量16,384
  • 0层树冠创建成本约为0.222 SOL。

示例#2:16384个节点成本为1.134 SOL

  • 最大深度14,最大缓冲区大小为64
  • 叶节点的最大数量为16384
  • 层高11的树冠大约需要花费1.134 SOL来创建。

示例#3:1,048,576个节点,成本为1.673 SOL

  • 最大深度20,最大缓冲区大小为256
  • 最大叶子节点数1,048,576
  • 树冠深度10,大约要花费1.673 SOL来创建。

示例#4:拥有1,048,576个节点,成本为15.814 SOL

  • 最大深度20,最大缓冲区大小为256
  • 叶子节点的最大数量1,048,576
  • 15树冠深度创建大约需要15.814 SOL

压缩的NFTs

压缩的NFTs是Solana上状态压缩的最受欢迎的用例之一。

通过压缩,一个一百万NFT收藏品可以以约50 SOL铸币,而其未压缩等效收藏品需要约12,000 SOL

如果您有兴趣自己创建压缩的NFTs,请阅读我们的开发者指南,了解如何铸造和转移压缩的NFTs。

总结

以下是本文中的要点总结:

状态压缩

  • 定义:状态压缩在Solana上是一种通过创建链下数据的指纹(哈希值)并将其存储在链上进行安全验证的方法。
  • 目的:利用Solana分类账的安全性来验证链下数据,确保其未被篡改,同时节省区块链分类账空间。
  • 实现方式:使用并发默克尔树结构,将每个数据片段(叶子)创建哈希值,最终生成一个根哈希并存储在链上。

什么是状态压缩?

  • 过程
    1. 取任意数据片段
    2. 创建该数据的哈希值
    3. 将哈希值作为叶子存储
    4. 将每对叶子哈希在一起生成分支
    5. 不断向上攀登树,直到生成最终的根哈希
  • 优势:显著降低存储和证明大量数据的成本。

默克尔树和并发默克尔树

  • 默克尔树
    • 基于哈希的二叉树结构,每个叶节点表示其数据的哈希值,每个分支节点表示其子叶哈希的哈希值。
    • 最终生成一个根哈希,用于验证存储在叶节点中的数据。
  • 并发默克尔树
    • 允许对同一棵树进行多次更改,保持树的完整性和有效性。
    • 在链上保留变更日志,允许在同一插槽中进行多次快速更改,提高吞吐量。

并发默克尔树大小调整

  • 决定因素
    • 最大深度:从叶子到树根的最大跳数,决定存储的数据片段数量。
    • 最大缓冲区大小:变更日志缓冲区的最大记录数。
    • 树冠深度:影响树的创建成本和并发更改数量。

示例

  • 铸造100个NFT:需要至少100个叶子或节点,计算出所需的最小最大深度。

通过这些要点总结,可以更好地理解Solana上的状态压缩技术及其实现方式和优势。

鱼雪

版本化交易是新的交易格式,允许在Solana运行时中添加功能,包括地址查找表。 虽然链上程序无需更改即可支持版本化交易的新功能(或保持向后兼容性), 但开发人员需要更新客户端代码,以防止由于不同交易版本导致的错误。

当前交易版本

Solana运行时支持两种交易版本

  • legacy - 较旧的交易格式,没有额外的好处
  • 0 - 添加了对地址查找表的支持

支持的最大交易版本

所有返回交易的RPC请求都应使用maxSupportedTransactionVersion选项指定应用程序支持的最高交易版本, 包括getBlockgetTransaction

如果返回的版本化交易高于设置的maxSupportedTransactionVersion, RPC请求将失败(例如,如果选择了legacy版本但返回了0版本的交易)。

warning

如果未设置maxSupportedTransactionVersion值,则RPC响应中只允许legacy交易。 因此,如果返回任何0版本交易,您的RPC请求将失败。

如何设置支持的最大版本

您可以使用@solana/web3.js库和直接发送到RPC端点的JSON格式请求来设置maxSupportedTransactionVersion

使用web3.js

使用@solana/web3.js库,您可以获取最近的区块或特定交易

// 连接到`devnet`集群并获取当前`slot`
const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
const slot = await connection.getSlot();

// 获取最新的区块(允许v0交易)
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});

// 获取特定交易(允许v0交易)
const getTx = await connection.getTransaction(
"3jpoANiFeVGisWRY5UP648xRXs3iQasCHABPWRWnoEjeA93nc79WrnGgpgazjq4K9m8g2NJoyKoWBV1Kx5VmtwHQ",
{
maxSupportedTransactionVersion: 0,
},
);

向RPC发送JSON请求

使用标准的JSON格式POST请求,您可以在检索特定区块时设置maxSupportedTransactionVersion

curl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d \
'{"jsonrpc": "2.0", "id":1, "method": "getBlock", "params": [430, {
"encoding":"json",
"maxSupportedTransactionVersion":0,
"transactionDetails":"full",
"rewards":false
}]}'

如何创建版本化交易

版本化交易的创建方式与旧方法类似,但在使用某些库时有些不同。

以下是使用@solana/web3.js库创建版本化交易的示例,用于在两个账户之间进行SOL转账。

注意事项

  • payer是一个有效的Keypair钱包,已经有SOL资金
  • toAccount是一个有效的Keypair

首先,导入web3.js库并创建到所需集群的连接(connection)。

然后定义我们将需要的最近区块哈希(blockhash)和最小租金(minRent):

const web3 = require("@solana/web3.js");

// 连接到集群并获取租金豁免状态的最低租金
const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
let minRent = await connection.getMinimumBalanceForRentExemption(0);
let blockhash = await connection
.getLatestBlockhash()
.then(res => res.blockhash);

创建一个包含所有所需指令(instructions)的数组(array)。

在下面的示例中,我们创建一个简单的SOL转账指令:

// 创建一个包含所需指令的数组
const instructions = [
web3.SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount.publicKey,
lamports: minRent,
}),
];

接下来,使用所需的指令构建一个MessageV0格式的交易消息

// 创建v0兼容的消息
const messageV0 = new web3.TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();

然后,创建一个新的VersionedTransaction,传入我们的v0兼容消息:

const transaction = new web3.VersionedTransaction(messageV0);

// 使用所需的签名者签署交易
transaction.sign([payer]);

您可以通过以下方式签署交易

  • 将签名数组传递给VersionedTransaction方法,或
  • 调用transaction.sign()方法,传递所需签名者的数组
info

调用transaction.sign()方法后,所有先前的交易签名将被提供的签名者的新签名完全替换

在所有必要账户对您的 VersionedTransaction 进行签名之后, 您可以将其发送到集群并等待(await)响应:

// 发送我们的v0交易到集群
const txId = await connection.sendTransaction(transaction);
console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
note

legacy交易不同, 通过sendTransaction发送版本化交易不支持通过传递签名者数组作为第二个参数进行交易签名。 您需要在调用connection.sendTransaction()之前签署交易。

版本化交易总结

版本化交易是Solana的新交易格式, 提供了包括地址查找表在内的附加功能。 开发者需要更新客户端代码,以避免因不同交易版本导致的错误。

  1. 当前交易版本 Solana运行时支持两种交易版本:
  • legacy:旧的交易格式,没有额外的功能。
  • 0:新增了对地址查找表的支持。
  1. 设置最大支持的交易版本 在所有返回交易的RPC请求中,应使用maxSupportedTransactionVersion选项指定应用程序支持的最高交易版本。
  • 如果返回的交易版本高于设置的maxSupportedTransactionVersion,RPC请求将失败。
  • 如果未设置maxSupportedTransactionVersion值,则RPC响应中只允许legacy交易。
  1. 设置方法 通过@solana/web3.js库或直接向RPC端点发送JSON格式的请求来设置maxSupportedTransactionVersion
鱼雪