1 个稳定版本

1.15.0 2023 年 1 月 22 日

#4#cbe

每月下载 32 次
5 个 crate 中使用 (通过 cbe-sdk)

Apache-2.0

1MB
16K SLoC

Cartallum CBE

Cartallum CBE 程序

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

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

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

还有问题吗?在我们的 Discord 上提问


lib.rs:

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

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

此库定义了

Cartallum CBE 程序库 中可以找到 cbe-program 的典型用法示例。

定义 cbe 程序

与典型的 Rust 程序相比,Cartallum CBE 程序 crate 有一些独特的属性。

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

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

Cartallum CBE 程序的骨架通常如下所示

#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {
    use cbe_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 = []

请注意,Cartallum CBE 程序必须指定其 crate-type 为 "cdylib",并且 "cdylib" 颗粒将被 cargo build-bpf 命令自动发现和构建。Cartallum CBE 程序通常还具有 "rlib" 颗粒类型,以便可以将它们链接到其他 Rust 颗粒。

链上与链下编译目标

Cartallum CBE 程序在 rbpf VM 上运行,该 VM 实现了 eBPF 指令集的变体。由于这个 crate 可以编译为链上和链下执行,而这两个环境的差异很大,因此它广泛使用 条件编译 来调整其实施以适应环境。用于识别链上程序的 cfg 判定式是 target_os = "cbe",如这个来自 cbe-program 代码库的示例所示,当在链上运行时通过系统调用记录消息,而在链下通过库调用

pub fn cbe_log(message: &str) {
    #[cfg(target_os = "cbe")]
    unsafe {
        cbe_log_(message.as_ptr(), message.len() as u64);
    }

    #[cfg(not(target_os = "cbe"))]
    program_stubs::cbe_log(message);
}

这个 cfg 模式也适用于需要同时在链上和链下工作的用户代码。

由于 cbe-programcbe-sdk 以前是一个单独的 crate,以及由于 cbe-program 对两个不同环境的双重使用,它包含一些在编译时不可用的功能。它还包含一些在链下场景中运行时将失败的功能。这种区别在文档中反映得并不好。

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

核心数据类型

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

序列化

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

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

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

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

    用户需要自己导入borsh包——它不是由cbe-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 是 Cartallum CBE 特定的序列化 API,许多旧程序在 Cartallum CBE 程序库 中使用它来定义它们的账户格式。它难以实现,并没有定义一个语言无关的序列化格式。一般不建议用于新代码。

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

跨程序指令执行

Cartallum CBE 程序可以通过 invokeinvoke_signed 函数调用其他程序,称为 跨程序调用 (CPI)。当调用另一个程序时,调用者必须提供要调用的 Instruction 以及指令所需的每个账户的 AccountInfo。由于程序获取 AccountInfo 值的唯一方式是来自运行时在 [程序入口点][entrypoint!],因此调用者程序必须间接需要被调用程序所需的任何账户,并由 调用者提供。

CPI 通过转移 scoobies 的简单示例

use cbe_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 scoobies = 1000000;

    invoke(
        &system_instruction::transfer(payer.key, recipient.key, scoobies),
        &[payer.clone(), recipient.clone(), system_account.clone()],
    )
}

Cartallum CBE 还包括一种机制,允许程序控制并签署账户,而无需保护相应的密钥,称为 程序推导地址。PDAs 通过 Pubkey::find_program_address 函数推导。使用 PDA,程序可以调用 invoke_signed 来调用另一个程序,同时在虚拟上“签署”PDA。

为 PDA 创建账户的简单示例

use cbe_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 scoobies = 10000000;
    let vault_size = 16;

    invoke_signed(
        &system_instruction::create_account(
            &payer.key,
            &vault_pda.key,
            scoobies,
            vault_size,
            &program_id,
        ),
        &[
            payer.clone(),
            vault_pda.clone(),
        ],
        &[
            &[
                b"vault",
                payer.key.as_ref(),
                &[vault_bump_seed],
            ],
        ]
    )?;
    Ok(())
}

本地程序

一些 Cartallum CBE 程序是 本地程序,它们运行与运行时一起分发的本地机器代码,具有众所周知的程序 ID。

一些本地程序可以被其他程序调用,但一些只能作为“顶层”指令执行,由链下客户端在 Transaction 中包含。

该软件包定义了大多数原生程序的程序ID。尽管某些原生程序不能被其他程序调用,但Cartallum CBE程序可能需要访问它们的程序ID。例如,一个程序可能需要验证ed25519签名验证指令是否与其自身的指令在同一交易中。对于许多原生程序,该软件包还定义了表示它们所处理指令的枚举,以及构建指令的构造函数。

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

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

对Cartallum CBE程序作者来说重要的原生程序包括

依赖项

~13–22MB
~350K SLoC