#Solana 的数据存储模型
Solana 网络上的所有数据都存储在账户中。你可以把 Solana 网络想象成一个巨大的键值对数据库:
- 键 (Key) → 账户地址(32 字节)
- 值 (Value) → 账户数据
┌─────────────────────────────────────────────────────────────────────┐
│ Solana 全局状态数据库 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Address (Key) Account (Value) │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ 9WzDX...FFPDg │ ───→ │ lamports: 1000000000 │ │
│ └──────────────────┘ │ data: [] │ │
│ │ owner: System Program │ │
│ │ executable: false │ │
│ └────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ TokenkegQ...5DA │ ───→ │ lamports: 1141440 │ │
│ └──────────────────┘ │ data: [BPF bytecode...] │ │
│ │ owner: BPF Loader │ │
│ │ executable: true │ │
│ └────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌────────────────────────────┐ │
│ │ EPjFWdd5...Dt1v │ ───→ │ lamports: 2039280 │ │
│ └──────────────────┘ │ data: [Mint data 82 bytes] │ │
│ │ owner: Token Program │ │
│ │ executable: false │ │
│ └────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
这与以太坊的存储模型有本质区别:
| 特性 | Solana | Ethereum |
|---|---|---|
| 数据位置 | 独立账户 | 合约内部 storage |
| 程序状态 | 无状态(代码和数据分离) | 有状态(代码和数据一起) |
| 并行处理 | 可以(账户独立) | 困难(状态共享) |
#账户地址
账户地址是一个 32 字节的唯一标识符,用于在 Solana 网络上定位账户。地址通常以 base58 编码字符串显示。
┌─────────────────────────────────────────────────────────────────┐
│ 账户地址格式 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原始字节 (32 bytes): │
│ [139, 45, 67, 234, 12, 89, 156, 23, 78, 201, ...] │
│ │
│ Base58 编码: │
│ 9WzDXwBbmPdCBoccqJfJR3oVGnWPH6iT8kgrCLxFFPDg │
│ │
│ 为什么用 Base58? │
│ - 比 Hex 更短(节省显示空间) │
│ - 避免混淆字符(没有 0/O, l/I) │
│ - 人类可读性更好 │
│ │
└─────────────────────────────────────────────────────────────────┘
#两种地址类型
Solana 支持两种主要的地址类型:
#1. Ed25519 公钥地址
这是最常见的地址类型。通过生成密钥对获得:
use solana_sdk::signer::{keypair::Keypair, Signer};
fn main() {
// 生成新的密钥对
let keypair = Keypair::new();
// 公钥 = 账户地址
println!("Public Key: {}", keypair.pubkey());
// 私钥用于签名交易(绝对保密!)
println!("Secret Key: {:?}", keypair.to_bytes());
}
输出示例:
Public Key: 9WzDXwBbmPdCBoccqJfJR3oVGnWPH6iT8kgrCLxFFPDg
Secret Key: [139, 45, 67, 234, ... 64 bytes total]
密钥对的关系:
┌────────────────┐ ┌────────────────┐
│ 私钥 (64B) │────→│ 公钥 (32B) │
│ Secret Key │ │ Public Key │
└────────────────┘ └────────────────┘
│ │
▼ ▼
签署交易 账户地址
#2. 程序派生地址 (PDA)
PDA 是通过程序 ID + 种子 (Seeds) 确定性派生的地址,没有对应的私钥:
use solana_sdk::pubkey::Pubkey;
fn main() {
// 程序 ID
let program_id = solana_sdk::pubkey!("11111111111111111111111111111111");
// Seeds 是用于派生地址的字节数组
let seeds = [b"helloWorld".as_ref()];
// 派生 PDA
let (pda, bump) = Pubkey::find_program_address(&seeds, &program_id);
println!("PDA: {}", pda);
println!("Bump: {}", bump); // 用于确保地址不在椭圆曲线上
}
为什么需要 PDA?
- 程序可以"拥有"和控制这些账户
- 地址是确定性的,任何人都能计算出来
- 用于存储程序状态、用户数据等
#账户结构
每个 Solana 账户都有相同的结构,最大大小为 10 MiB:
pub struct Account {
/// 账户余额(单位:lamports,1 SOL = 10^9 lamports)
pub lamports: u64,
/// 账户存储的数据
pub data: Vec<u8>,
/// 拥有此账户的程序(只有 owner 能修改 data)
pub owner: Pubkey,
/// 是否是可执行程序
pub executable: bool,
/// 租金周期(已弃用,现在账户需要保持最低余额免租)
pub rent_epoch: Epoch,
}
账户结构图解:
┌─────────────────────────────────────────────────────────────────┐
│ Account 结构 (内存布局) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┬─────────────────────────────────────────────┐ │
│ │ lamports │ u64 (8 bytes) - SOL 余额 │ │
│ │ 1000000000 │ 1 SOL = 1,000,000,000 lamports │ │
│ ├─────────────┼─────────────────────────────────────────────┤ │
│ │ data │ Vec<u8> - 可变长度数据 │ │
│ │ [82 bytes] │ 钱包:空 / 程序:字节码 / 数据账户:状态 │ │
│ ├─────────────┼─────────────────────────────────────────────┤ │
│ │ owner │ Pubkey (32 bytes) - 拥有者程序 │ │
│ │ TokenProg │ 只有 owner 能修改 data 字段 │ │
│ ├─────────────┼─────────────────────────────────────────────┤ │
│ │ executable │ bool (1 byte) - 是否可执行 │ │
│ │ false │ true = 程序账户, false = 数据账户 │ │
│ ├─────────────┼─────────────────────────────────────────────┤ │
│ │ rent_epoch │ u64 (8 bytes) - 已弃用 │ │
│ │ 0 │ 现在使用"租金豁免"机制 │ │
│ └─────────────┴─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
#三种账户类型
#1. 钱包账户(System Account)
最简单的账户类型,用于存储 SOL:
┌────────────────────────────────────────┐
│ 钱包账户特征 │
├────────────────────────────────────────┤
│ owner: System Program │
│ (11111111...1111) │
│ data: 空 (0 bytes) │
│ executable: false │
│ 用途: 存储 SOL 余额 │
└────────────────────────────────────────┘
#2. 程序账户(Program Account)
存储可执行代码:
┌────────────────────────────────────────┐
│ 程序账户特征 │
├────────────────────────────────────────┤
│ owner: BPF Loader │
│ (BPFLoader2111...111) │
│ data: 编译后的 BPF 字节码 │
│ executable: true │
│ 用途: 执行链上逻辑 │
└────────────────────────────────────────┘
#3. 数据账户(Data Account)
存储程序状态:
┌────────────────────────────────────────┐
│ 数据账户特征 │
├────────────────────────────────────────┤
│ owner: 对应的程序 │
│ (如 Token Program) │
│ data: 序列化的状态数据 │
│ executable: false │
│ 用途: 存储用户数据、代币信息等 │
└────────────────────────────────────────┘
对比总结:
| 类型 | owner | data | executable | 示例 |
|---|---|---|---|---|
| 钱包 | System Program | 空 | false | 你的 SOL 地址 |
| 程序 | BPF Loader | 字节码 | true | Token Program |
| 数据 | 对应程序 | 状态 | false | USDC Mint |
#实战:用 Rust 读取账户数据
让我们通过实际代码来理解账户:
#项目配置
首先创建项目并添加依赖:
# Cargo.toml
[package]
name = "solana-account-reader"
version = "0.1.0"
edition = "2021"
[dependencies]
solana-client = "1.18"
solana-sdk = "1.18"
spl-token = "4.0"
tokio = { version = "1.0", features = ["full"] }
#示例 1:读取钱包账户
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
fn main() {
// 连接到 Devnet
let client = RpcClient::new("https://api.devnet.solana.com");
// 钱包地址
let wallet = Pubkey::from_str("9WzDXwBbmPdCBoccqJfJR3oVGnWPH6iT8kgrCLxFFPDg")
.unwrap();
match client.get_account(&wallet) {
Ok(account) => {
println!("=== 钱包账户信息 ===");
println!("地址: {}", wallet);
println!("余额: {} lamports ({:.9} SOL)",
account.lamports,
account.lamports as f64 / 1_000_000_000.0
);
println!("Owner: {}", account.owner);
println!("可执行: {}", account.executable);
println!("数据长度: {} bytes", account.data.len());
}
Err(e) => println!("错误: {}", e),
}
}
预期输出:
=== 钱包账户信息 ===
地址: 9WzDXwBbmPdCBoccqJfJR3oVGnWPH6iT8kgrCLxFFPDg
余额: 1000000000 lamports (1.000000000 SOL)
Owner: 11111111111111111111111111111111
可执行: false
数据长度: 0 bytes
#示例 2:读取程序账户
fn read_program_account() {
let client = RpcClient::new("https://api.mainnet-beta.solana.com");
// Token Program 地址(所有网络相同)
let token_program = Pubkey::from_str(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
).unwrap();
match client.get_account(&token_program) {
Ok(account) => {
println!("=== 程序账户信息 ===");
println!("地址: {}", token_program);
println!("余额: {} lamports", account.lamports);
println!("Owner: {}", account.owner);
println!("可执行: {}", account.executable); // true!
println!("代码大小: {} bytes ({:.2} KB)",
account.data.len(),
account.data.len() as f64 / 1024.0
);
}
Err(e) => println!("错误: {}", e),
}
}
预期输出:
=== 程序账户信息 ===
地址: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
余额: 1141440 lamports
Owner: BPFLoader2111111111111111111111111111111111
可执行: true
代码大小: 130892 bytes (127.82 KB)
#示例 3:读取并解析 Mint 账户
use spl_token::state::Mint;
use solana_sdk::program_pack::Pack;
fn read_mint_account() {
let client = RpcClient::new("https://api.mainnet-beta.solana.com");
// USDC Mint 地址
let usdc_mint = Pubkey::from_str(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
).unwrap();
match client.get_account(&usdc_mint) {
Ok(account) => {
println!("=== USDC Mint 账户 ===");
println!("地址: {}", usdc_mint);
println!("Owner: {}", account.owner);
println!("数据长度: {} bytes", account.data.len());
// 解析 Mint 数据
if let Ok(mint) = Mint::unpack(&account.data) {
println!("\n--- 解析后的 Mint 数据 ---");
println!("总供应量: {}", mint.supply);
println!("小数位数: {}", mint.decimals);
println!("已初始化: {}", mint.is_initialized);
println!("铸币权限: {:?}", mint.mint_authority);
println!("冻结权限: {:?}", mint.freeze_authority);
// 格式化显示
let formatted_supply = mint.supply as f64
/ 10_f64.powi(mint.decimals as i32);
println!("\n格式化供应量: {:.2} USDC", formatted_supply);
}
}
Err(e) => println!("错误: {}", e),
}
}
预期输出:
=== USDC Mint 账户 ===
地址: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
数据长度: 82 bytes
--- 解析后的 Mint 数据 ---
总供应量: 29876543210000000
小数位数: 6
已初始化: true
铸币权限: COption::Some(2wmVCSfPxGPjrnMMn7rchp4uaeoTqN39mXFC2zhPdri9)
冻结权限: COption::Some(3sNBr7kMccME5D55xNgsmYpZnzPgP2g12CixAajXypn6)
格式化供应量: 29876543210.00 USDC
#手动解析账户数据
有时你需要手动解析账户数据,理解字节布局很重要:
#Mint 账户数据布局 (82 bytes)
┌─────────────────────────────────────────────────────────────────┐
│ Mint 数据结构 (82 bytes) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Offset Size Field Type │
│ ────── ──── ───── ──── │
│ [0..4] 4 mint_authority_option u32 (COption tag) │
│ [4..36] 32 mint_authority Pubkey │
│ [36..44] 8 supply u64 │
│ [44] 1 decimals u8 │
│ [45] 1 is_initialized bool │
│ [46..50] 4 freeze_authority_option u32 (COption tag) │
│ [50..82] 32 freeze_authority Pubkey │
│ │
│ COption: 0 = None, 1 = Some │
│ │
└─────────────────────────────────────────────────────────────────┘
#手动解析示例
fn manual_parse_mint(data: &[u8]) {
// 检查数据长度
if data.len() < 82 {
println!("数据长度不足");
return;
}
// 解析 supply (u64, 小端序)
let supply = u64::from_le_bytes(
data[36..44].try_into().unwrap()
);
// 解析 decimals (u8)
let decimals = data[44];
// 解析 is_initialized (bool)
let is_initialized = data[45] != 0;
// 解析 mint_authority (COption<Pubkey>)
let has_mint_authority = u32::from_le_bytes(
data[0..4].try_into().unwrap()
) == 1;
let mint_authority = if has_mint_authority {
Some(Pubkey::try_from(&data[4..36]).unwrap())
} else {
None
};
println!("Supply: {}", supply);
println!("Decimals: {}", decimals);
println!("Initialized: {}", is_initialized);
println!("Mint Authority: {:?}", mint_authority);
}
#Owner 权限模型
Solana 的安全模型基于 owner 字段:
┌─────────────────────────────────────────────────────────────────┐
│ Owner 权限规则 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 规则 1: 只有 owner 程序可以修改账户的 data 字段 │
│ 规则 2: 只有 owner 程序可以减少账户的 lamports │
│ 规则 3: 任何人都可以增加账户的 lamports(转账 SOL) │
│ 规则 4: 只有 owner 程序可以将 owner 转让给其他程序 │
│ 规则 5: 新账户默认 owner 是 System Program │
│ │
│ 示例: │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Token Mint │ owner → │ Token │ ← 只有 Token │
│ │ Account │ │ Program │ Program 能修改 │
│ └──────────────┘ └──────────────┘ Mint 数据 │
│ │
└─────────────────────────────────────────────────────────────────┘
#租金机制
Solana 账户需要支付"租金"来占用链上存储空间:
// 计算账户需要的最低余额(租金豁免)
let min_balance = client
.get_minimum_balance_for_rent_exemption(data_len)?;
println!("{}字节数据需要 {} lamports 租金豁免",
data_len, min_balance);
常见账户的租金豁免金额:
| 账户类型 | 数据大小 | 最低余额 |
|---|---|---|
| 钱包账户 | 0 bytes | ~0.00089 SOL |
| Mint 账户 | 82 bytes | ~0.00145 SOL |
| Token 账户 | 165 bytes | ~0.00203 SOL |
#完整示例代码
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use spl_token::state::Mint;
use solana_sdk::program_pack::Pack;
use std::str::FromStr;
fn main() {
println!("=== Solana 账户读取学习 ===\n");
let devnet = RpcClient::new("https://api.devnet.solana.com");
let mainnet = RpcClient::new("https://api.mainnet-beta.solana.com");
// 1. 读取钱包账户
println!("--- 1. 钱包账户 ---");
let wallet = Pubkey::from_str(
"9WzDXwBbmPdCBoccqJfJR3oVGnWPH6iT8kgrCLxFFPDg"
).unwrap();
if let Ok(acc) = devnet.get_account(&wallet) {
println!("余额: {} SOL", acc.lamports as f64 / 1e9);
println!("Owner: {} (System Program)", acc.owner);
}
// 2. 读取程序账户
println!("\n--- 2. 程序账户 (Token Program) ---");
let token_program = Pubkey::from_str(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
).unwrap();
if let Ok(acc) = mainnet.get_account(&token_program) {
println!("可执行: {}", acc.executable);
println!("代码大小: {} KB", acc.data.len() / 1024);
}
// 3. 读取并解析 Mint 账户
println!("\n--- 3. USDC Mint 账户 ---");
let usdc_mint = Pubkey::from_str(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
).unwrap();
if let Ok(acc) = mainnet.get_account(&usdc_mint) {
if let Ok(mint) = Mint::unpack(&acc.data) {
let supply = mint.supply as f64 / 10_f64.powi(mint.decimals as i32);
println!("总供应量: {:.2} USDC", supply);
println!("小数位数: {}", mint.decimals);
}
}
println!("\n=== 学习完成 ===");
}
#关键概念总结
| 概念 | 说明 |
|---|---|
| 账户 | Solana 数据存储的基本单元 |
| 地址 | 32 字节的唯一标识符 (base58 编码) |
| lamports | SOL 的最小单位,1 SOL = 10^9 lamports |
| owner | 拥有账户的程序,只有它能修改 data |
| executable | 区分程序账户和数据账户 |
| PDA | 程序派生地址,由 seeds 确定性生成 |
#下一步学习
- Solana PDA 详解 - 深入理解程序派生地址
- Anchor 入门教程 - 学习 Anchor 框架开发
- Solana Rust 学习路线 - 系统学习 Solana 开发
本文内容哈希已存证,点击下方按钮可将哈希写入 Polygon 链上。