12 个版本 (7 个稳定版)

1.18.3 2024年2月28日
1.18.2 2023年12月14日
1.18.0 2023年11月26日
1.14.17 2023年8月31日
1.14.17-rc32023年4月30日

#1704 in 魔法豆

Download history 258/week @ 2024-04-20 265/week @ 2024-04-27 263/week @ 2024-05-04 244/week @ 2024-05-11 224/week @ 2024-05-18 206/week @ 2024-05-25 267/week @ 2024-06-01 165/week @ 2024-06-08 184/week @ 2024-06-15 228/week @ 2024-06-22 48/week @ 2024-06-29 65/week @ 2024-07-06 266/week @ 2024-07-13 147/week @ 2024-07-20 242/week @ 2024-07-27 195/week @ 2024-08-03

852 每月下载量
130 个crate(48个直接)中使用

Apache-2.0

1MB
20K SLoC

Miraland

Miraland 程序

使用 Miraland 程序 crate 在 Rust 中编写链上程序。如果编写客户端应用程序,请使用 Miraland SDK crate

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

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

还有问题?在我们的 Stack Exchange 上提问


lib.rs:

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

所有在链上运行的 Miraland Rust 程序都将链接到此 crate,该 crate作为 Miraland 程序的标准库。Miraland 程序还链接到 Rust 标准库,尽管它已被 修改 以适应 Miraland 运行时环境。虽然与 Miraland 网络交互的链下程序 可以 链接到此 crate,但它们通常使用 miraland-sdk crate,该crate重新导出所有来自 miraland-program 的模块。

此库定义了

Miraland程序库中可以找到使用miraland-program的习惯用法示例。

定义Solana程序

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

  • 它们通常同时针对链上使用和链下使用进行编译。这主要是因为链下客户端可能需要访问由链上程序定义的数据类型。
  • 它们不定义main函数,而是使用entrypoint!宏定义它们的入口点。
  • 它们被编译为Miraland运行时可以动态加载的"cdylib"crate类型。
  • 它们在受限的虚拟机环境中运行,虽然它们可以访问Rust标准库,但标准库的许多功能,尤其是与操作系统服务相关的功能,在运行时会失败,会静默无操作,或者未定义。有关更多信息,请参阅Miraland文档中的Rust标准库限制

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

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

#[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 = []

请注意,Miraland程序必须指定其crate-type为"cdylib","cdylib"crate将自动被cargo build-bpf命令发现和构建。Miraland程序通常还包含crate-type "rlib",以便可以将它们链接到其他Rust crate。

链上与链下编译目标

Miraland程序在rbpf虚拟机上运行,该虚拟机实现了eBPF指令集的变体。由于此crate可以针对链上和链下执行进行编译,这两种环境显著不同,因此它广泛使用条件编译来调整其实施以适应环境。用于识别链上程序编译的cfg谓词是target_os = "solana",如从miraland-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模式也适用于需要在链上和链下都工作的用户代码。

miraland-programmiraland-sdk以前是一个单独的crate。由于这一历史,以及由于miraland-program在两个不同环境中的双重用途,它包含了一些在编译时不可用给链上程序的特性。它还包含了一些在链下场景中运行时会失败的链上特性。这种区别在文档中没有很好地反映出来。

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

核心数据类型

  • Pubkey — Miraland账户的地址。一些账户地址是ed25519公钥,对应的私钥由链下管理。然而,账户地址通常没有对应的私钥——例如,与程序派生地址——或者私钥与程序的操作不相关,甚至可能已经被废弃。由于运行Miraland程序无法安全地创建或管理私钥,完整的Keypairmiraland-program中未定义,而是在miraland-sdk中定义。
  • Hash — 密码哈希。用于唯一标识块,也用于通用哈希。
  • AccountInfo — 单个Miraland账户的描述。所有可能被程序调用访问的账户都作为AccountInfo提供给程序入口。
  • Instruction — 指示运行时执行程序的指令,传递给它一组账户和程序特定的数据。
  • ProgramErrorProgramResult — 所有程序都必须返回的错误类型,以u64的形式报告给运行时。
  • Mln — Miraland原生代币类型,包含与lamports(MLN的最小分数单位)之间的转换,在native_token模块中。

序列化

在Miraland运行时、程序和网络中,至少使用三种不同的序列化格式,而miraland-program为程序提供了所需格式的访问。

在用户编写的Miraland程序代码中,序列化主要用于访问AccountInfo数据和Instruction数据,这两者都是程序特定的二进制数据。每个程序都可以自由决定自己的序列化格式,但来自其他来源的数据——例如sysvars——必须使用该数据或数据类型的文档中指示的方法进行反序列化。

Miraland中使用的三种序列化格式是

  • Borsh,由NEAR项目开发的一个紧凑且定义良好的格式,适用于协议定义和归档存储。它有一个Rust实现和一个JavaScript实现,并推荐用于所有目的。

    用户需要自行导入 borsh 包 —— 虽然 miraland-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 是一个 Miraland 特定的序列化 API,许多旧程序在 Miraland 程序库 中使用它来定义其账户格式。它难以实现,并且没有定义一个语言独立的序列化格式。通常不建议用于新代码。

开发者应仔细考虑序列化的 CPU 成本,在正确性和易用性需求之间取得平衡:现成的序列化格式通常比精心编写的应用特定格式成本更高;但是,应用特定格式更难确保正确性,并且难以提供多语言实现。程序使用手写代码打包和解包数据并不罕见。

跨程序指令执行

Miraland 程序可以使用 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)?;

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

Miraland 还包括一种机制,允许程序在不需要保护相应的密钥的情况下控制并签名账户,称为 程序派生地址。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。即使一些本地程序不能被其他程序调用,Miraland程序可能也需要访问它们的程序ID。例如,一个程序可能需要验证ed25519签名验证指令是否包含在与它自己的指令相同的交易中。对于许多本地程序,此crate还定义了代表它们处理的指令的枚举,以及构建指令的构造函数。

以下列出了程序ID和指令构造函数的位置,以及它们是否可以被其他程序调用。

虽然一些本地程序自创世块以来一直处于活动状态,但其他一些在特定的 slot 后动态激活,有些尚未激活。此文档不区分特定网络上的哪些本地程序处于活动状态。《code>solana feature status CLI命令可以帮助确定活动功能。

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

依赖关系

~15–24MB
~401K SLoC