Skip to main content

Overview

This guide demonstrates how to implement cranks using MagicBlock’s Ephemeral Rollups (ER) with the Anchor framework. The implementation follows this flow:
  1. Initialize counter on Solana base layer
  2. Delegate counter account to Ephemeral Rollup for faster execution
  3. Schedule a crank task that automatically increments the counter
  4. Execute the crank automatically at specified intervals
  5. Undelegate the account back to Solana base layer when done

Execution Flow

1. User calls initialize() on Solana base layer
   └─> Creates Counter PDA with count = 0

2. User calls delegate() on Solana base layer
   └─> Moves Counter account to Ephemeral Rollup

3. User calls schedule_increment() on Ephemeral Rollup
   └─> CPI to MagicBlock program
       └─> Schedules task with:
           - task_id: 1
           - interval: 100ms
           - iterations: 3
           - instruction: increment()

4. MagicBlock automatically executes increment() 3 times:
   └─> Execution 1: count = 1 (at T+0ms)
   └─> Execution 2: count = 2 (at T+100ms)
   └─> Execution 3: count = 3 (at T+200ms)

5. User calls undelegate() on Ephemeral Rollup
   └─> Commits changes and moves Counter back to Solana base layer

Core Components

Scheduled Function

Let’s take for example that you wanted to schedule the following simple increment instrcution for a 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(())
}

Schedule Increment Function

The core crank scheduling logic:
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:
  • Creates an instruction to increment the counter
  • Serializes it into a ScheduleTask instruction for MagicBlock
  • Uses CPI (Cross-Program Invocation) to call the MagicBlock program
  • The MagicBlock program handles the scheduling and execution

Schedule Arguments

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
    pub task_id: u64,                      // Unique identifier for the task
    pub execution_interval_millis: u64,    // Time between executions in milliseconds
    pub iterations: u64,                   // Number of times to execute
}

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>,
}
Important: Uses AccountInfo instead of Account<Counter> to avoid Anchor re-serializing stale data after CPI calls.