返回文章列表

Solana 账户模型详解:从零理解区块链数据存储

2025年12月28日|14 min read

#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          │         │
│                              └────────────────────────────┘         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

这与以太坊的存储模型有本质区别:

特性SolanaEthereum
数据位置独立账户合约内部 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                     │
│  用途:       存储用户数据、代币信息等    │
└────────────────────────────────────────┘

对比总结:

类型ownerdataexecutable示例
钱包System Programfalse你的 SOL 地址
程序BPF Loader字节码trueToken Program
数据对应程序状态falseUSDC 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 编码)
lamportsSOL 的最小单位,1 SOL = 10^9 lamports
owner拥有账户的程序,只有它能修改 data
executable区分程序账户和数据账户
PDA程序派生地址,由 seeds 确定性生成

#下一步学习


本文内容哈希已存证,点击下方按钮可将哈希写入 Polygon 链上。

END

💬 评论与讨论

使用 GitHub 账号登录即可评论,欢迎讨论和提问!