Skip to main content

Quick Access

Check out basic counter example in other implementations:

Step-By-Step Guide

The lifecycle of integrating Ephemeral Rollups in your program is as follows:
1
Write your Solana program as you normally would.
2
Add CPI hooks to delegate, commit and undelegate state accounts through Ephemeral Rollup sessions.
3
Deploy your program directly on Solana using Solana CLI.
4
Send transactions without modifications on-chain and off-chain that also comply with the SVM RPC specification.

Counter Example

The following software packages may be required, other versions may also be compatible:
SoftwareVersionInstallation Guide
Solana2.3.13Install Solana
Rust1.85.0Install Rust
Node24.10.0Install Node

Code Snippets

  • 1. Write Program
  • 2. Delegate
  • 3. Commit
  • 4. Undelegate
The program implements two main instructions:
  1. InitializeCounter: Initialize and sets the counter to 0 (called on Base Layer)
  2. IncreaseCounter: Increments the initialized counter by X amount (called on Base Layer or ER)
The program implements specific instructions for delegating and undelegating the counter:
  1. Delegate: Delegates counter from Base Layer to ER (called on Base Layer)
  2. CommitAndUndelegate: Schedules sync of counter from ER to Base Layer, and undelegates counter on ER (called on ER)
  3. Commit: Schedules sync of counter from ER to Base Layer (called on ER)
  4. Undelegate: Undelegates counter on the Base Layer (called on Base Layer through validator CPI)
Here’s the core structure of our program:
pub enum ProgramInstruction {
    InitializeCounter,
    IncreaseCounter {
        increase_by: u64
    },
    Delegate,
    CommitAndUndelegate,
    Commit,
    Undelegate {
        pda_seeds: Vec<Vec<u8>>
    }
}

#[derive(BorshDeserialize)]
struct IncreaseCounterPayload {
    increase_by: u64,
}

impl ProgramInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        // Ensure the input has at least 8 bytes for the variant
        if input.len() < 8 {
            return Err(ProgramError::InvalidInstructionData);
        }

        // Extract the first 8 bytes as variant
        let (variant_bytes, rest) = input.split_at(8);
        let mut variant = [0u8; 8];
        variant.copy_from_slice(variant_bytes);

        Ok(match variant {
            [0, 0, 0, 0, 0, 0, 0, 0] => Self::InitializeCounter,
            [1, 0, 0, 0, 0, 0, 0, 0] => {
                let payload = IncreaseCounterPayload::try_from_slice(rest)?;
                Self::IncreaseCounter {
                    increase_by: payload.increase_by,
                }
            },
            [2, 0, 0, 0, 0, 0, 0, 0] => Self::Delegate,
            [3, 0, 0, 0, 0, 0, 0, 0] => Self::CommitAndUndelegate,
            [4, 0, 0, 0, 0, 0, 0, 0] => Self::Commit,
            [196, 28, 41, 206, 48, 37, 51, 167] => {
                let pda_seeds: Vec<Vec<u8>> = Vec::<Vec<u8>>::try_from_slice(rest)?;
                Self::Undelegate {
                    pda_seeds
                }
            }
            _ => return Err(ProgramError::InvalidInstructionData),
        })
    }
}
Your “Undelegate” instruction must have the exact discriminator. It is never called by you, instead the validator on the Base Layer will callback with a CPI into your program after undelegating your account on ER.
⬆️ Back to Top

Advanced Code Snippets

  • Resize PDA
  • On-Curve Delegation
When resizing a delegated PDA:
  • PDA must have enough lamports to remain rent-exempt for the new account size.
  • If additional lamports are needed, the payer account must be delegated to provide the difference.
  • PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
  • Use system_instruction::allocate
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
    pub count: u64,
}

// Resize counter account
pub fn resize_counter_account(
    counter_acc: &AccountInfo,
    payer: &AccountInfo,
    program_id: &Pubkey,
    new_size: usize,
    bump: u8,
) -> ProgramResult {
    let rent = Rent::get()?;
    let lamports_required = rent.minimum_balance(new_size);

    let current_lamports = counter_acc.lamports();
    if lamports_required > current_lamports {
        let lamports_to_add = lamports_required - current_lamports;
        invoke_signed(
            &system_instruction::transfer(
                &payer.key,
                &counter_acc.key,
                lamports_to_add,
            ),
            &[payer.clone(), counter_acc.clone()],
            &[&[COUNTER_SEED, &[bump]]],
        )?;
    }

    // Allocate new size
    invoke_signed(
        &system_instruction::allocate(&counter_acc.key, new_size as u64),
        &[counter_acc.clone()],
        &[&[COUNTER_SEED, &[bump]]],
    )?;

    // Assign back to program
    invoke_signed(
        &system_instruction::assign(&counter_acc.key, program_id),
        &[counter_acc.clone()],
        &[&[COUNTER_SEED, &[bump]]],
    )?;

    msg!("Counter account resized to {} bytes", new_size);
    Ok(())
}
⬆️ Back to Top

Solana Explorer

Get insights about your transactions and accounts on Solana:

Solana RPC Providers

Send transactions and requests through existing RPC providers:

Solana Validator Dashboard

Find real-time updates on Solana’s validator infrastructure:

Server Status

Subscribe to Solana’s and MagicBlock’s server status: