跳转到主要内容

概览

本指南演示如何在 Anchor 框架下,借助 MagicBlock 的 Ephemeral Rollups(ER) 实现 crank。整体流程如下:
  1. 在 Solana 基础层初始化 counter
  2. 将 counter account 委托到 Ephemeral Rollup 以获得更快执行
  3. 调度一个自动递增 counter 的 crank 任务
  4. 按指定间隔自动执行 crank
  5. 完成后将 account 取消委托并提交回 Solana 基础层

执行流程

1. 用户在 Solana 基础层调用 `initialize()`
   └─> 创建 `count = 0` 的 Counter PDA

2. 用户在 Solana 基础层调用 `delegate()`
   └─> 将 Counter account 移动到 Ephemeral Rollup

3. 用户在 Ephemeral Rollup 上调用 `schedule_increment()`
   └─> 对 MagicBlock program 发起 CPI
       └─> 调度一个任务,参数如下:
           - task_id: 1
           - interval: 100ms
           - iterations: 3
           - instruction: increment()

4. MagicBlock 自动执行 `increment()` 3 次:
   └─> 第 1 次执行:`count = 1`(T+0ms)
   └─> 第 2 次执行:`count = 2`(T+100ms)
   └─> 第 3 次执行:`count = 3`(T+200ms)

5. 用户在 Ephemeral Rollup 上调用 `undelegate()`
   └─> 提交变更并将 Counter 移回 Solana 基础层

核心组成部分

被调度的函数

假设你想为一个 counter 调度下面这个简单的递增指令。
pub fn increment(ctx: Context<Increment>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.count += 1;
    if counter.count > 1000 {
        counter.count = 0;
    }
    msg!("PDA {} count: {}", counter.key(), counter.count);
    Ok(())
}

调度递增函数

核心的 crank 调度逻辑如下:
pub fn schedule_increment(ctx: Context<ScheduleIncrement>, args: ScheduleIncrementArgs) -> Result<()> {
    let increment_ix = Instruction {
        program_id: crate::ID,
        accounts: vec![AccountMeta::new(ctx.accounts.counter.key(), false)],
         // Defining the instruction to call.
        data: anchor_lang::InstructionData::data(&crate::instruction::Increment {}),
    };
    
    let ix_data = bincode::serialize(&MagicBlockInstruction::ScheduleTask(
        ScheduleTaskArgs {
            task_id: args.task_id,
            execution_interval_millis: args.execution_interval_millis,
            iterations: args.iterations,
            instructions: vec![increment_ix],
        },
    ))
    .map_err(|err| {
        msg!("ERROR: failed to serialize args {:?}", err);
        ProgramError::InvalidArgument
    })?;

    let schedule_ix = Instruction::new_with_bytes(
        MAGIC_PROGRAM_ID,
        &ix_data,
        vec![
            AccountMeta::new(ctx.accounts.payer.key(), true),
            AccountMeta::new(ctx.accounts.counter.key(), false),
        ],
    );
    
    invoke_signed(
        &schedule_ix,
        &[
            ctx.accounts.payer.to_account_info(),
            ctx.accounts.counter.to_account_info(),
        ],
        &[],
    )?;
    
    Ok(())
}
关键点:
  • 创建一个用于递增 counter 的指令
  • 将其序列化为发送给 MagicBlock 的 ScheduleTask 指令
  • 使用 CPI(Cross-Program Invocation)调用 MagicBlock program
  • 由 MagicBlock program 负责实际的调度与执行

调度参数

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
    pub task_id: u64,                      // 任务的唯一标识符
    pub execution_interval_millis: u64,    // 两次执行之间的毫秒间隔
    pub iterations: u64,                   // 总执行次数
}

ScheduleIncrement 上下文

#[derive(Accounts)]
pub struct ScheduleIncrement<'info> {
    /// CHECK: used for CPI
    #[account()]
    pub magic_program: AccountInfo<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: Passed to CPI - using AccountInfo to avoid Anchor re-serializing stale data after CPI
    #[account(mut, seeds = [COUNTER_SEED], bump)]
    pub counter: AccountInfo<'info>,
    /// CHECK: used for CPI
    pub program: AccountInfo<'info>,
}
重要说明:这里使用 AccountInfo 而不是 Account<Counter>,以避免 Anchor 在 CPI 调用后重新序列化过期数据。