16个版本 (2个稳定版)

1.16.0 2023年2月21日
1.16.0-alpha.112023年5月30日
1.16.0-alpha.102023年3月24日
1.16.0-alpha.52023年2月27日
1.15.0 2023年1月7日

#24 in #on-chain

Download history 229/week @ 2024-03-29 134/week @ 2024-04-05 144/week @ 2024-04-12 346/week @ 2024-04-19 136/week @ 2024-04-26 167/week @ 2024-05-03 141/week @ 2024-05-10 138/week @ 2024-05-17 142/week @ 2024-05-24 105/week @ 2024-05-31 82/week @ 2024-06-07 149/week @ 2024-06-14 145/week @ 2024-06-21 68/week @ 2024-06-28 33/week @ 2024-07-05 92/week @ 2024-07-12

373 每月下载量
用于 67 个crate (7直接)

Apache-2.0

1MB
17K SLoC

Solana

Solana程序

使用Solana程序Crate用Rust编写链上程序。如果编写客户端应用程序,请改用Solana SDK Crate

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

HelloworldSolana程序库提供了如何使用此crate的示例。

还有疑问?在Discord上向我们提问


lib.rs:

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

所有在链上运行的Solana Rust程序都将链接到此crate,它充当Solana程序的规范库。Solana程序还链接到Rust标准库,尽管它已针对Solana运行时环境进行了修改。虽然与Solana网络交互的链下程序可以链接到此crate,但它们通常改用solana-sdk crate,它重新导出所有来自solana-program的模块。

此库定义了

Solana程序库中可以找到solana-program使用的惯用示例。

定义solana程序

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

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

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

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"包将由cargo build-bpf命令自动发现和构建。Solana程序通常还有"rlib"类型的crate,以便它们可以链接到其他Rust crate。

链上与链下编译目标

Solana程序在rbpf VM上运行,该VM实现了eBPF指令集的一个变体。由于这个crate可以编译为链上和链下执行,而这些环境差异很大,因此它广泛使用条件编译来根据环境调整其实施。用于识别链上程序的cfg谓词是target_os = "solana",如以下来自solana-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模式也适用于需要同时在链上和链下工作的用户代码。

由于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原生代币类型,包含与lamports之间的转换,lamports是SOL的最小分数单位,在native_token模块中。

序列化

在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 Program Library 中用于定义它们的账户格式。它难以实现,并且没有定义一种语言无关的序列化格式。通常不推荐用于新代码。

开发者应该仔细考虑序列化的CPU成本,权衡正确性和易用性:现成的序列化格式往往比精心编写的应用特定格式更昂贵;但应用特定格式更难以确保正确性,并为多语言实现提供支持。程序用手写代码打包和解包数据并不少见。

跨程序指令执行

Solana程序可以使用 invokeinvoke_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()],
    )
}

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程序作者重要的本地程序包括

依赖项

约12–21MB
约333K SLoC