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 还支持一种称为程序派生地址 (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}`);
账户类型
账户的最大大小为 10MiB,并且 Solana 上的每个账户都共享相同的基础 账户 类型。
账户类型
每个 Solana 账户都有以下字段:
data
:一个字节数组,用于存储账户的任意数据。对于不可执行的账户,这通常存储要读取的状态。对于程序账户(智能合约),这包含可执行的程序代码。数据字段通常称为“账户数据”。executable
:此标志显示账户是否为程序。lamports
:账户的余额,以 lamports 为单位,lamports 是 SOL 的最小单位(1 SOL = 10 亿 lamports)。owner
:拥有此账户的程序的程序 ID(公钥)。只有拥有者程序可以更改账户的数据或扣减其 lamports 余额。rent_epoch
:一个遗留字段,源自 Solana 曾经有一个机制定期从账户中扣除 lamports。虽然此字段仍然存在于账户类型中,但自从租金收取被弃用后,它已不再使用。
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,}
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);
租金
要在链上存储数据,账户还必须保持与账户中存储的数据量(以字节为单位)成比例的 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);
程序账户
部署一个 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);
当您部署一个 Solana 程序时,它会存储在一个程序账户中。程序账户由加载器程序拥有。加载器有多个版本,但除了 loader-v3 之外,所有版本都将可执行代码直接存储在程序账户中。loader-v3 将可执行代码存储在一个单独的“程序数据账户”中,而程序账户仅指向它。当您部署新程序时,Solana CLI 默认使用最新的加载器版本。
缓冲账户
Loader-v3 有一种特殊的账户类型,用于在部署或重新部署/升级期间临时存储程序的上传。在 loader-v4 中,仍然存在缓冲区,但它们只是普通的程序账户。
程序数据账户
Loader-v3 的工作方式与其他所有 BPF Loader 程序不同。程序账户仅包含一个程序数据账户的地址,该账户存储实际的可执行代码:
程序数据账户
不要将这些程序数据账户与程序的数据账户(见下文)混淆。
数据账户
在 Solana 上,程序的可执行代码存储在与程序状态不同的账户中。这类似于操作系统通常将程序和其数据分开存储在不同的文件中。
为了维护状态,程序定义了指令来创建它们拥有的独立账户。这些账户每个都有自己唯一的地址,并可以存储程序定义的任意数据。
数据账户
请注意,只有 系统程序 可以创建新账户。一旦系统程序创建了一个账户,它可以将新账户的所有权转移或分配给另一个程序。
换句话说,为自定义程序创建数据账户需要两个步骤:
- 调用系统程序创建一个账户,然后将所有权转移给自定义程序
- 调用现在拥有该账户的自定义程序,根据程序的指令初始化账户数据
这个账户创建过程通常被抽象为一个步骤,但了解其底层过程是有帮助的。
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 exampleconst rpc = createSolanaRpc("http://127.0.0.1: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);
Is this page helpful?