1 个稳定版本

1.14.20 2023年7月31日
1.14.12 2023年8月19日

#41 in #on-chain

25 每月下载量
5 个包中使用 (通过 solomka-sdk)

Apache-2.0

695KB
13K SLoC

Solana

索拉纳程序

使用 Solana 程序包来用 Rust 语言编写链上程序。如果编写客户端应用程序,请使用Solana SDK 包

有关 Solana 的更多信息,请参阅Solana 文档

Hello WorldSolana 程序库 提供了如何使用此包的示例。

还有疑问?在 Stack Exchange 上向我们提问


lib.rs:

所有 Solana 链上 Rust 程序的基础库。

所有在链上运行的 Solana Rust 程序都将链接到这个包,它作为 Solana 程序的标准库。Solana 程序也会链接到 Rust 标准库,尽管它在 Solana 运行时环境中进行了修改。虽然与 Solana 网络交互的链下程序 可以 链接到这个包,但它们通常使用 solana-sdk 包,它导出从 solana-program 中所有模块。

该库定义了

Solana 程序库 中可以找到 solana-program 的典型用法示例。

定义索拉纳程序

与典型的 Rust 程序相比,索拉纳程序包具有一些独特的属性

  • 它们通常既为链上使用又为链下使用编译。这主要是因为链下客户端可能需要访问由链上程序定义的数据类型。
  • 它们没有定义 main 函数,而是使用 entrypoint! 宏来定义它们的入口点。
  • 它们被编译为 "cdylib" 包类型,以便由 Solana 运行时动态加载。
  • 它们在受限的 VM 环境中运行,虽然它们可以访问 Rust 标准库,但标准库的许多功能,尤其是与操作系统服务相关的功能,在运行时将失败、无声地什么也不做或未定义。有关更多信息,请参阅 Solana 文档中的 Rust 标准库的限制

由于链接在一起的多crate不能都定义程序入口点(请参阅 entrypoint! 文档),因此一个常见的约定是使用名为 no-entrypointCargo 功能 来允许禁用程序入口点。

Solana 程序的骨架通常如下所示

#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {
    use solomka_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-type 为 "cdylib",而 "cdylib" crate 将自动被 cargo build-bpf 命令发现和构建。Solana 程序通常还有 "rlib" crate-type,以便将它们链接到其他 Rust crate。

链上与链下编译目标

Solana 程序在 rbpf VM 上运行,该 VM 实现了 eBPF 指令集的一种变体。因为这个 crate 可以编译为链上和链下执行,而这两个环境显著不同,它广泛使用 条件编译 来针对环境定制其实施。用于识别链上程序编译的 cfg 断言是 target_os = "solana",如下例所示,在链上运行时通过系统调用记录消息,而在链下运行时通过库调用

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-programsolana-sdk 之前是单个 crate,并且由于 solana-program 在两个不同环境中的双重使用,它包含一些在编译时不可用于链上程序的功能。它还包含一些在链下场景中将失败的链上功能。这种区别在文档中反映得并不好。

有关 Solana 对 eBPF 的实现及其限制的更完整描述,请参阅 Solana 的主文档中的 链上程序

核心数据类型

  • Pubkey — Solana账户的地址。某些账户地址是ed25519公钥,对应的私钥由链外管理。然而,通常账户地址并没有对应的私钥——例如程序派生地址——或者私钥与程序的操作无关,甚至可能已被废弃。由于运行Solana程序无法安全地创建或管理私钥,完整的Keypair结构体不在solana-program中定义,而是在solana-sdk中定义。
  • Hash — 加密散列。用于唯一标识区块,也用于通用哈希。
  • AccountInfo — 单个Solana账户的描述。所有可能被程序调用访问的账户都作为AccountInfo提供给程序入口。
  • Instruction — 告诉运行时执行程序的指令,传递给它一组账户和程序特定数据。
  • ProgramErrorProgramResult——所有程序必须返回的错误类型,以u64的形式报告给运行时。
  • Sol——Solana原生代币类型,可在native_token模块中将它与lamports(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程序可以使用invokeinvoke_signed函数调用其他程序,称为跨程序调用(CPI)。在调用另一个程序时,调用者必须提供要调用的Instruction,以及指令所需的每个账户的AccountInfo。由于程序获取AccountInfo值的唯一方式是来自运行时的[程序入口点][entrypoint!],因此任何由被调用程序所需的账户都必须通过调用者程序间接地被要求,并由它的调用者提供。

CPI通过CPI转账lamports的简单示例

use solomka_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()],
    )
}

Solana还包括一种机制,允许程序在不需要保护相应密钥的情况下控制并签名账户,称为程序派生地址。PDA通过Pubkey::find_program_address函数派生。使用PDA,程序可以调用invoke_signed来调用另一个程序,同时在虚拟上“签名”PDA。

为PDA创建账户的简单示例

use solomka_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和指令构造函数的位置在下面的列表中列出,以及它们是否可以被其他程序调用。

虽然一些本地程序自创世区块以来一直活跃,但其他程序在特定槽位后被动态激活,还有一些尚未激活。本文档不区分任何特定网络上的哪个本地程序是活跃的。《solana feature status》CLI命令可以帮助确定活跃的功能。

对Solana程序作者重要的本地程序包括

系统变量

系统变量是特殊的账户,包含关于网络集群、区块链历史和执行事务的动态更新数据。

系统变量的程序ID在sysvar模块中定义,简单的系统变量实现了Sysvar::get方法,该方法直接从运行时加载系统变量,例如以下示例中记录了clock系统变量。

use solomka_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(())
}

由于Solana系统变量是账户,如果将AccountInfo提供给程序,则程序可以使用Sysvar::from_account_info反序列化系统变量以访问其数据,如以下示例中再次记录了clock系统变量。

use solomka_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特质的自定义反序列化器。这些情况在各个系统变量的模块中有文档说明。

有关更多详细信息,请参阅Solana系统变量文档

依赖关系

~11–21MB
~309K SLoC