Solana 账户模型

在 Solana 上,所有数据都存储在所谓的“账户”中。您可以将 Solana 上的数据视为一个公共数据库,其中只有一个“账户”表,该表中的每个条目都是一个“账户”。每个 Solana 账户都共享相同的基础 账户类型

账户账户

关键点

  • 账户最多可以存储 10MiB 的数据,这些数据可以是可执行的程序代码或程序状态。
  • 账户需要支付与存储数据量成比例的 租金押金(以 lamports,即 SOL 计),当您关闭账户时,可以完全取回押金。
  • 每个账户都有一个程序 所有者。只有拥有账户的程序可以更改其数据或扣除其 lamport 余额,但任何人都可以增加余额。
  • Sysvar 账户 是存储网络集群状态的特殊账户。
  • 程序账户 存储智能合约的可执行代码。
  • 数据账户 是由程序创建的,用于存储和管理程序状态。

账户

Solana 上的每个账户都有一个唯一的 32 字节地址,通常显示为 base58 编码的字符串(例如 14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5)。

账户与其地址之间的关系类似于键值对,其中地址是定位账户对应链上数据的键。账户地址充当“账户表”中每个条目的“唯一 ID”。

账户地址账户地址

大多数 Solana 账户使用 Ed25519 公钥作为其地址。

import { generateKeyPairSigner } from "@solana/kit";
// Kit does not enable extractable private keys
const keypairSigner = await generateKeyPairSigner();
console.log(keypairSigner);
Click to execute the code.

虽然公钥通常用作账户地址,但 Solana 还支持一种称为程序派生地址 (PDAs) 的功能。PDAs 是可以从程序 ID 和可选输入(种子)确定性派生的特殊地址。

import { Address, getProgramDerivedAddress } from "@solana/kit";
const programAddress = "11111111111111111111111111111111" as Address;
const seeds = ["helloWorld"];
const [pda, bump] = await getProgramDerivedAddress({
programAddress,
seeds
});
console.log(`PDA: ${pda}`);
console.log(`Bump: ${bump}`);
Click to execute the code.

账户类型

账户的最大大小为 10MiB,并且 Solana 上的每个账户都共享相同的基础 账户 类型。

账户类型账户类型

每个 Solana 账户都有以下字段:

  • data:一个字节数组,用于存储账户的任意数据。对于不可执行的账户,这通常存储要读取的状态。对于程序账户(智能合约),这包含可执行的程序代码。数据字段通常称为“账户数据”。
  • executable:此标志显示账户是否为程序。
  • lamports:账户的余额,以 lamports 为单位,lamports 是 SOL 的最小单位(1 SOL = 10 亿 lamports)。
  • owner:拥有此账户的程序的程序 ID(公钥)。只有拥有者程序可以更改账户的数据或扣减其 lamports 余额。
  • rent_epoch:一个遗留字段,源自 Solana 曾经有一个机制定期从账户中扣除 lamports。虽然此字段仍然存在于账户类型中,但自从租金收取被弃用后,它已不再使用。
Base Account Type
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}
import {
airdropFactory,
createSolanaRpc,
createSolanaRpcSubscriptions,
generateKeyPairSigner,
lamports
} from "@solana/kit";
// Create a connection to Solana cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate a new keypair
const keypair = await generateKeyPairSigner();
console.log(`Public Key: ${keypair.address}`);
// Funding an address with SOL automatically creates an account
const signature = await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: keypair.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed"
});
const accountInfo = await rpc.getAccountInfo(keypair.address).send();
console.log(accountInfo);
Click to execute the code.

租金

要在链上存储数据,账户还必须保持与账户中存储的数据量(以字节为单位)成比例的 lamport (SOL) 余额。这个余额被称为“租金”,但它更像是一种押金,因为当您关闭账户时可以取回全额金额。您可以在这里使用这些常量找到计算方法。

“租金”一词来源于一种已弃用的机制,该机制会定期从低于租金阈值的账户中扣除 lamport。这种机制现在已经不再使用。

程序所有者

在 Solana 上,“智能合约”被称为程序。程序所有权是 Solana 账户模型的关键部分。每个账户都有一个指定的程序作为其所有者。只有所有者程序可以:

  • 更改账户的 data字段
  • 从账户余额中扣除 lamport

系统程序

默认情况下,所有新账户都归系统程序所有。系统程序执行以下几个关键操作:

  • 新账户创建:只有系统程序可以创建新账户。
  • 空间分配:为每个账户的数据字段设置字节容量。
  • 转移/分配程序所有权:一旦系统程序创建了一个账户,它可以将指定的程序所有者重新分配给另一个程序账户。这就是自定义程序如何接管由系统程序创建的新账户的所有权。

在 Solana 上,所有“钱包”账户只是由系统程序拥有的账户。这些账户中的 lamport 余额显示了钱包拥有的 SOL 数量。只有由系统程序拥有的账户才能支付交易费用。

系统账户系统账户

Sysvar 账户

Sysvar 账户是位于预定义地址的特殊账户,用于访问集群状态数据。这些账户会动态更新网络集群的相关数据。您可以在此处找到 Sysvar 账户的完整列表。

import { Address, createSolanaRpc } from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const SYSVAR_CLOCK_ADDRESS =
"SysvarC1ock11111111111111111111111111111111" as Address;
const accountInfo = await rpc
.getAccountInfo(SYSVAR_CLOCK_ADDRESS, { encoding: "base64" })
.send();
console.log(accountInfo);
Click to execute the code.

程序账户

部署一个 Solana 程序会创建一个可执行的程序账户。程序账户存储程序的可执行代码。

程序账户由加载器程序拥有。

程序账户程序账户

为了简化,您可以将程序账户视为程序本身。当您调用程序的指令时,您需要指定程序账户的地址(通常称为“程序 ID”)。

import { Address, createSolanaRpc } from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const programId = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" as Address;
const accountInfo = await rpc
.getAccountInfo(programId, { encoding: "base64" })
.send();
console.log(accountInfo);
Click to execute the code.

当您部署一个 Solana 程序时,它会存储在一个程序账户中。程序账户由加载器程序拥有。加载器有多个版本,但除了 loader-v3 之外,所有版本都将可执行代码直接存储在程序账户中。loader-v3 将可执行代码存储在一个单独的“程序数据账户”中,而程序账户仅指向它。当您部署新程序时,Solana CLI 默认使用最新的加载器版本。

缓冲账户

Loader-v3 有一种特殊的账户类型,用于在部署或重新部署/升级期间临时存储程序的上传。在 loader-v4 中,仍然存在缓冲区,但它们只是普通的程序账户。

程序数据账户

Loader-v3 的工作方式与其他所有 BPF Loader 程序不同。程序账户仅包含一个程序数据账户的地址,该账户存储实际的可执行代码:

程序数据账户程序数据账户

不要将这些程序数据账户与程序的数据账户(见下文)混淆。

数据账户

在 Solana 上,程序的可执行代码存储在与程序状态不同的账户中。这类似于操作系统通常将程序和其数据分开存储在不同的文件中。

为了维护状态,程序定义了指令来创建它们拥有的独立账户。这些账户每个都有自己唯一的地址,并可以存储程序定义的任意数据。

数据账户数据账户

请注意,只有 系统程序 可以创建新账户。一旦系统程序创建了一个账户,它可以将新账户的所有权转移或分配给另一个程序。

换句话说,为自定义程序创建数据账户需要两个步骤:

  1. 调用系统程序创建一个账户,然后将所有权转移给自定义程序
  2. 调用现在拥有该账户的自定义程序,根据程序的指令初始化账户数据

这个账户创建过程通常被抽象为一个步骤,但了解其底层过程是有帮助的。

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
getInitializeMintInstruction,
getMintSize,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
// Create Connection, local validator in this example
const rpc = createSolanaRpc("http://127.0.0.1:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate keypairs for fee payer
const feePayer = await generateKeyPairSigner();
// Fund fee payer
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: feePayer.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed"
});
// Generate keypair to use as address of mint
const mint = await generateKeyPairSigner();
// Get default mint account size (in bytes), no extensions enabled
const space = BigInt(getMintSize());
// Get minimum balance for rent exemption
const rent = await rpc.getMinimumBalanceForRentExemption(space).send();
// Instruction to create new account for mint (token 2022 program)
// Invokes the system program
const createAccountInstruction = getCreateAccountInstruction({
payer: feePayer,
newAccount: mint,
lamports: rent,
space,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
});
// Instruction to initialize mint account data
// Invokes the token 2022 program
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 9,
mintAuthority: feePayer.address
});
const instructions = [createAccountInstruction, initializeMintInstruction];
// Get latest blockhash to include in transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }), // Create transaction message
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx), // Set fee payer
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set transaction blockhash
(tx) => appendTransactionMessageInstructions(instructions, tx) // Append instructions
);
// Sign transaction message with required signers (fee payer and mint keypair)
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Mint Address:", mint.address);
console.log("Transaction Signature:", transactionSignature);
const accountInfo = await rpc.getAccountInfo(mint.address).send();
console.log(accountInfo);
Click to execute the code.

Is this page helpful?

Table of Contents

Edit Page