Solana —— 账户模型与简单交互

在 Solana 中,所有数据都存储在称为”账户”(Accounts)的结构中。可以将 Solana 上的数据视为一个公共数据库,其中有一个名为”Accounts”的表,表中的每一条记录就是一个”账户”。

1. 核心要点

  • 存储容量:账户最多可以存储 10MiB 的数据,可以是可执行的程序代码或程序状态
  • 租金押金:账户需要按比例缴纳租金押金(以 lamports 计,即 SOL 的最小单位),押金金额与存储的数据量成正比,关闭账户时可以完全收回
  • 所有权机制:每个账户都有一个程序所有者。只有拥有该账户的程序才能修改其数据或扣除其 lamport 余额,但任何人都可以增加余额
  • 特殊账户类型
    • Sysvar 账户:存储网络集群状态的特殊账户
    • Program 账户:存储智能合约的可执行代码
    • Data 账户:由程序创建用于存储和管理程序状态

2. 账户结构

2.1 账户地址

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

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

地址生成方式:

  1. Ed25519 公钥地址(最常见)
    • 大多数 Solana 账户使用 Ed25519 公钥作为其地址
  2. 程序派生地址(PDA)
    • 可以从程序 ID 和可选输入(种子)确定性地派生的特殊地址
    • PDA 是一种特殊的地址类型,不需要私钥就可以使用

2.2 账户类型定义

所有 Solana 账户都具有相同的基础结构:

1
2
3
4
5
6
7
pub struct Account {
pub lamports: u64, // 账户中的 lamports 余额
pub data: Vec<u8>, // 账户中存储的数据
pub owner: Pubkey, // 拥有此账户的程序。如果可执行,则为加载此账户的程序
pub executable: bool, // 此账户的数据是否包含已加载的程序(现在为只读)
pub rent_epoch: Epoch, // 此账户下次需要支付租金的 epoch(已弃用)
}

2.3 字段详解

Lamports 字段

账户的余额(以 lamports 为单位,1 SOL = 10 亿 lamports)。

重要特性:

  • 账户必须保持最低 lamport 余额,该余额与存储的数据量成正比(称为”租金”)
  • 关闭账户时可以完全收回存储在账户中的 lamport 余额

Data 字段

存储账户任意数据的字节数组,也称为”账户数据”。

不同账户类型的 data 内容:

  • 对于程序账户:包含可执行程序代码本身或存储可执行代码的另一个账户的地址
  • 对于非可执行账户:通常存储要读取的状态数据

读取账户数据的步骤:

  1. 使用地址(公钥)获取账户
  2. 将账户的 data 字段从原始字节反序列化为适当的数据结构(由拥有该账户的程序定义)

Owner 字段

拥有此账户的程序 ID(公钥)。

所有权规则:

  • 只有所有者程序才能更改账户的数据或扣除其 lamports 余额
  • 程序中定义的指令决定了账户的数据和 lamports 余额如何被更改

Executable 字段

指示账户是否为可执行程序:

  • true:账户是可执行的 Solana 程序
  • false:账户是存储状态的数据账户

对于可执行账户,owner 字段包含加载器程序的程序 ID。加载器程序是负责加载和管理可执行程序账户的内置程序。

Rent Epoch 字段

**注意:**这是一个已弃用的遗留字段,不再使用。最初用于跟踪账户何时需要支付租金以维护其网络数据,此租金收取机制已被废弃。

3. 租金机制

要在链上存储数据,账户必须保持与存储数据量(字节)成正比的 lamport(SOL)余额。这个余额称为”租金”,但它更像是押金,因为关闭账户时可以收回全部金额。

注意: “租金”一词来自已弃用的机制,该机制会定期从低于租金阈值的账户中扣除 lamports。此机制已不再活跃。

4. 程序所有权

在 Solana 上,”智能合约”被称为”程序”。程序所有权是 Solana 账户模型的关键部分。

4.1 所有者权限

只有所有者程序可以:

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

每个程序定义存储在账户 data 字段中的数据结构。程序的指令决定了如何更改这些数据和账户的 lamports 余额。

4.2 System Program(系统程序)

默认情况下,所有新账户都归 System Program 所有

System Program 的关键功能:

功能 描述
空间分配 为新账户分配存储空间
分配程序所有权 将账户所有权转移给其他程序
转账 SOL 在账户之间转移 lamports

System Account(系统账户):

  • Solana 上的所有”钱包”账户都是由 System Program 拥有的”系统账户”
  • 这些账户中的 lamport 余额显示钱包拥有的 SOL 数量
  • 只有系统账户可以支付交易费用
  • 当首次向新地址发送 SOL 时,会在该地址自动创建一个由 System Program 拥有的账户
  • 新创建的系统账户的 owner 字段显示为 System Program 地址:11111111111111111111111111111111

5. 特殊账户类型

5.1 Sysvar Accounts(系统变量账户)

Sysvar 账户是位于预定义地址的特殊账户,提供对集群状态数据的访问。这些账户会动态更新网络集群的相关数据。

例如,可以通过 Sysvar Clock 账户获取当前的集群时间信息,这些账户的数据可以被获取并反序列化为相应的数据结构。

5.2 Program Account(程序账户)

部署 Solana 程序会创建一个可执行的程序账户,用于存储程序的可执行代码。程序账户由加载器程序(Loader Program)拥有。

关键特点:

  • 程序账户可以简单地视为程序本身
  • 调用程序的指令时,需要指定程序账户的地址(通常称为”Program ID”)
  • 程序账户的 executable 字段设置为 true

程序部署方式:

  1. Loader-v3 之前的版本
    • 可执行代码直接存储在程序账户中
  2. Loader-v3(当前默认):
    • 可执行代码存储在单独的”程序数据账户”(Program Data Account)中
    • 程序账户只是指向该数据账户
    • Solana CLI 默认使用最新的加载器版本

5.3 Buffer Account(缓冲区账户)

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

5.4 Program Data Account(程序数据账户)

Loader-v3 工作方式与其他 BPF 加载器程序不同:

  • 程序账户只包含程序数据账户的地址
  • 程序数据账户存储实际的可执行代码

注意: 不要将程序数据账户与程序的数据账户(Data Account)混淆。

5.5 Data Account(数据账户)

在 Solana 上,程序的可执行代码与程序的状态存储在不同的账户中。这类似于操作系统通常为程序及其数据使用单独文件的方式。

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

重要提示: 只有 System Program 可以创建新账户。System Program 创建账户后,可以将新账户的所有权分配给另一个程序。

创建数据账户的两步过程:

  1. 调用 System Program:创建账户,然后将所有权转移给自定义程序
  2. 调用自定义程序:初始化账户数据(按程序指令定义)

示例:创建 Token Mint 账户

  • 首先调用 System Program 创建账户并分配空间
  • 然后调用 Token Program 初始化 mint 账户的数据
  • 需要计算租金豁免所需的最低余额
  • 完成后账户由 Token Program 拥有并存储 mint 的相关数据

6. 账户交互实战

6.1 环境准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 启动本地验证器
solana-test-validator

# 切换到本地网络
solana config set -ul

# 创建本地钱包账户
solana-keygen new --outfile ~/.config/solana/wallet1.json
solana-keygen new --outfile ~/.config/solana/wallet2.json

# 设置默认钱包
solana config set --keypair ~/.config/solana/wallet1.json

# 请求空投
solana airdrop 10

6.2 项目设置

1
2
3
4
5
6
7
# 创建新项目
cargo new account-interaction

# 添加依赖库
cargo add solana_client
cargo add solana_sdk
cargo add solana_program

6.3 请求空投与查询余额

通过这个示例,你可以学习如何:

  • 连接到 Solana 网络
  • 请求空投到指定账户
  • 查询账户余额
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;

// Wallet1: 8BG3BQmLhCsayYUGinVyUMzfni7CM1WiVApqZmGaJbjW
// Wallet2: 65bUqQKj4Axew8Z7KpXGodFowdXbWBE4ne8ocUZMoKmz
fn main() {
// 创建 Solana 客户端连接
let rpc_url = "http://127.0.0.1:8899";
let client = RpcClient::new(rpc_url);

// 账户公钥
let account_pubkey = Pubkey::from_str("8BG3BQmLhCsayYUGinVyUMzfni7CM1WiVApqZmGaJbjW")
.expect("Invalid public key");

// 1 SOL = 10 亿 lamports
let amount = 1_000_000_000;

// 请求空投
match client.request_airdrop(&account_pubkey, amount) {
Ok(signature) => println!("空投成功! 签名: {}", signature),
Err(err) => eprintln!("空投失败: {}", err),
}

// 查询账户余额
match client.get_balance(&account_pubkey) {
Ok(balance) => {
println!("账户余额: {} lamports", balance);
println!("账户余额: {} SOL", balance as f64 / 1_000_000_000.0);
}
Err(err) => eprintln!("获取余额失败: {}", err),
}
}

6.4 转账交易

这个示例演示了 Solana 账户之间的转账流程:

  • 加载发送方密钥对
  • 创建转账指令
  • 构建、签名并发送交易
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
pubkey::Pubkey,
signature::{Signer, read_keypair_file},
system_instruction,
transaction::Transaction,
};
use std::str::FromStr;

// Wallet1: 8BG3BQmLhCsayYUGinVyUMzfni7CM1WiVApqZmGaJbjW
// Wallet2: 65bUqQKj4Axew8Z7KpXGodFowdXbWBE4ne8ocUZMoKmz
fn main() {
// 创建 Solana 客户端连接
let rpc_url = "http://127.0.0.1:8899";
let client = RpcClient::new(rpc_url);

// 加载发送方密钥对
let sender =
read_keypair_file("/home/sol/.config/solana/wallet1.json").expect("无法读取密钥文件");

// 接收方公钥
let receiver_pubkey = Pubkey::from_str("65bUqQKj4Axew8Z7KpXGodFowdXbWBE4ne8ocUZMoKmz")
.expect("Invalid public key");

// 转账金额:1 SOL
let amount = 1_000_000_000;

// 创建转账指令
let transfer_instruction =
system_instruction::transfer(&sender.pubkey(), &receiver_pubkey, amount);

// 获取最新区块哈希
let recent_blockhash = client.get_latest_blockhash().expect("无法获取最新区块哈希");

// 创建并签名交易
let transaction = Transaction::new_signed_with_payer(
&[transfer_instruction],
Some(&sender.pubkey()),
&[&sender],
recent_blockhash,
);

// 发送并确认交易
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => println!("转账成功! 签名: {}", signature),
Err(err) => eprintln!("转账失败: {}", err),
}

// 查询接收方余额
match client.get_balance(&receiver_pubkey) {
Ok(balance) => {
println!("接收方账户余额: {} lamports", balance);
println!("接收方账户余额: {} SOL", balance as f64 / 1_000_000_000.0);
}
Err(err) => eprintln!("获取余额失败: {}", err),
}
}

7. 总结

  1. 账户是 Solana 的基础存储单元:所有数据都存储在账户中
  2. 账户具有统一的结构:所有账户共享相同的基础字段
  3. 所有权是关键:只有所有者程序可以修改账户数据
  4. 租金是可恢复的押金:不是真正的费用,关闭账户时可以收回
  5. 程序和数据分离:可执行代码和状态数据存储在不同的账户中
  6. System Program 是账户的创建者:所有新账户都由 System Program 创建

Hooray!Solana 账户模型学习完成!!!