Private Ephemeral Rollups are Ephemeral Rollups that enable fine-grained permission over permissioned accounts in a Trusted Execution Environment with compliance at its heart. Each permission account maintains a list of members with specific flags that determine what actions they can perform.
Member flags define fine-grained permissions for each member. Flags can be combined using bitwise OR to grant multiple permissions.Flag Descriptions:
AUTHORITY: Allows a member to update and delegate permission settings, add/remove other members, and update member flags.
TX_LOGS: Allows a member to view transaction execution logs.
TX_BALANCES: Allows a member to view account balance changes.
TX_MESSAGE: Allows a member to view transaction message data.
ACCOUNT_SIGNATURES: Allows a member to view account signatures
Rust SDK
Pinocchio
Web3.js
Kit
use ephemeral_rollups_sdk::access_control::structs::{ Member, AUTHORITY_FLAG, TX_LOGS_FLAG, TX_BALANCES_FLAG, TX_MESSAGE_FLAG, ACCOUNT_SIGNATURES_FLAG,};// Set flags by combining them with bitwise ORlet flags = AUTHORITY_FLAG | TX_LOGS_FLAG;// Create a member with combined flagslet mut member = Member { flags, pubkey: user_pubkey,};// Check if member has a specific flag using bitwise ANDlet is_authority = (member.flags & AUTHORITY_FLAG) != 0;let can_see_logs = (member.flags & TX_LOGS_FLAG) != 0;// Use helper methods to set/remove flagsmember.set_flags(TX_BALANCES_FLAG); // Add a flagmember.remove_flags(TX_LOGS_FLAG); // Remove a flag
use ephemeral_rollups_pinocchio::types::{Member, MemberFlags};use pinocchio::Address;// Create and set flags using individual methodslet mut flags = MemberFlags::new();flags.set(MemberFlags::AUTHORITY);flags.set(MemberFlags::TX_LOGS);flags.set(MemberFlags::TX_BALANCES);// Create a member with flagslet member = Member { flags, pubkey: user_address,};// Remove a flagflags.remove(MemberFlags::TX_LOGS);// Create flags from individual boolean valueslet flags = MemberFlags::from_acl_flags( true, // authority true, // tx_logs false, // tx_balances true, // tx_message false, // account_signatures);// Convert flags to byte valuelet flag_byte = flags.to_acl_flag_byte();// Create flags from byte valuelet flags = MemberFlags::from_acl_flag_byte(flag_byte);
import { PublicKey } from "@solana/web3.js";import { AUTHORITY_FLAG, TX_LOGS_FLAG, TX_BALANCES_FLAG, TX_MESSAGE_FLAG, ACCOUNT_SIGNATURES_FLAG, type Member,} from "@magicblock-labs/ephemeral-rollups-sdk";// Set flags by combining them with bitwise ORconst flags = AUTHORITY_FLAG | TX_LOGS_FLAG;// Create a member with combined flagsconst member: Member = { flags, pubkey: new PublicKey(userAddress),};// Check if a flag is present using bitwise ANDconst isAuthority = (member.flags & AUTHORITY_FLAG) !== 0;const canSeeLogs = (member.flags & TX_LOGS_FLAG) !== 0;const canSeeBalances = (member.flags & TX_BALANCES_FLAG) !== 0;// Add a flag to existing flagsconst updatedFlags = member.flags | TX_BALANCES_FLAG;// Remove a flag from existing flagsconst removedFlags = member.flags & ~TX_LOGS_FLAG;
import { AUTHORITY_FLAG, TX_LOGS_FLAG, TX_BALANCES_FLAG, TX_MESSAGE_FLAG, ACCOUNT_SIGNATURES_FLAG, isAuthority, canSeeTxLogs, canSeeTxBalances, canSeeTxMessages, canSeeAccountSignatures, type Member,} from "@magicblock-labs/ephemeral-rollups-sdk";// Set flags by combining them with bitwise ORconst flags = AUTHORITY_FLAG | TX_LOGS_FLAG | TX_BALANCES_FLAG;// Create a member with combined flagsconst member: Member = { flags, pubkey: userAddress,};// Use helper functions to check specific permissionsconst canModifyPermission = isAuthority(member, userAddress);const canViewLogs = canSeeTxLogs(member, userAddress);const canViewBalances = canSeeTxBalances(member, userAddress);const canViewMessages = canSeeTxMessages(member, userAddress);const canViewSignatures = canSeeAccountSignatures(member, userAddress);// Add a flag to existing memberconst updatedFlags = member.flags | TX_MESSAGE_FLAG;// Remove a flag from existing memberconst removedFlags = member.flags & ~TX_LOGS_FLAG;
EphemeralPermission accounts live entirely on the Ephemeral Rollup and are
paid for by the delegated PDA — no base-layer permission account to create,
delegate, or commit-and-undelegate. Three CPI ops cover the full lifecycle:
Create, Update, Close — all PDA-signed by the data account on
the ER, via MagicBlock’s Permission Program ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1.
Prerequisite — delegate the data PDA. Only the data account is delegated to
the TEE validator (on the base layer, via MagicBlock’s Delegation Program
DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh). Once delegated, the PDA signs
all three permission ops on the ER using its program seeds and pays the
ephemeral permission rent — so it must be pre-funded at initialize time. See
Quickstart
for the end-to-end flow.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
Mainnet
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
Devnet
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
Localnet
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Initialize a new EphemeralPermission account on the ER via
CreateEphemeralPermissionCpi. Payer = the delegated data PDA, which
signs with its program seeds and covers the rent from the lamports
pre-funded at initialize time.
Anchor
Rust SDK
Pinocchio
Kit
Web3.js
use ephemeral_rollups_sdk::access_control::{ instructions::CreateEphemeralPermissionCpi, structs::{EphemeralMembersArgs, Member},};// Counter PDA pays for its own permission rent (it carries lamports onto the ER// after delegation and signs as PDA via seeds).let signers = [ COUNTER_SEED, ctx.accounts.counter.authority.as_ref(), &[ctx.bumps.counter],];CreateEphemeralPermissionCpi { payer: ctx.accounts.counter.to_account_info(), // pays ephemeral rent permissioned_account: ctx.accounts.counter.to_account_info(), // what the permission gates permission: ctx.accounts.permission.to_account_info(), vault: ctx.accounts.ephemeral_vault.to_account_info(), magic_program: ctx.accounts.magic_program.to_account_info(), permission_program: ctx.accounts.permission_program.to_account_info(), args: EphemeralMembersArgs { is_private: false, // start public — flip via UpdateEphemeralPermission members: vec![], },}.invoke_signed(&[&signers])?;
use ephemeral_rollups_sdk::access_control::{ instructions::CreateEphemeralPermissionCpi, structs::{EphemeralMembersArgs, Member},};// `permissioned_account` (here a counter PDA) signs as PDA via seeds; pass the// same seeds you used for `find_program_address` to derive it.let seeds: &[&[u8]] = &[ COUNTER_SEED, permissioned_account_authority.as_ref(), &[bump],];CreateEphemeralPermissionCpi { payer: &counter_account_info, // pays ephemeral rent permissioned_account: &counter_account_info, // what the permission gates permission: &permission_account_info, vault: &ephemeral_vault_account_info, magic_program: &magic_program_account_info, permission_program: &permission_program_account_info, args: EphemeralMembersArgs { is_private: false, // start public — flip via UpdateEphemeralPermission members: vec![], },}.invoke_signed(&[seeds])?;
use ephemeral_rollups_pinocchio::acl::{ CreateEphemeralPermission, EphemeralMembersArgs, Member,};use pinocchio::cpi::{Seed, Signer};// Buffer size: discriminator (8) + EphemeralMembersArgs body.// 64 bytes covers up to 1 member with slack for future Update calls.const PERMISSION_CPI_BUF: usize = 64;// PDA-signed CPI — the counter PDA pays rent and authorizes the permission.let bump_seed = [bump];let seeds_array: [Seed; 3] = [ Seed::from(b"counter"), Seed::from(authority.address().as_ref()), Seed::from(&bump_seed),];let signer = Signer::from(&seeds_array);let members: [Member; 0] = []; // start public; toggle via UpdateCreateEphemeralPermission { payer: counter_account, permissioned_account: counter_account, permission, vault, magic_program, permission_program, args: EphemeralMembersArgs { is_private: false, members: &members, },}.invoke_signed::<PERMISSION_CPI_BUF>(&[signer])?;
import { MAGIC_PROGRAM_ID, PERMISSION_PROGRAM_ID, EPHEMERAL_VAULT_ID,} from "@magicblock-labs/ephemeral-rollups-sdk";import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";// EphemeralPermissions are created on the ER by the delegated PDA (via the// user-program's wrapper instruction). Submit to the ER connection, not base.const initIx = await counterProgram.methods .initPermission() .accountsPartial({ authority: tempKeypair.address, counter: counterPda, permission: permissionPda, permissionProgram: PERMISSION_PROGRAM_ID, ephemeralVault: EPHEMERAL_VAULT_ID, magicProgram: MAGIC_PROGRAM_ID, }) .instruction();const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => appendTransactionMessageInstructions([initIx], tx),);const sig = await ephemeralConnection.sendAndConfirmTransaction( transactionMessage, [tempKeypair], { commitment: "confirmed" },);console.log("init_permission tx:", sig);
import { MAGIC_PROGRAM_ID, PERMISSION_PROGRAM_ID, EPHEMERAL_VAULT_ID,} from "@magicblock-labs/ephemeral-rollups-sdk";import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";// EphemeralPermissions are created on the ER by the delegated PDA (via the// user-program's wrapper instruction). Submit to the ER connection, not base.const initIx = await counterProgram.methods .initPermission() .accountsPartial({ authority: tempKeypair.publicKey, counter: counterPda, permission: permissionPda, permissionProgram: PERMISSION_PROGRAM_ID, ephemeralVault: EPHEMERAL_VAULT_ID, magicProgram: MAGIC_PROGRAM_ID, }) .instruction();const tx = new Transaction().add(initIx);const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);console.log("init_permission tx:", sig);
Use Cases:
Bootstrap access control for a newly delegated PDA on the ER
Start public (is_private: false, empty members) and tighten later via Update
Flip the privacy flag and rewrite the member list via
UpdateEphemeralPermissionCpi. Rebuild the full member list every call
(including the authority) so the data PDA can never lock itself out.
Anchor
Rust SDK
Pinocchio
Kit
Web3.js
use ephemeral_rollups_sdk::access_control::{ instructions::UpdateEphemeralPermissionCpi, structs::{ EphemeralMembersArgs, Member, TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG, },};let signers = [ COUNTER_SEED, ctx.accounts.counter.authority.as_ref(), &[ctx.bumps.counter],];// When private, only listed members can read ER state via the TEE.// Empty member list + is_private=false = fully public.let members = if is_private { vec![Member { flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG, pubkey: ctx.accounts.counter.authority, }]} else { vec![]};UpdateEphemeralPermissionCpi { payer: ctx.accounts.counter.to_account_info(), permissioned_account: ctx.accounts.counter.to_account_info(), permission: ctx.accounts.permission.to_account_info(), vault: ctx.accounts.ephemeral_vault.to_account_info(), magic_program: ctx.accounts.magic_program.to_account_info(), permission_program: ctx.accounts.permission_program.to_account_info(), authority: ctx.accounts.counter.to_account_info(), authority_is_signer: false, // PDA signs via the seeds above args: EphemeralMembersArgs { is_private, members },}.invoke_signed(&[&signers])?;
use ephemeral_rollups_sdk::access_control::{ instructions::UpdateEphemeralPermissionCpi, structs::{ EphemeralMembersArgs, Member, TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG, },};let seeds: &[&[u8]] = &[ COUNTER_SEED, permissioned_account_authority.as_ref(), &[bump],];// When private, only listed members can read ER state via the TEE.let members = if is_private { vec![Member { flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG, pubkey: permissioned_account_authority, }]} else { vec![]};UpdateEphemeralPermissionCpi { payer: &counter_account_info, permissioned_account: &counter_account_info, permission: &permission_account_info, vault: &ephemeral_vault_account_info, magic_program: &magic_program_account_info, permission_program: &permission_program_account_info, authority: &counter_account_info, authority_is_signer: false, // PDA signs via the seeds above args: EphemeralMembersArgs { is_private, members },}.invoke_signed(&[seeds])?;
use ephemeral_rollups_pinocchio::acl::{ EphemeralMembersArgs, Member, MemberFlags, UpdateEphemeralPermission,};use pinocchio::cpi::{Seed, Signer};const PERMISSION_CPI_BUF: usize = 64;let bump_seed = [bump];let seeds_array: [Seed; 3] = [ Seed::from(b"counter"), Seed::from(authority.address().as_ref()), Seed::from(&bump_seed),];let signer = Signer::from(&seeds_array);// Read the on-chain Counter to grab `authority` — the sole "private" member.let counter_authority = { let data = counter_account.try_borrow()?; Counter::load(&data)?.authority};let single_member = [Member { flags: MemberFlags::from_acl_flag_byte( MemberFlags::TX_LOGS | MemberFlags::TX_MESSAGE | MemberFlags::TX_BALANCES, ), pubkey: counter_authority,}];let members: &[Member] = if is_private { &single_member } else { &[] };UpdateEphemeralPermission { payer: counter_account, permissioned_account: counter_account, permission, vault, magic_program, permission_program, authority: counter_account, authority_is_signer: false, // PDA signs via the seeds above args: EphemeralMembersArgs { is_private, members },}.invoke_signed::<PERMISSION_CPI_BUF>(&[signer])?;
import { MAGIC_PROGRAM_ID, PERMISSION_PROGRAM_ID, EPHEMERAL_VAULT_ID,} from "@magicblock-labs/ephemeral-rollups-sdk";import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";// Toggle the `is_private` flag. Idempotent — the program rebuilds the member// list every call so the authority never locks itself out.const updateIx = await counterProgram.methods .setPrivacy(isPrivate) .accountsPartial({ authority: tempKeypair.address, counter: counterPda, permission: permissionPda, permissionProgram: PERMISSION_PROGRAM_ID, ephemeralVault: EPHEMERAL_VAULT_ID, magicProgram: MAGIC_PROGRAM_ID, }) .instruction();const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => appendTransactionMessageInstructions([updateIx], tx),);const sig = await ephemeralConnection.sendAndConfirmTransaction( transactionMessage, [tempKeypair], { commitment: "confirmed" },);console.log("set_privacy tx:", sig);
import { MAGIC_PROGRAM_ID, PERMISSION_PROGRAM_ID, EPHEMERAL_VAULT_ID,} from "@magicblock-labs/ephemeral-rollups-sdk";import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";// Toggle the `is_private` flag. Idempotent — the program rebuilds the member// list every call so the authority never locks itself out.const updateIx = await counterProgram.methods .setPrivacy(isPrivate) .accountsPartial({ authority: tempKeypair.publicKey, counter: counterPda, permission: permissionPda, permissionProgram: PERMISSION_PROGRAM_ID, ephemeralVault: EPHEMERAL_VAULT_ID, magicProgram: MAGIC_PROGRAM_ID, }) .instruction();const tx = new Transaction().add(updateIx);const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);console.log("set_privacy tx:", sig);
Use Cases:
Toggle is_private on demand (e.g. private play, public reveal)
Add new viewers with TX_LOGS | TX_MESSAGE | TX_BALANCES flags
Revoke a member by omitting them from the next call’s member list
Close the EphemeralPermission account on the ER via
CloseEphemeralPermissionCpi. Rent is refunded to the data PDA (the
original payer). Optional — only call when the permission is no longer
needed.
Anchor
Rust SDK
Pinocchio
Kit
Web3.js
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;let signers = [ COUNTER_SEED, ctx.accounts.counter.authority.as_ref(), &[ctx.bumps.counter],];// Refunds the permission's rent to `payer` (the counter PDA).CloseEphemeralPermissionCpi { payer: ctx.accounts.counter.to_account_info(), permissioned_account: ctx.accounts.counter.to_account_info(), permission: ctx.accounts.permission.to_account_info(), vault: ctx.accounts.ephemeral_vault.to_account_info(), magic_program: ctx.accounts.magic_program.to_account_info(), permission_program: ctx.accounts.permission_program.to_account_info(), authority: ctx.accounts.counter.to_account_info(), authority_is_signer: false,}.invoke_signed(&[&signers])?;
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;let seeds: &[&[u8]] = &[ COUNTER_SEED, permissioned_account_authority.as_ref(), &[bump],];// Refunds the permission's rent to `payer` (the counter PDA).CloseEphemeralPermissionCpi { payer: &counter_account_info, permissioned_account: &counter_account_info, permission: &permission_account_info, vault: &ephemeral_vault_account_info, magic_program: &magic_program_account_info, permission_program: &permission_program_account_info, authority: &counter_account_info, authority_is_signer: false,}.invoke_signed(&[seeds])?;
use ephemeral_rollups_pinocchio::acl::CloseEphemeralPermission;use pinocchio::cpi::{Seed, Signer};let bump_seed = [bump];let seeds_array: [Seed; 3] = [ Seed::from(b"counter"), Seed::from(authority.address().as_ref()), Seed::from(&bump_seed),];let signer = Signer::from(&seeds_array);// Refunds the permission's rent to `payer` (the counter PDA).CloseEphemeralPermission { payer: counter_account, permissioned_account: counter_account, permission, vault, magic_program, permission_program, authority: counter_account, authority_is_signer: false,}.invoke_signed(&[signer])?;
Authority Management: Always assign AUTHORITY_FLAG to at least one trusted member
Least Privilege: Grant only necessary flags to each member
Real-time Updates: Permissions can be updated in real-time on Private Ephemeral Rollup without undelegating, allowing dynamic access control adjustments
Cleanup: Undelegate and close unused permission accounts to free SOL
Signer Validation: Only members with AUTHORITY_FLAG or program with permissioned account can authorize changes
Public Accounts: Setting members to None makes the account publicly visible
Default Authority: By default, the owner of the permissioned account is added as permission authority to members of permission account.
Empty Member List: If members field is set to empty list, the permissioned account is fully restricted and private. Only the owner of permissioned account can modify the permission.
Access Auditing: Use member flags to audit and control access