Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/magicblock-labs/magicblock-engine-examples/llms.txt

Use this file to discover all available pages before exploring further.

A simple counter program demonstrating how to use native Rust (without Anchor) with Ephemeral Rollups. This example shows the low-level implementation of delegation, transaction execution, and state management using Borsh serialization.

What You’ll Learn

  • How to implement Ephemeral Rollups delegation in native Rust
  • How to use Borsh serialization for instruction data
  • How to manually handle CPI calls to the delegation program
  • How to structure a native Solana program with multiple instructions
  • How to commit and undelegate accounts without Anchor macros

Program Structure

The Rust counter program includes the following instructions:
  • 0: InitializeCounter - Initialize the counter PDA to 0
  • 1: IncreaseCounter - Increase the counter by a specified amount
  • 2: Delegate - Delegate the counter account to Ephemeral Rollups
  • 3: CommitAndUndelegate - Commit and undelegate the account
  • 4: Commit - Commit changes to the base layer
  • 5: IncrementAndCommit - Increment and commit in one instruction
  • 6: IncrementAndUndelegate - Increment and undelegate in one instruction
  • 7: Undelegate - Undelegate with custom PDA seeds

Software Requirements

Ensure you have the following software packages installed before building the program.
SoftwareVersionInstallation Guide
Solana2.3.13Install Solana
Rust1.85.0Install Rust
Node24.10.0Install Node
# Check and initialize your Solana version
agave-install list
agave-install init 2.3.13

# Check and initialize your Rust version
rustup show
rustup install 1.85.0

Build and Test

1

Build the program

Build the Solana program using the BPF toolchain:
cargo build-sbf
2

Deploy the program

Deploy the compiled program to your chosen cluster:
solana program deploy target/deploy/rust_counter.so
3

Configure environment

Add your wallet private key and RPC endpoints to .env:
PRIVATE_KEY=
4

Run tests

Install dependencies and run the test suite:
yarn install
yarn test

Program Implementation

Project Structure

The native Rust program is organized into modules:
pub mod entrypoint; // entrypoint where the Solana program process starts
pub mod processor;  // where instruction logics are processed
pub mod instruction; // where instruction discriminators are defined
pub mod state;      // where on-chain account structures are defined

Instruction Enum

Instructions are defined as an enum with associated data:
use borsh::BorshDeserialize;

pub enum ProgramInstruction {
    InitializeCounter,
    IncreaseCounter { increase_by: u64 },
    Delegate,
    CommitAndUndelegate,
    Commit,
    Undelegate { pda_seeds: Vec<Vec<u8>> },
    IncrementAndCommit { increase_by: u64 },
    IncrementAndUndelegate { increase_by: u64 },
}

impl ProgramInstruction {
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        // Extract the first 8 bytes as variant discriminator
        let (ix_discriminator, rest) = input.split_at(8);

        Ok(match ix_discriminator {
            [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,
            // ... other variants
            _ => return Err(ProgramError::InvalidInstructionData),
        })
    }
}

State Definition

The counter state uses Borsh for serialization:
use borsh::{BorshDeserialize, BorshSerialize};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
    pub count: u64,
}

impl Counter {
    pub const SIZE: usize = 8; // 8 bytes for u64
}

Delegation Implementation

The delegation function shows how to manually handle CPI calls:
use ephemeral_rollups_sdk::cpi::{
    delegate_account, DelegateAccounts, DelegateConfig,
};

pub fn process_delegate(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // Get accounts
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    let pda_to_delegate = next_account_info(account_info_iter)?;
    let owner_program = next_account_info(account_info_iter)?;
    let delegation_buffer = next_account_info(account_info_iter)?;
    let delegation_record = next_account_info(account_info_iter)?;
    let delegation_metadata = next_account_info(account_info_iter)?;
    let delegation_program = next_account_info(account_info_iter)?;
    let validator_account = account_info_iter.next();

    // Optional: client-provided validator or default validator
    let validator_pubkey: Option<Pubkey> = validator_account.map(|acc_info| acc_info.key.clone());

    // Prepare counter pda seeds
    let seed_1 = b"counter";
    let seed_2 = initializer.key.as_ref();
    let pda_seeds: &[&[u8]] = &[seed_1, seed_2];

    let delegate_accounts = DelegateAccounts {
        payer: initializer,
        pda: pda_to_delegate,
        owner_program,
        buffer: delegation_buffer,
        delegation_record,
        delegation_metadata,
        delegation_program,
        system_program,
    };

    let delegate_config = DelegateConfig {
        validator: validator_pubkey, // Set delegating ER validator
        ..Default::default()
    };

    delegate_account(delegate_accounts, pda_seeds, delegate_config)?;

    Ok(())
}
Without Anchor macros, you must manually retrieve and pass all required accounts for delegation.

Increment Implementation

The increment function uses Borsh for deserialization and serialization:
pub fn process_increase_counter(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    increase_by: u64,
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let initializer_account = next_account_info(accounts_iter)?;
    let counter_account = next_account_info(accounts_iter)?;

    // Verify PDA
    let (counter_pda, _bump_seed) =
        Pubkey::find_program_address(&[b"counter", initializer_account.key.as_ref()], program_id);
    if counter_pda != *counter_account.key {
        return Err(ProgramError::InvalidArgument);
    }

    // Increment using Borsh deserialization and serialization
    let mut counter_data = Counter::try_from_slice(&counter_account.data.borrow())?;
    counter_data.count += increase_by;
    counter_data.serialize(&mut &mut counter_account.data.borrow_mut()[..])?;
    msg!("PDA {} count: {}", counter_account.key, counter_data.count);

    Ok(())
}

Commit and Undelegate

Committing and undelegating requires manual account handling:
use ephemeral_rollups_sdk::ephem::{
    commit_accounts, commit_and_undelegate_accounts,
};

pub fn process_commit(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;
    let counter_account = next_account_info(account_info_iter)?;
    let magic_program = next_account_info(account_info_iter)?;
    let magic_context = next_account_info(account_info_iter)?;

    if !initializer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    commit_accounts(
        initializer,
        vec![counter_account],
        magic_context,
        magic_program,
    )?;

    Ok(())
}

pub fn process_commit_and_undelegate(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;
    let counter_account = next_account_info(account_info_iter)?;
    let magic_program = next_account_info(account_info_iter)?;
    let magic_context = next_account_info(account_info_iter)?;

    if !initializer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    commit_and_undelegate_accounts(
        initializer,
        vec![counter_account],
        magic_context,
        magic_program,
    )?;

    Ok(())
}

TypeScript Client Usage

Delegate to Ephemeral Rollups

import { 
  Connection, 
  DELEGATION_PROGRAM_ID,
  delegationRecordPdaFromDelegatedAccount,
  delegationMetadataPdaFromDelegatedAccount,
  delegateBufferPdaFromDelegatedAccountAndOwnerProgram,
} from "@magicblock-labs/ephemeral-rollups-kit";
import * as borsh from "borsh";

const remainingAccounts = connection.clusterUrlHttp.includes("localhost")
  ? [{
      address: address("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
      role: AccountRole.READONLY
    }]
  : [];

const accounts = [
  { address: userPubkey, role: AccountRole.WRITABLE_SIGNER},
  { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
  { address: counterPda, role: AccountRole.WRITABLE },
  { address: PROGRAM_ID, role: AccountRole.READONLY },
  {
    address: await delegateBufferPdaFromDelegatedAccountAndOwnerProgram(counterPda, PROGRAM_ID),
    role: AccountRole.WRITABLE
  },
  {
    address: await delegationRecordPdaFromDelegatedAccount(counterPda),
    role: AccountRole.WRITABLE
  },
  {
    address: await delegationMetadataPdaFromDelegatedAccount(counterPda),
    role: AccountRole.WRITABLE
  },
  { address: DELEGATION_PROGRAM_ID, role: AccountRole.READONLY },
  ...remainingAccounts,
];

const serializedInstructionData = Buffer.from(
  CounterInstruction.Delegate,
  "hex"
);

const delegateIx: Instruction = {
  accounts,
  programAddress: PROGRAM_ID,
  data: serializedInstructionData,
};

const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  tx => setTransactionMessageFeePayer(userPubkey, tx),
  tx => appendTransactionMessageInstructions([delegateIx], tx)
);

const txHash = await connection.sendAndConfirmTransaction(
  transactionMessage,
  [userKeypair],
  { commitment: "confirmed", skipPreflight: true }
);

Execute on Ephemeral Rollups

const accounts = [
  { address: userPubkey, role: AccountRole.WRITABLE_SIGNER},
  { address: counterPda, role: AccountRole.WRITABLE },
];

const serializedInstructionData = Buffer.concat([
  Buffer.from(CounterInstruction.IncreaseCounter, "hex"),
  borsh.serialize(
    IncreaseCounterPayload.schema,
    new IncreaseCounterPayload(1)
  ),
]);

const increaseCounterIx: Instruction = {
  accounts,
  programAddress: PROGRAM_ID,
  data: serializedInstructionData,
};

const transactionMessage = pipe(
  createTransactionMessage({ version: 0 }),
  tx => setTransactionMessageFeePayer(userPubkey, tx),
  tx => appendTransactionMessageInstructions([increaseCounterIx], tx)
);

const txHash = await ephemeralConnection.sendAndConfirmTransaction(
  transactionMessage,
  [userKeypair],
  { commitment: "confirmed", skipPreflight: true }
);

Key Features

Native Rust

Pure Rust implementation without framework dependencies

Borsh Serialization

Efficient binary serialization for instruction data and accounts

Manual Control

Full control over account validation and CPI calls

Lightweight

Minimal dependencies and smaller program size
Native Rust programs require more boilerplate code compared to Anchor, but offer maximum flexibility and control.

Source Code

View the complete source code on GitHub: rust-counter on GitHub

Build docs developers (and LLMs) love