312 个稳定版本
新 2.0.7 | 2024 年 8 月 24 日 |
---|---|
2.0.3 | 2024 年 7 月 20 日 |
1.18.22 | 2024 年 8 月 9 日 |
1.18.20 | 2024 年 7 月 24 日 |
1.4.13 | 2020 年 11 月 29 日 |
619 在 神奇豆
1MB
19K SLoC
Solana 程序
使用 Solana 程序包库来用 Rust 编写链上程序。如果编写客户端应用程序,请使用 Solana SDK 程序包。
有关 Solana 的更多信息,请参阅 Solana 文档。
Solana 程序库 提供了如何使用此程序包的示例。
还有问题?在我们的 Stack Exchange 上提问
lib.rs
:
所有 Solana 链上 Rust 程序的基础库。
所有在链上运行的 Solana Rust 程序都将链接到此程序包,该程序包作为 Solana 程序的标准库。Solana 程序还链接到 Rust 标准库,尽管它已针对 Solana 运行时环境进行了修改。虽然与 Solana 网络交互的链下程序 可以 链接到此程序包,但它们通常使用 solana-sdk
程序包,该程序包重新导出从 solana-program
的所有模块。
此库定义了
可以在Solana程序库中找到 solana-program
的典型用法示例。
定义solana程序
与典型的Rust程序相比,Solana程序包有一些独特的属性
- 它们通常既适用于链上使用,也适用于链下使用。这主要是因为链下客户端可能需要访问链上程序定义的数据类型。
- 它们不定义
main
函数,而是使用entrypoint!
宏来定义它们的入口点。 - 它们被编译为 "cdylib" crate 类型,以便由Solana运行时动态加载。
- 它们在受限的VM环境中运行,虽然它们可以访问 Rust标准库,但标准库的许多功能,尤其是与OS服务相关的功能,在运行时将失败、无声地什么也不做或未定义。有关更多信息,请参阅Solana文档中的 Rust标准库的限制。
由于链接在一起的多个crate不能都定义程序入口点(请参阅 entrypoint!
文档),因此通常使用名为 no-entrypoint
的 Cargo功能 来允许禁用程序入口点。
Solana程序的骨架通常如下所示
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Decode and dispatch instructions here.
todo!()
}
}
// Additional code goes here.
包含以下内容的 Cargo.toml
文件
[lib]
crate-type = ["cdylib", "rlib"]
[features]
no-entrypoint = []
请注意,Solana程序必须指定其crate类型为 "cdylib",并且 "cdylib" crate将自动被cargo命令发现和构建。
链上与链下编译目标
Solana程序在rbpf VM上运行,该VM实现了eBPF指令集的变体。因为此crate既可以编译为链上执行,也可以编译为链下执行,这两个环境显著不同,因此广泛使用条件编译来适应这些环境。用于识别链上程序的 cfg
模式也用于确定链下执行时的编译目标。
pub fn sol_log(message: &str) {
#[cfg(target_os = "solana")]
unsafe {
sol_log_(message.as_ptr(), message.len() as u64);
}
#[cfg(not(target_os = "solana"))]
program_stubs::sol_log(message);
}
此 cfg
模式也适用于需要同时在链上和链下运行的代码。
solana-program
和 solana-sdk
之前是一个单独的crate。由于这个历史和由于对两个不同环境的双重使用,它包含一些仅在编译时不可用的功能,以及一些在链下场景中会失败的功能。这个区别在文档中并没有很好地反映出来。
有关Solana对eBPF的实现及其局限性的更完整描述,请参阅Solana主文档中的链上程序。
核心数据类型
Pubkey
— Solana账户的地址。一些账户地址是ed25519公钥,对应私钥由链下管理。然而,通常账户地址没有对应的私钥,例如程序导出地址,或者私钥对于程序的操作不相关,甚至可能已被废弃。由于运行Solana程序不能安全地创建或管理私钥,所以完整的Keypair
结构在solana-program
中未定义,而是在solana-sdk
中定义。Hash
— 密码学哈希。用于唯一标识区块,也用于通用哈希。AccountInfo
— 单个Solana账户的描述。所有可能被程序调用访问的账户都以AccountInfo
的形式提供给程序入口。Instruction
— 告诉运行时执行程序,并将一组账户和程序特定数据传递给程序的指令。ProgramError
和ProgramResult
— 所有程序必须返回的错误类型,以u64
的形式报告给运行时。Sol
— Solana原生代币类型,包括在native_token
模块中将lamports(SOL的最小分数单位)转换到和从SOL的功能。
序列化
在Solana运行时、程序和网络中,至少使用三种不同的序列化格式,而solana-program
为程序提供了对这些格式的访问。
在用户编写的Solana程序代码中,序列化主要用于访问AccountInfo
数据和Instruction
数据,这些都是程序特定的二进制数据。每个程序都可以自由决定自己的序列化格式,但从其他来源(例如sysvars)接收的数据必须使用该数据或数据类型的文档中指明的方法进行反序列化。
Solana中使用的三种序列化格式如下
-
Borsh,由NEAR项目开发的紧凑且规范化的格式,适用于协议定义和归档存储。它有Rust实现和JavaScript实现,并被推荐用于所有目的。
用户需要自行导入
borsh
包——它不是由solana-program
重导出的,尽管此包在其borsh
模块中提供了几个有用的工具,这些工具在borsh
库中不可用。Instruction::new_with_borsh
函数通过使用borsh序列化一个值来创建一个Instruction
。 -
Bincode,一种实现Serde Rust API的紧凑序列化格式。由于它没有规范也没有JavaScript实现,并且比borsh使用更多的CPU,因此不建议用于新代码。
许多系统程序和本地程序指令使用bincode进行序列化,并在运行时用于其他目的。在这些情况下,Rust程序员通常不会直接接触到编码格式,因为它隐藏在API后面。
Instruction::new_with_bincode
函数通过使用bincode序列化值来创建一个Instruction
。 -
Pack
,这是一个Solana特定的序列化API,被许多旧版本的Solana程序库用于定义其账户格式。它难以实现,并且没有定义一个与语言无关的序列化格式。通常不建议用于新代码。
开发人员应仔细考虑序列化的CPU成本,在正确性和易用性需求之间进行平衡:现成的序列化格式往往比精心编写的应用特定格式成本更高;但应用特定格式更难以确保正确性,并为多语言实现提供支持。程序使用手写代码进行数据打包和解包的情况并不少见。
跨程序指令执行
Solana程序可以使用invoke
和invoke_signed
函数调用其他程序,称为跨程序调用(CPI)。在调用另一个程序时,调用者必须提供要调用的Instruction
以及指令所需的每个账户的AccountInfo
。由于程序获取AccountInfo
的唯一方式是通过在[程序入口点][entrypoint!]从运行时接收它们,因此任何被调用程序所需的账户都必须由调用程序间接需要,并由其调用者提供。
CPI通过转移lamports的简单示例
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke,
pubkey::Pubkey,
system_instruction,
system_program,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
assert!(payer.is_writable);
assert!(payer.is_signer);
assert!(recipient.is_writable);
let lamports = 1000000;
invoke(
&system_instruction::transfer(payer.key, recipient.key, lamports),
&[payer.clone(), recipient.clone()],
)
}
Solana还包括一种机制,允许程序在不保护相应密钥的情况下控制和签名账户,称为程序派生地址。PDA通过Pubkey::find_program_address
函数派生。有了PDA,程序可以调用invoke_signed
来调用另一个程序,同时在虚拟上“签名”PDA。
为PDA创建账户的简单示例
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
system_instruction,
system_program,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let vault_pda = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
assert!(payer.is_writable);
assert!(payer.is_signer);
assert!(vault_pda.is_writable);
assert_eq!(vault_pda.owner, &system_program::ID);
assert!(system_program::check_id(system_program.key));
let vault_bump_seed = instruction_data[0];
let vault_seeds = &[b"vault", payer.key.as_ref(), &[vault_bump_seed]];
let expected_vault_pda = Pubkey::create_program_address(vault_seeds, program_id)?;
assert_eq!(vault_pda.key, &expected_vault_pda);
let lamports = 10000000;
let vault_size = 16;
invoke_signed(
&system_instruction::create_account(
&payer.key,
&vault_pda.key,
lamports,
vault_size,
&program_id,
),
&[
payer.clone(),
vault_pda.clone(),
],
&[
&[
b"vault",
payer.key.as_ref(),
&[vault_bump_seed],
],
]
)?;
Ok(())
}
本地程序
一些Solana程序是本地程序,运行与运行时一起分发的本地机器代码,具有已知的程序ID。
一些本地程序可以被其他程序调用,但有些只能作为由离链客户端包含在Transaction
中的“顶级”指令执行。
此crate定义了大多数本地程序的程序ID。即使某些本地程序不能被其他程序调用,Solana程序可能仍需要访问它们的程序ID。例如,一个程序可能需要验证ed25519签名验证指令是否与其自身指令包含在同一个交易中。对于许多本地程序,此crate还定义了表示它们处理的指令的枚举以及构建指令的构造函数。
以下列表中列出了程序ID和指令构造函数的位置,以及它们是否可以被其他程序调用。
虽然一些本地程序自创世块以来一直活跃,但其他程序在特定slot之后动态激活,还有一些尚未激活。本文档并未区分任何特定网络上活跃的本地程序。可以使用solana feature status
CLI命令帮助确定活跃的功能。
对Solana程序作者来说,以下本地程序很重要
-
系统程序:创建新账户,分配账户数据,将账户分配给拥有程序,从系统程序拥有的账户中转移lamports并支付交易费用。
- ID:
solana_program::system_program
- 指令:
solana_program::system_instruction
- 可以被程序调用吗?是
- ID:
-
计算预算程序:为交易请求额外的CPU或内存资源。当从另一个程序调用时,此程序不执行任何操作。
- ID:
solana_sdk::compute_budget
- 指令:
solana_sdk::compute_budget
- 可以被程序调用吗?否
- ID:
-
ed25519程序:验证ed25519签名。
- ID:
solana_program::ed25519_program
- 指令:
solana_sdk::ed25519_instruction
- 可以被程序调用吗?否
- ID:
-
secp256k1程序:验证secp256k1公钥恢复操作。
- ID:
solana_program::secp256k1_program
- 指令:
solana_sdk::secp256k1_instruction
- 可以被程序调用吗?否
- ID:
-
BPF加载器:在链上部署和执行不可变程序。
- ID:
solana_program::bpf_loader
- 指令:
solana_program::loader_instruction
- 可以被程序调用吗?是
- ID:
-
可升级BPF加载器:在链上部署、升级和执行可升级程序。
-
已弃用的BPF加载器:在链上部署和执行不可变程序。
- ID:
solana_program::bpf_loader_deprecated
- 指令:
solana_program::loader_instruction
- 可以被程序调用吗?是
- ID:
依赖项
~11–19MB
~276K SLoC