Solana 技术训练营 2026

2026年01月09日更新 196 人订阅
课程介绍
C4: Solana 程序开发入门: 简单链上数据存储程序扩展为可交易的代币程序
C6:Anchor 入门: SPL 与 Token 2022

Batch 指令 —— 一次 CPI 中处理多个操

批量指令(Batch Instructions)

跨程序调用(CPI)每次调用都会产生 1,000 个计算单元的基础开销。对于在同一条指令中频繁接收 CPI 的程序来说,这种开销会成为显著的性能瓶颈。

为了解决这一低效问题,Dean 在 p-token 的 这个 PR 中引入了 “batch” 指令,使得可以在一次 CPI 调用中执行多个操作。

Batch 指令

Batch 指令允许在一次 CPI 中处理多个操作,而不需要为每个操作分别发起调用。这可以显著降低在处理多个相关操作时的计算单元消耗。

结构

Batch 指令使用了一种增强的头部结构,包含:

  • 账户数量:内部指令所需的账户数量
  • 数据长度:指令数据的大小

该头部结构使得在一个 batch 中高效地处理多个“内部”指令成为可能。系统会循环并按顺序处理每一个内部指令。

我们使用 u8::MAX(255)作为 batch 指令的判别值(discriminator)。

<ArticleSection name="Entrypoint Design" id="entrypoint-design" level="h2" />

入口函数首先会检查指令的判别值,以决定是处理 batch 指令还是普通指令。这可以防止在 batch 指令中再嵌套 batch 指令的情况出现,因为那样是不安全的。

如下所示:

#[inline(always)]
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let [discriminator, remaining @ ..] = instruction_data else {
        return Err(TokenError::InvalidInstruction.into());
    };

    let result = if *discriminator == 255 {
        // 255 - Batch
        #[cfg(feature = "logging")]
        pinocchio::msg!("Instruction: Batch");

        process_batch(accounts, remaining)
    } else {
        inner_process_instruction(accounts, instruction_data)
    };

    result.inspect_err(log_error)
}

处理 Batch

process_batch 函数负责处理 batch 的核心逻辑:

/// The size of the batch instruction header.
///
/// The header of each instruction consists of two `u8` values:
///  * number of the accounts
///  * length of the instruction data
const IX_HEADER_SIZE: usize = 2;

pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult {
    loop {
        // Validates the instruction data and accounts offset.

        if instruction_data.len() &lt; IX_HEADER_SIZE {
            // The instruction data must have at least two bytes.
            return Err(TokenError::InvalidInstruction.into());
        }

        // SAFETY: The instruction data is guaranteed to have at least two bytes
        // (header) + one byte (discriminator) and the values are within the bounds
        // of an `usize`.
        let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize };
        let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize };

        if instruction_data.len() &lt; data_offset || data_offset == IX_HEADER_SIZE {
            return Err(TokenError::InvalidInstruction.into());
        }

        if accounts.len() &lt; expected_accounts {
            return Err(ProgramError::NotEnoughAccountKeys);
        }

        // Process the instruction.

        // SAFETY: The instruction data and accounts lengths are already validated so
        // all slices are guaranteed to be valid.
        let (ix_accounts, ix_data) = unsafe {
            (
                accounts.get_unchecked(..expected_accounts),
                instruction_data.get_unchecked(IX_HEADER_SIZE..data_offset),
            )
        };

        inner_process_instruction(ix_accounts, ix_data)?;

        if data_offset == instruction_data.len() {
            // The batch is complete.
            break;
        }

        accounts = &accounts[expected_accounts..];
        instruction_data = &instruction_data[data_offset..];
    }

    Ok(())
}

该函数中发生的流程如下:

  • 头部校验:函数首先校验指令数据是否至少包含所需的头部大小(2 字节)。
  • 账户与数据提取:提取期望的账户数量并计算数据偏移量,同时对这些值进行校验以防止未定义行为。
  • 指令处理:使用标准的 inner_process_instruction 函数,并传入对应的账户和数据来处理每一条内部指令。
  • 循环控制:函数会持续处理,直到所有 batch 中的指令都执行完成;在每次迭代中推进账户切片和指令数据切片。

由于运行时只会在 batch 处理结束时统一检查账户所有权,因此某些指令在 batch 中执行时可能需要显式地进行所有权校验。那些不会修改账户,或已经显式执行了所有权校验的指令,则不需要额外的验证。

点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论