12个稳定版本
1.14.17 | 2023年3月8日 |
---|---|
1.14.3 | 2022年10月15日 |
1.10.34 | 2022年7月23日 |
1.9.29 | 2022年6月6日 |
1.6.19 | 2021年8月15日 |
#15 in 魔法豆
545 每月下载量
在 58 个crate中使用 (17直接)
740KB
14K SLoC
Safecoin程序
使用Safecoin程序crate在Rust中编写链上程序。如果编写客户端应用程序,请使用Safecoin SDK crate。
有关Safecoin的更多信息,请参阅Safecoin文档。
Helloworld和Safecoin程序库提供了如何使用此crate的示例。
还有疑问吗?在我们的Discord上提问。
lib.rs
:
所有Safecoin链上Rust程序的基础库。
所有在链上运行的Safecoin Rust程序都将链接到这个crate,它作为Safecoin程序的规范库。Safecoin程序也链接到Rust标准库,尽管它已经针对Safecoin运行环境进行了修改。虽然与Safecoin网络交互的链下程序可以链接到这个crate,但它们通常使用safecoin-sdk
crate,它导出所有来自safecoin-program
的模块。
该库定义了
在 Safecoin 程序库 中可以找到 safecoin-program
使用的惯用示例。
定义 Safecoin 程序
Safecoin 程序与典型的 Rust 程序相比具有一些独特的属性
- 它们通常既用于链上使用也用于链下使用。这主要是因为链下客户端可能需要访问由链上程序定义的数据类型。
- 它们不定义
main
函数,而是使用entrypoint!
宏来定义它们的入口点。 - 它们被编译为 "cdylib" 包类型,以便由 Safecoin 运行时动态加载。
- 它们在一个受限的虚拟机环境中运行,虽然它们可以访问 Rust 标准库,但标准库的许多功能,尤其是与 OS 服务相关的功能,将在运行时失败、默默地什么也不做或未定义。有关更多信息,请参阅 Safecoin 文档中的 Rust 标准库限制。
由于链接在一起的多重包不能都定义程序入口点(请参阅 entrypoint!
文档),一个常见的约定是使用名为 no-entrypoint
的 Cargo 功能 来允许禁用程序入口点。
Safecoin 程序的骨架通常如下所示
#[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 = []
请注意,Safecoin 程序必须指定其包类型为 "cdylib",并且 "cdylib" 包将自动由 cargo build-bpf
命令发现和构建。Safecoin 程序通常还包含 "rlib" 包类型,以便可以将它们链接到其他 Rust 包。
链上与链下编译目标
Safecoin 程序在 rbpf 虚拟机上运行,该虚拟机实现了 eBPF 指令集的一种变体。由于此包可以编译为链上和链下执行,其环境差异很大,因此它广泛使用 条件编译 来根据环境定制其实施。用于识别链上程序编译的 cfg
判定式是 target_os = "solana"
,如从 safecoin-program
代码库中的此示例,在链上运行时通过系统调用记录消息,在链下运行时通过库调用记录消息。
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
模式也适用于需要同时在链上和链下工作的用户代码。
由于 safecoin-program
和 safecoin-sdk
以前是一个单独的包,并且由于 safecoin-program
在两个不同环境中的双重使用,它包含一些在编译时不可用的功能。它还包含一些在链上环境中运行时将在链下场景中失败的功能。这种区别在文档中反映得并不好。
有关 Safecoin 对 eBPF 的实现及其限制的更完整描述,请参阅主 Safecoin 文档中的 链上程序。
核心数据类型
Pubkey
— Safecoin 账户的地址。某些账户地址是ed25519公钥,对应的私钥由链下管理。然而,通常账户地址没有对应的私钥——例如程序推导地址——或者私钥与程序的操作无关,甚至已经被废弃。由于运行 Safecoin 程序不能安全地创建或管理私钥,所以完整的Keypair
结构体并未在safecoin-program
中定义,而是在safecoin-sdk
中定义。Hash
— 加密散列。用于唯一标识区块,也用于通用哈希。AccountInfo
— 单个 Safecoin 账户的描述。所有可能被程序调用访问的账户都作为AccountInfo
提供给程序入口点。Instruction
— 告诉运行时执行程序的指令,传递给程序一组账户和特定于程序的日期。ProgramError
和ProgramResult
— 所有程序必须返回的错误类型,以u64
的形式报告给运行时。Safe
— Safecoin 的本地代币类型,可在native_token
模块中将它与 lamports(SAFE 的最小分数单位)之间进行转换。
序列化
在 Safecoin 运行时、程序和网络中,至少使用三种不同的序列化格式,而 safecoin-program
为程序提供了所需的访问权限。
在用户编写的 Safecoin 程序代码中,序列化主要用于访问 AccountInfo
数据和 Instruction
数据,这两者都是程序特定的二进制数据。每个程序都可以自由决定自己的序列化格式,但来自其他来源的数据——例如sysvars——必须使用该数据或数据类型的文档中指定的方法进行反序列化。
Safecoin 中使用的三种序列化格式如下
-
Borsh,由 NEAR 项目开发的紧凑且规范化的格式,适用于协议定义和归档存储。它有一个Rust 实现和一个JavaScript 实现,并被推荐用于所有目的。
用户需要自己导入
borsh
包——它不是由safecoin-program
重新导出的,尽管这个包在其borsh
模块 中提供了一些有用的工具,这些工具在borsh
库中是不可用的。
函数通过使用 borsh 序列化一个值来创建一个Instruction::new_with_borsh
Instruction
。 -
Bincode 是一种紧凑的序列化格式,它实现了 Serde Rust API。由于它没有规范也没有 JavaScript 实现,并且比 borsh 使用更多的 CPU,因此不建议用于新代码。
许多系统程序和本地程序指令使用 bincode 进行序列化,并在运行时用于其他目的。在这些情况下,Rust 程序员通常不会直接接触到编码格式,因为它是隐藏在 API 背后的。
函数通过使用 bincode 序列化一个值来创建一个Instruction::new_with_bincode
Instruction
。 -
Pack
是一个 Safecoin 特定的序列化 API,它被许多旧版本的 Safecoin 程序库中的程序用于定义它们的账户格式。它很难实现,并且没有定义一种语言无关的序列化格式。一般不建议用于新代码。
开发者应该仔细考虑序列化的 CPU 成本,平衡正确性和易用性:现成的序列化格式往往比精心编写的应用特定格式更昂贵;但是,应用特定格式更难确保其正确性,并且难以提供多语言实现。程序用手工编写的代码打包和解包数据的情况并不少见。
跨程序指令执行
Safecoin 程序可以使用 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)?;
// The system program is a required account to invoke a system
// instruction, even though we don't use it directly.
let system_account = next_account_info(account_info_iter)?;
assert!(payer.is_writable);
assert!(payer.is_signer);
assert!(recipient.is_writable);
assert!(system_program::check_id(system_account.key));
let lamports = 1000000;
invoke(
&system_instruction::transfer(payer.key, recipient.key, lamports),
&[payer.clone(), recipient.clone(), system_account.clone()],
)
}
Safecoin 还包括一种机制,允许程序在不需要保护相应的密钥的情况下控制和签署账户,称为 程序推导地址。PDAs 使用 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(())
}
本地程序
一些 Safecoin 程序是 本地程序,运行与运行时一起分发的本地机器代码,具有众所周知的程序 ID。
一些本地程序可以被其他程序调用,但一些程序只能作为由链下客户端包含在 Transaction
中的“顶级”指令来执行。
这个crate定义了大多数本地程序的程序ID。尽管一些本地程序不能被其他程序调用,但Safecoin程序可能需要访问它们的程序ID。例如,一个程序可能需要验证ed25519签名验证指令是否包含在其自身指令所在的同一交易中。对于许多本地程序,此crate还定义了表示它们处理的指令的枚举,以及构建指令的构造函数。
以下列出了程序ID和指令构造函数的位置,以及它们是否可以被其他程序调用。
虽然一些本地程序自创世块以来一直处于活跃状态,但其他一些程序在特定的slot之后动态激活,还有一些尚未激活。此文档不会区分任何特定网络上的哪些本地程序处于活跃状态。《code>safecoin feature status CLI命令可以帮助确定活动功能。
对Safecoin程序作者重要的本地程序包括
-
系统程序:创建新账户,分配账户数据,将账户分配给拥有程序,从系统程序拥有的账户中转移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加载器: 在链上部署和执行不可变程序。
- 标识符:
solana_program::bpf_loader_deprecated
- 指令:
solana_program::loader_instruction
- 可以被程序调用?是
- 标识符:
系统变量
系统变量是特殊的账户,包含有关网络集群、区块链历史和执行事务的动态更新数据。
系统变量的程序标识符定义在sysvar
模块中,简单的系统变量实现了Sysvar::get
方法,该方法直接从运行时加载系统变量,例如本例中记录的clock
系统变量。
use solana_program::{
account_info::AccountInfo,
clock,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
sysvar::Sysvar,
};
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let clock = clock::Clock::get()?;
msg!("clock: {:#?}", clock);
Ok(())
}
由于Safecoin系统变量是账户,如果将AccountInfo
提供给程序,则程序可以使用Sysvar::from_account_info
反序列化系统变量以访问其数据,例如本例中再次记录的clock
系统变量。
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
sysvar::Sysvar,
};
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let clock_account = next_account_info(account_info_iter)?;
let clock = clock::Clock::from_account_info(&clock_account)?;
msg!("clock: {:#?}", clock);
Ok(())
}
尽可能的情况下,程序应优先调用Sysvar::get
而不是使用Sysvar::from_account_info
反序列化,因为后者除了需要将系统变量账户地址传递给程序外,还强加了反序列化的额外开销,浪费了事务可用的有限空间。只有为了与传递系统变量账户的旧程序兼容,才应考虑使用Sysvar::get
检索可以检索的系统变量。
某些系统变量太大,无法在程序中反序列化,并且Sysvar::from_account_info
返回错误。某些系统变量太大,无法在程序中反序列化,尝试这样做将耗尽程序的计算预算。某些系统变量没有实现Sysvar::get
并返回错误。某些系统变量具有不实现Sysvar
特质的自定义反序列化器。这些情况在各个系统变量模块中有记录。
有关更多详细信息,请参阅Safecoin系统变量文档。
依赖项
~11-20MB
~308K SLoC