Solana 简介
在 Solana 上进行开发之前,您需要了解一些使 Solana 独特的基本概念。本指南涵盖了账户、交易、程序及其交互方式。
Solana 上的账户
Solana 的架构以账户为中心:账户是存储在区块链上的数据容器。可以将账户想象成文件系统中的单个文件,每个文件都有特定的属性和一个控制它的所有者。
每个 Solana 账户都有相同的基本结构:
pub struct Account {
/// lamports in the account
pub 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 rent
pub rent_epoch: Epoch,
}
每个账户都有一个唯一的 32 字节地址,以 base58 编码的字符串形式显示(例如,14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5
)。此地址是账户在区块链上的标识符,用于定位特定数据。
账户最多可以存储 10 MiB 的数据,这些数据可以是可执行的程序代码或特定程序的数据。
所有账户都需要根据其数据大小存入一定数量的 lamport 押金以达到“免租”状态。“租金”一词具有历史意义,因为 lamport 最初会在每个 epoch 从账户中扣除,但这一功能现已禁用。如今,这种押金更像是可退还的押金。只要您的账户保持其数据大小所需的最低余额,它就会保持免租状态并无限期存在。当您不再需要某个账户时,可以关闭它并全额取回押金。
每个账户都由一个程序拥有,只有该拥有程序可以修改账户的数据或提取其 lamport。然而,任何人都可以增加账户的 lamport 余额,这对于资助操作或支付租金而无需调用程序本身非常有用。
签署权限的运作方式因所有权而异。由系统程序拥有的账户可以签署交易以修改其自身数据、转移所有权或回收存储的 lamports。一旦所有权转移到另一个程序,该程序将完全控制该账户,无论您是否仍然拥有私钥。这种控制的转移是永久且不可逆的。
账户类型
最常见的账户类型是系统账户,它存储 lamports(SOL 的最小单位)并由系统程序拥有。这些账户作为基本的钱包账户,用户可以直接与之交互以发送和接收 SOL。
代币账户具有专门的用途,用于存储 SPL 代币信息,包括所有权和代币元数据。这些账户由代币程序拥有,并管理 Solana 生态系统中的所有代币相关操作。代币账户属于 数据账户。
数据账户存储特定于应用程序的信息,并由自定义程序拥有。这些账户保存您的应用程序状态,其结构可以根据您的程序需求进行设计,从简单的用户资料到复杂的财务数据。
最后,程序账户包含在 Solana 上运行的可执行代码,也就是智能合约所在的位置。这些账户被标记为 executable: true
,并存储处理指令和管理状态的程序逻辑。
使用账户数据
以下是程序与账户数据交互的方式:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
pub name: String,
pub balance: u64,
pub posts: Vec<u32>,
}
pub fn update_user_data(accounts: &[AccountInfo], new_name: String) -> ProgramResult {
let user_account = &accounts[0];
// Deserialize existing data
let mut user_data = UserAccount::try_from_slice(&user_account.data.borrow())?;
// Modify the data
user_data.name = new_name;
// Serialize back to account
user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
与简单插入记录的数据库不同,Solana 账户必须在使用前明确创建并提供资金。
Solana 上的交易
Solana 交易是原子操作,可以包含多个指令。交易中的所有指令要么一起成功,要么一起失败:不存在部分执行的情况。
一笔交易包括:
- 指令:要执行的单个操作
- 账户:每个指令将读取或写入的特定账户
- 签名者:授权交易的账户
Transaction {
instructions: [
// Instruction 1: Transfer SOL
system_program::transfer(from_wallet, to_wallet, amount),
// Instruction 2: Update user profile
my_program::update_profile(user_account, new_name),
// Instruction 3: Log activity
my_program::log_activity(activity_account, "transfer", amount),
],
accounts: [from_wallet, to_wallet, user_account, activity_account]
signers: [user_keypair],
}
交易要求和费用
交易总大小限制为 1,232 字节,这限制了可以包含的指令和账户数量。
交易中的每个指令需要三个基本组件:要调用的程序地址、指令将读取或写入的所有账户,以及任何附加数据(如函数参数)。
指令按照您在交易中指定的顺序依次执行。
每笔交易需支付每个签名 5,000 lamports 的基础费用,以补偿验证者处理您的交易。
您还可以支付可选的优先费,以提高当前领导者快速处理您交易的可能性。此优先费的计算方式为您的计算单元限制乘以您的计算单元价格(以微 lamports 为单位)。
prioritization_fee = compute_unit_limit × compute_unit_price
Programs on Solana
Solana 上的程序本质上是无状态的,这意味着它们在函数调用之间不维护任何内部状态。相反,它们接收账户作为输入,处理这些账户中的数据,并返回修改后的结果。
这种无状态设计确保了行为的可预测性,并支持强大的可组合性模式。
程序本身存储在标记为 executable: true
的特殊账户中,其中包含在调用时执行的已编译二进制代码。
用户通过发送包含特定指令的交易与这些程序交互,每个指令都针对特定的程序功能,并附带必要的账户数据和参数。
use solana_program::prelude::*;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
pub name: String,
pub created_at: i64,
}
pub fn create_user(
accounts: &[AccountInfo],
name: String,
) -> ProgramResult {
let user_account = &accounts[0];
let user = User {
name,
created_at: Clock::get()?.unix_timestamp,
};
user.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
程序可以通过其指定的升级权限进行更新,使开发者能够在部署后修复漏洞并添加功能。然而,移除此升级权限会使程序永久不可变,从而向用户提供代码永不更改的保证。
为了实现透明性和安全性,用户可以通过可验证的构建来确认链上程序与其公开的源代码相匹配,确保部署的字节码与发布的源代码完全一致。
程序派生地址 (PDAs)
PDA 是通过确定性生成的地址,能够实现强大的可编程模式。它们使用种子和程序 ID 创建,生成的地址没有对应的私钥。
PDA 使用 SHA-256
哈希算法,并结合特定的输入,包括自定义种子、确保结果不在曲线上的 bump 值、将拥有 PDA 的程序 ID 和一个常量标记。
当哈希生成一个在曲线上的地址(大约 50% 的概率)时,系统会从 bump 值 255 开始,依次递减到 254、253 等,直到找到一个不在曲线上的结果。
use solana_nostd_sha256::hashv;
const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress";
let pda = hashv(&[
seed_data.as_ref(), // Your custom seeds
&[bump], // Bump to ensure off-curve
program_id.as_ref(), // Program that owns this PDA
PDA_MARKER,
]);
优势
PDA 的确定性特性消除了存储地址的需求:您可以在需要时从相同的种子重新生成它们。
这创建了可预测的寻址方案,在链上功能类似于哈希映射结构。更重要的是,程序可以为其自己的 PDA 签名,从而实现自主资产管理而无需暴露私钥:
let seeds = &[b"vault", user.as_ref(), &[bump]];
invoke_signed(
&transfer_instruction,
&[from_pda, to_account, token_program],
&[&seeds[..]], // Program proves PDA control
)?;
跨程序调用 (CPI)
CPI 允许程序在同一事务中调用其他程序,从而实现真正的可组合性,使多个程序能够在无需外部协调的情况下原子性地交互。
这使开发者能够通过组合现有程序来构建复杂的应用程序,而无需从头开始重建功能。
CPI(跨程序调用)遵循与常规指令相同的模式,需要您指定目标程序、所需的账户以及指令数据,主要区别在于它们可以在其他程序内部执行。
调用程序在控制流程的同时,将特定操作委托给专门的程序:
let cpi_accounts = TransferAccounts {
from: source_account.clone(),
to: destination_account.clone(),
authority: authority_account.clone(),
};
let cpi_ctx = CpiContext::new(token_program.clone(), cpi_accounts);
token_program::cpi::transfer(cpi_ctx, amount)?;
约束与能力
原始交易签名者在 CPI 链中始终保持其权限,使程序能够无缝地代表用户操作。
然而,程序最多只能进行 4 层深的 CPI(A → B → C → D
),以防止无限递归。在 CPI 中,程序还可以使用 CpiContext::new_with_signer
为其 PDA(程序派生账户)签名,从而实现复杂的自主操作。
这种可组合性使得在单个原子交易中跨多个程序执行复杂操作成为可能,从而使 Solana 应用程序高度模块化和互操作性强。