メインコンテンツへスキップ

概要

このガイドでは、Anchor フレームワークと MagicBlock の Ephemeral Rollups(ER) を使って cranks を実装する方法を説明します。流れは次のとおりです。
  1. Solana base layer 上で counter を初期化する
  2. 高速実行のため counter account を Ephemeral Rollup に delegate する
  3. counter を自動 increment する crank task を schedule する
  4. 指定間隔で crank を自動実行する
  5. 完了後に account を undelegate して Solana base layer に戻す

実行フロー

1. User が Solana base layer 上で `initialize()` を呼び出す
   └─> `count = 0` の Counter PDA を作成

2. User が Solana base layer 上で `delegate()` を呼び出す
   └─> Counter account を Ephemeral Rollup に移動

3. User が Ephemeral Rollup 上で `schedule_increment()` を呼び出す
   └─> MagicBlock program へ CPI
       └─> 次の内容で task を schedule:
           - 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. User が Ephemeral Rollup 上で `undelegate()` を呼び出す
   └─> 変更を commit し、Counter を Solana base layer に戻す

コアコンポーネント

スケジュール対象の関数

たとえば、counter に対して次のような単純な increment instruction を schedule したいとします。
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(())
}

Increment をスケジュールする関数

crank scheduling の中心となるロジックは次のとおりです。
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(())
}
Key Points:
  • counter を increment する instruction を作成する
  • それを MagicBlock 向けの ScheduleTask instruction に serialize する
  • CPI(Cross-Program Invocation)で MagicBlock program を呼び出す
  • 実際の scheduling と execution は MagicBlock program が担当する

スケジュール引数

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
    pub task_id: u64,                      // task の一意識別子
    pub execution_interval_millis: u64,    // 実行間隔(ミリ秒)
    pub iterations: u64,                   // 実行回数
}

ScheduleIncrement の Context

#[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>,
}
重要: CPI 呼び出し後に Anchor が古いデータを再 serialize しないよう、Account<Counter> ではなく AccountInfo を使っています。