All articles
solanamagicblockephemeral-rollupsanchorrustreal-time

MagicBlock by example: the delegate → ER → commit loop

A code walk through magicblock-engine-examples — the canonical repo for Ephemeral Rollups. The same Anchor program runs on base chain and the ER; you delegate a PDA, run fast txs, then commit/undelegate. Program + client side, and which example to start with.

Share
devrels.xyz/a/68short link

If you've read the Ephemeral Rollups overview, you know the pitch: 10ms blocks, near-zero fees, no new chain to bridge to. What that article doesn't give you is the muscle memory. magicblock-engine-examples does — it's the repo that turns "ephemeral rollup" from a concept into a loop you can run.

MagicBlock by example: the delegate → ER → commit loop

The whole mental model fits in one sentence: it's one Anchor program, served by two RPC endpoints. A PDA lives on Solana like normal. You delegate it to an Ephemeral Rollup validator, which means write-authority temporarily moves to the ER. You fire transactions at the ER's RPC — fast and effectively free — and when you're done you commit the state back to base chain and undelegate. Same program ID, same instructions; only the endpoint changes.

The lifecycle, on the program side

The canonical example is anchor-counter — a PDA counter. Beyond the boring initialize/increment, it adds the four instructions that are Ephemeral Rollups. The #[ephemeral] macro and the ephemeral_rollups_sdk crate do the heavy lifting:

rust
use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;

#[ephemeral]
#[program]
pub mod public_counter {
    use super::*;

    // 1. Hand the PDA's write-authority to an ER validator.
    pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
        ctx.accounts.delegate_pda(
            &ctx.accounts.payer,
            &[COUNTER_SEED],
            DelegateConfig {
                validator: ctx.remaining_accounts.first().map(|a| a.key()),
                ..Default::default()
            },
        )?;
        Ok(())
    }

    // 2. Push the ER's current state back to base chain (still delegated).
    pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> {
        MagicIntentBundleBuilder::new(
            ctx.accounts.payer.to_account_info(),
            ctx.accounts.magic_context.to_account_info(),
            ctx.accounts.magic_program.to_account_info(),
        )
        .commit(&[ctx.accounts.counter.to_account_info()])
        .build_and_invoke()?;
        Ok(())
    }

    // 3. Commit AND return write-authority to base chain.
    pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> {
        MagicIntentBundleBuilder::new(
            ctx.accounts.payer.to_account_info(),
            ctx.accounts.magic_context.to_account_info(),
            ctx.accounts.magic_program.to_account_info(),
        )
        .commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
        .build_and_invoke()?;
        Ok(())
    }
}

Note the combo instructions in the real source — increment_and_commit and increment_and_undelegate — which mutate state and flush it in one transaction. That's the common shape in a real app: do work on the ER, periodically (or at session end) commit it down.

The lifecycle, on the client side

This is where the "two endpoints" idea becomes concrete. The test sets up two providers — one for Solana devnet, one for the ER — and just routes the same program calls to whichever it wants:

typescript
import * as anchor from "@coral-xyz/anchor"
import { GetCommitmentSignature } from "@magicblock-labs/ephemeral-rollups-sdk"

// Base layer: ordinary Solana devnet
const provider = new anchor.AnchorProvider(
  new anchor.web3.Connection("https://api.devnet.solana.com"),
  wallet,
)

// Ephemeral Rollup: a different RPC + WS endpoint
const erProvider = new anchor.AnchorProvider(
  new anchor.web3.Connection(
    "https://devnet-as.magicblock.app/",
    { wsEndpoint: "wss://devnet-as.magicblock.app/" },
  ),
  wallet,
)

// 1. Delegate on the BASE layer
await program.methods.delegate().rpc()   // via `provider`

// 2. Increment on the ER — same instruction, ER endpoint
let tx = await program.methods.increment().transaction()
tx.feePayer = erProvider.wallet.publicKey
tx.recentBlockhash = (await erProvider.connection.getLatestBlockhash()).blockhash
tx = await erProvider.wallet.signTransaction(tx)
const erTxHash = await erProvider.sendAndConfirm(tx)   // ~10ms, ~free

// 3. Read the base-chain commit signature once it lands
const commitSig = await GetCommitmentSignature(erProvider.connection, /* ... */)

// 4. increment_and_undelegate on the ER returns authority to base chain
await program.methods.incrementAndUndelegate().rpc()   // via `erProvider`

That's the entire trick. No bridge, no separate token, no rollup SDK in your frontend — an AnchorProvider pointed at a different URL. The ER endpoint above (devnet-as.magicblock.app) is the public devnet one; the rollups are still under testing, so grab a current endpoint from MagicBlock's Discord.

The map: where to start

The repo has ~17 examples. They're not a tutorial sequence — they're a pattern library. Roughly:

  • Counters (start here)anchor-counter is the reference. Then the same program in rust-counter (native) and pinocchio-counter (no heap, no Borsh) if you care about CU budget.
  • Auth & permissions session-keys (gpl-session delegated signers across base chain and ER), private-counter and pinocchio-private-counter (state gated by an on-rollup permission account).
  • Scheduling & ephemeral state crank-counter (MagicBlock's scheduled crank drives the tx), ephemeral-account-chats (state that lives only on the ER).
  • Tokensdummy-token-transfer and spl-tokens delegate token accounts and move value on both layers.
  • VRF & gamesroll-dice and pinocchio-roll-dice (verifiable randomness on the ER), rewards-delegated-vrf, and rock-paper-scissor (hidden moves until reveal — a clean real-time-game shape).
  • Advanced patternsmagic-actions (trigger base-chain actions from inside an ER) and oncurve-delegation (delegate non-PDA, on-curve accounts).

Pre-Anchor-1.0 versions live under 00-LEGACY_EXAMPLES if you're still on Anchor 0.32.1. For full end-to-end demos rather than primitives, MagicBlock points you at their separate starter-kits repo.

The honest read

Two things to keep straight. First, delegation is a real ownership change — while a PDA is delegated, base-chain transactions can't mutate it; the ER is the source of truth until you commit. That's the right model for a game session or an order book, and the wrong one for state that arbitrary base-chain programs need to write concurrently. Second, commit cadence is a design decision: commit too rarely and a validator hiccup risks more un-flushed state; commit every tx and you give back some of the latency win. The examples deliberately show both commit and increment_and_commit so you can pick.

And the honest scope: Ephemeral Rollups earn their keep for real-time, high-frequency state — games, live markets, agent loops. A CRUD app that writes a few times a minute doesn't need any of this. But if you've been reaching for an L2 or an off-chain server to get interactivity, this repo is the argument that you can stay on Solana and just change the endpoint.

References

Ephemeral Rollups are easy to over-mystify. The examples repo cuts through it: delegate a PDA, point your provider at the ER, do your fast work, commit it home. One program, two endpoints.

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

MagicBlock by example: the delegate → ER → commit loop | devrels.xyz