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 keysconst keypairSigner = await generateKeyPairSigner();console.log(keypairSigner);
虽然公钥通常用作账户地址,但 Solana 还支持一种称为程序派生地址(PDA)的功能。PDA 是可以从程序 ID 和可选输入(seed)确定性派生的特殊地址。
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}`);
账户类型
账户的最大大小为 10MiB,并且 Solana 上的每个账户都共享相同的基础账户类型。
账户类型
每个 Solana 账户都有以下字段。
pub struct Account {/// lamports in the accountpub 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 rentpub rent_epoch: Epoch,}
Lamports 字段
账户的余额以 lamport 为单位,lamport 是 SOL 的最小单位(1 SOL =
10 亿 lamport)。账户的 SOL 余额是 lamports 字段中的金额转换为 SOL。
Solana 账户必须拥有与账户中存储的数据量(以字节为单位)成比例的最低 lamport 余额。此最低余额称为“租金”。
当账户关闭时,存储在账户中的 lamport 余额可以完全恢复。
数据字段
一个字节数组,用于存储账户的任意数据。该数据字段通常被称为“账户数据”。
- 对于程序账户(智能合约),此字段包含可执行程序代码本身或存储可执行程序代码的另一个账户的地址。
- 对于非可执行账户,此字段通常存储需要被读取的状态。
从 Solana 账户读取数据需要两个步骤:
- 使用账户的地址(公钥)获取账户
- 将账户的数据字段从原始字节反序列化为适当的数据结构,该结构由拥有该账户的程序定义
所有者字段
拥有此账户的程序的程序 ID(公钥)。
每个 Solana 账户都有一个指定的程序作为其所有者。只有拥有账户的程序可以更改账户的数据或扣除其 lamports 余额。
程序中定义的指令决定了账户的数据和 lamports 余额可以如何更改。
可执行字段
此字段指示账户是否为可执行程序。
- 如果
true,该账户是一个可执行的 Solana 程序。 - 如果
false,该账户是存储状态的数据账户。
对于可执行账户,owner
字段包含加载程序的程序 ID。加载程序是负责加载和管理可执行程序账户的内置程序。
租金周期字段
rent_epoch 字段是一个已不再使用的遗留字段。
最初,该字段用于跟踪账户何时需要支付租金(以 lamports 计)以维护其在网络上的数据。然而,这种租金收取机制现已被弃用。
Rent
为了在链上存储数据,账户还必须保持与账户中存储的数据量(以字节为单位)成比例的 lamport (SOL) 余额。这个余额被称为“rent”,但它更像是一种押金,因为当您关闭账户时,可以取回全部金额。您可以在这里找到计算方法,并使用这些常量。
“rent”一词来源于一种已弃用的机制,该机制会定期从低于 rent 阈值的账户中扣除 lamport。这种机制现已不再使用。
Program Owner
在 Solana 上,“智能合约”被称为程序。程序所有权是 Solana 账户模型的关键部分。每个账户都有一个指定的程序作为其所有者。只有所有者程序可以:
- 更改账户的
data字段 - 从账户余额中扣除 lamport
每个程序定义存储在账户 data
字段中的数据结构。程序的指令决定了如何更改这些数据以及账户的 lamports 余额。
System Program
默认情况下,所有新账户都归System Program所有。System Program 执行以下关键功能:
| 功能 | 描述 |
|---|---|
| 新账户创建 | 只有 System Program 可以创建新账户。 |
| 空间分配 | 设置每个账户数据字段的字节容量。 |
| 分配程序所有权 | 一旦 System Program 创建了一个账户,它可以将指定的程序所有者重新分配给另一个程序账户。这就是自定义程序如何接管由 System Program 创建的新账户的所有权。 |
| 转移 SOL | 将 lamport (SOL) 从 System Accounts 转移到其他账户。 |
请注意,Solana 上所有的 "钱包" 账户都是由 System Program 拥有的 "系统账户"。这些账户中的 lamport 余额显示了钱包拥有的 SOL 数量。只有系统账户可以支付交易费用。
系统账户
当 SOL 第一次发送到一个新地址时,会自动在该地址创建一个由 System Program 拥有的账户。
在下面的示例中,生成了一个新的 keypair,并用 SOL 为其提供资金。运行代码查看输出结果。请注意,账户的
owner 字段是 System Program,其地址为 11111111111111111111111111111111。
import {airdropFactory,createSolanaRpc,createSolanaRpcSubscriptions,generateKeyPairSigner,lamports} from "@solana/kit";// Create a connection to Solana clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate a new keypairconst keypair = await generateKeyPairSigner();console.log(`Public Key: ${keypair.address}`);// Funding an address with SOL automatically creates an accountconst 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);
Sysvar 账户
Sysvar 账户是一些位于预定义地址的特殊账户,用于提供对集群状态数据的访问。这些账户会动态更新网络集群的相关数据。您可以在这里找到 Sysvar 账户的完整列表。
以下示例展示了如何从 Sysvar Clock 账户中获取并反序列化数据。
import { createSolanaRpc } from "@solana/kit";import { fetchSysvarClock, SYSVAR_CLOCK_ADDRESS } from "@solana/sysvars";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const accountInfo = await rpc.getAccountInfo(SYSVAR_CLOCK_ADDRESS, { encoding: "base64" }).send();console.log(accountInfo);// Automatically fetch and deserialize the account dataconst clock = await fetchSysvarClock(rpc);console.log(clock);
Program Account
部署一个 Solana 程序会创建一个可执行的 program account。program account 存储程序的可执行代码。program account 由 Loader Program 拥有。
Program Account
为了简化理解,可以将 program account 视为程序本身。当您调用程序的指令时,需要指定 program account 的地址(通常称为 "Program ID")。
以下示例获取了 Token Program 账户,展示了 program account 具有相同的基础
Account 类型,除了 executable 字段被设置为 true。由于 program
account 的数据字段中包含可执行代码,因此我们不会对数据进行反序列化。
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);
当您部署一个 Solana 程序时,它会存储在一个 program account 中。Program account 由 Loader Program 拥有。Loader 有多个版本,但除了 loader-v3 以外,所有版本都将可执行代码直接存储在 program account 中。Loader-v3 将可执行代码存储在一个单独的 "program data account" 中,而 program account 仅指向它。当您部署新程序时,Solana CLI 默认使用最新的 loader 版本。
Buffer Account
Loader-v3 在部署或升级程序时有一种特殊的账户类型,用于临时存储程序的上传。在 loader-v4 中,仍然存在 buffer,但它们只是普通的 program account。
Program Data Account
Loader-v3 的工作方式与其他所有 BPF Loader 程序不同。Program account 仅包含一个 program data account 的地址,而 program data account 存储实际的可执行代码:
Program Data Account
不要将这些 program data account 与程序的数据账户(见下文)混淆。
Data Account
在 Solana 上,程序的可执行代码存储在与程序状态不同的账户中。这类似于操作系统通常将程序和其数据分开存储的方式。
为了维护状态,程序定义了指令来创建它们拥有的单独账户。这些账户每个都有自己唯一的地址,并可以存储程序定义的任意数据。
Data Account
请注意,只有 System Program 可以创建新账户。一旦 System Program 创建了一个账户,它可以将新账户的所有权分配给其他程序。
换句话说,为自定义程序创建数据账户需要两个步骤:
- 调用 System Program 创建一个账户,然后将所有权转移给自定义程序
- 调用现在拥有该账户的自定义程序,根据程序的指令初始化账户数据
这个账户创建过程通常被抽象为一个步骤,但了解其底层过程是有帮助的。
以下示例展示了如何创建和获取由 Token 2022 程序拥有的 Token Mint 账户。
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,fetchMint} from "@solana-program/token-2022";// Create Connection, local validator in this exampleconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate keypairs for fee payerconst feePayer = await generateKeyPairSigner();// Fund fee payerawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: feePayer.address,lamports: lamports(1_000_000_000n),commitment: "confirmed"});// Generate keypair to use as address of mintconst mint = await generateKeyPairSigner();// Get default mint account size (in bytes), no extensions enabledconst space = BigInt(getMintSize());// Get minimum balance for rent exemptionconst rent = await rpc.getMinimumBalanceForRentExemption(space).send();// Instruction to create new account for mint (token 2022 program)// Invokes the system programconst createAccountInstruction = getCreateAccountInstruction({payer: feePayer,newAccount: mint,lamports: rent,space,programAddress: TOKEN_2022_PROGRAM_ADDRESS});// Instruction to initialize mint account data// Invokes the token 2022 programconst initializeMintInstruction = getInitializeMintInstruction({mint: mint.address,decimals: 9,mintAuthority: feePayer.address});const instructions = [createAccountInstruction, initializeMintInstruction];// Get latest blockhash to include in transactionconst { value: latestBlockhash } = await rpc.getLatestBlockhash().send();// Create transaction messageconst 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 transactionawait sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });// Get transaction signatureconst 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);const mintAccount = await fetchMint(rpc, mint.address);console.log(mintAccount);
Is this page helpful?