2 个版本

0.0.2 2024 年 4 月 13 日
0.0.1 2023 年 1 月 8 日

#1 in #bba

Apache-2.0

76KB
1K SLoC

BBA 链程序

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

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

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

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


lib.rs:

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

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

此库定义了

可以在 BBA 链程序库 中找到 bbachain-program 的典型用法示例。

定义 bbachain 程序

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

  • 它们通常既用于链上使用也用于链下使用。这主要是因为链下客户端可能需要访问由链上程序定义的数据类型。
  • 它们没有定义一个main函数,而是使用entrypoint!宏定义它们的入口点。
  • 它们被编译为"cdylib" crate类型,以便由BBA Chain运行时动态加载。
  • 它们在一个受限的虚拟机环境中运行,虽然它们确实可以访问Rust标准库,但标准库的许多功能,尤其是与OS服务相关的功能,在运行时将会失败,将默默无动于衷,或者未定义。有关更多详细信息,请参阅BBA Chain文档中的Rust标准库限制

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

BBA Chain程序的骨架通常如下所示

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

请注意,BBA Chain程序必须指定其crate类型为"cdylib",并且"cdylib" crate将自动由cargo build-bpf命令发现和构建。BBA Chain程序也经常有crate类型"rlib",这样它们就可以与其他Rust crate链接。

链上与链下编译目标

BBA Chain程序在rbpf VM上运行,该VM实现了eBPF指令集的一个变体。因为这个crate可以编译为链上和链下执行,它们的运行环境显著不同,因此它广泛使用条件编译来定制其实施以适应环境。用于识别链上程序编译的cfg谓词是target_arch = "bpf",如下所示,这是从bbachain-program代码库中的一个示例,当在链上运行时通过系统调用记录一条消息,当在链下运行时通过库调用。

pub fn sol_log(message: &str) {
    #[cfg(target_arch = "bpf")]
    unsafe {
        sol_log_(message.as_ptr(), message.len() as u64);
    }

    #[cfg(not(target_arch = "bpf"))]
    program_stubs::sol_log(message);
}

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

由于bbachain-programbbachain-sdk之前是一个crate,以及由于bbachain-program用于两个不同环境的双重用途,它包含一些在编译时不可用给链上程序的功能。它还包含一些在链下场景中运行时会失败的功能。这种区别在文档中反映得并不好。

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

核心数据类型

  • Pubkey — BBA Chain账户的地址。一些账户地址是ed25519公钥,相应的私钥在链外管理。然而,账户地址通常没有对应的私钥,例如程序派生地址,或者私钥与程序的操作不相关,甚至可能已经被销毁。由于运行BBA Chain程序不能安全地创建或管理私钥,因此完整的Keypair结构未在bbachain-program中定义,而是在bbachain-sdk中定义。
  • Hash — SHA-256哈希。用于唯一标识区块,也用于通用哈希。
  • AccountInfo — 单个BBA Chain账户的描述。程序可能访问的所有账户都作为AccountInfo提供给程序入口。
  • Instruction — 告诉运行时执行程序的指令,传递给它一组账户和特定于程序的数据。
  • ProgramErrorProgramResult — 所有程序必须返回的错误类型,报告给运行时作为u64
  • Sol — BBA Chain原生代币类型,可在native_token模块中将daltons(BBA的最小分数单位)与之转换,在native_token模块中。

序列化

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

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

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

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

    用户需要自行导入 borsh 包 —— 虽然 bbachain-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 是一个特定于 BBA Chain 的序列化 API,它被 BBA Chain 程序库 中的许多旧程序用于定义它们的账户格式。它难以实现,并且没有定义一个与语言无关的序列化格式。通常不推荐用于新代码。

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

跨程序指令执行

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

CPI 通过转账 daltons 的简单示例

use bbachain_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 daltons = 1000000;

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

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

创建 PDA 账户的简单示例

use bbachain_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 daltons = 10000000;
    let vault_size = 16;

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

原生程序

一些BBA Chain程序是原生程序,运行与运行时一起分发的本地机器码,具有已知的程序ID。

一些原生程序可以被其他程序调用,但有些只能作为“顶级”指令在链外客户端的Transaction中执行。

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

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

虽然一些原生程序从创世块以来一直活跃,但其他一些在特定的slot之后动态激活,还有一些尚未激活。本文档不区分特定网络上哪些原生程序是活动的。可以使用bbachain feature status CLI命令帮助确定活动的功能。

对BBA Chain程序作者重要的原生程序包括

系统变量

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

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

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

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

use bbachain_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 trait的自定义反序列化器。这些情况在各个系统变量模块中都有记录。

有关更多详细信息,请参阅BBA Chain关于系统变量的文档

依赖关系

~6–15MB
~185K SLoC