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.
Account delegation is the process of temporarily transferring ownership of an account to the MagicBlock delegation program. Once delegated, the account can be used in an Ephemeral Rollup where transactions execute with minimal latency.
Overview
Delegation enables accounts to be processed by Ephemeral Rollup validators while maintaining state consistency with the Solana base layer. During delegation:
The account’s owner is temporarily changed to the delegation program
The account can be modified through the ER with low latency
State automatically commits back to Solana at configured intervals
The account can be undelegated to return full control to the original owner
Delegation with Anchor
The Anchor framework provides the simplest way to add delegation support to your programs.
Setup
Add the ephemeral-rollups-sdk to your Cargo.toml:
cargo add ephemeral-rollups-sdk
Import the delegation macros and functions:
use ephemeral_rollups_sdk :: anchor :: {delegate, ephemeral};
use ephemeral_rollups_sdk :: cpi :: DelegateConfig ;
Add the #[ephemeral] attribute to your program module:
#[ephemeral]
#[program]
pub mod anchor_counter {
// Your program instructions
}
Create a delegate instruction
Add a delegate instruction with the #[delegate] attribute:
pub fn delegate ( ctx : Context < DelegateInput >) -> Result <()> {
ctx . accounts . delegate_pda (
& ctx . accounts . payer,
& [ COUNTER_SEED ],
DelegateConfig {
validator : ctx . remaining_accounts . first () . map ( | acc | acc . key ()),
.. Default :: default ()
},
) ? ;
Ok (())
}
#[delegate]
#[derive( Accounts )]
pub struct DelegateInput <' info > {
pub payer : Signer <' info >,
/// The PDA to delegate
#[account( mut , del)]
pub pda : AccountInfo <' info >,
}
The #[delegate] macro automatically adds the required accounts for delegation (buffer, record, metadata, delegation program, etc.).
Complete Anchor example
From the anchor-counter example:
/home/daytona/workspace/source/anchor-counter/programs/anchor-counter/src/lib.rs
use anchor_lang :: prelude ::* ;
use ephemeral_rollups_sdk :: anchor :: {delegate, ephemeral};
use ephemeral_rollups_sdk :: cpi :: DelegateConfig ;
pub const COUNTER_SEED : & [ u8 ] = b"counter" ;
#[ephemeral]
#[program]
pub mod anchor_counter {
use super ::* ;
pub fn delegate ( ctx : Context < DelegateInput >) -> Result <()> {
ctx . accounts . delegate_pda (
& ctx . accounts . payer,
& [ COUNTER_SEED ],
DelegateConfig {
// Optionally set a specific validator
validator : ctx . remaining_accounts . first () . map ( | acc | acc . key ()),
.. Default :: default ()
},
) ? ;
Ok (())
}
}
#[delegate]
#[derive( Accounts )]
pub struct DelegateInput <' info > {
pub payer : Signer <' info >,
#[account( mut , del)]
pub pda : AccountInfo <' info >,
}
Delegation with native Rust
For programs that don’t use Anchor, you can delegate accounts using the native Rust SDK.
Manual delegation
From the rust-counter example:
/home/daytona/workspace/source/rust-counter/src/processor.rs
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 ,
.. Default :: default ()
};
delegate_account ( delegate_accounts , pda_seeds , delegate_config ) ? ;
Ok (())
}
Delegation with Bolt
The Bolt framework provides ECS-style delegation using component attributes.
Component delegation
From the bolt-counter example:
/home/daytona/workspace/source/bolt-counter/programs-ecs/components/counter/src/lib.rs
use bolt_lang ::* ;
#[component(delegate)]
#[derive( Default )]
pub struct Counter {
pub count : u64 ,
}
The #[component(delegate)] attribute automatically makes the component delegatable.
Client-side delegation
import { createDelegateInstruction , FindComponentPda } from "@magicblock-labs/bolt-sdk" ;
const counterPda = FindComponentPda ({
componentId: counterComponent . programId ,
entity: entityPda ,
});
const delegateIx = createDelegateInstruction ({
entity: entityPda ,
account: counterPda ,
ownerProgram: counterComponent . programId ,
payer: provider . wallet . publicKey ,
});
const tx = new anchor . web3 . Transaction (). add ( delegateIx );
const txSign = await provider . sendAndConfirm ( tx );
Delegation configuration
The DelegateConfig struct allows you to customize delegation behavior:
pub struct DelegateConfig {
/// How often to commit state to base layer (in milliseconds)
pub commit_frequency_ms : u32 ,
/// Optional specific ER validator to delegate to
pub validator : Option < Pubkey >,
}
Commit frequency
The commit_frequency_ms parameter controls how often the ER automatically commits state back to Solana:
let delegate_config = DelegateConfig {
commit_frequency_ms : 30_000 , // Commit every 30 seconds
validator : None ,
};
Setting a very low commit frequency increases base layer transaction costs. Balance latency needs with cost considerations.
Validator selection
You can optionally specify which ER validator should handle the delegation:
// Anchor example
pub fn delegate ( ctx : Context < DelegateInput >) -> Result <()> {
ctx . accounts . delegate_pda (
& ctx . accounts . payer,
& [ COUNTER_SEED ],
DelegateConfig {
// Use validator from remaining accounts if provided
validator : ctx . remaining_accounts . first () . map ( | acc | acc . key ()),
.. Default :: default ()
},
) ? ;
Ok (())
}
For local development, you typically need to specify the local validator identity:
const remainingAccounts = connectionER . rpcEndpoint . includes ( "localhost" )
? [
{
pubkey: new PublicKey ( "mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev" ),
isSigner: false ,
isWritable: false ,
},
]
: [];
let tx = await program . methods
. delegate ()
. accounts ({ payer: wallet . publicKey , pda: counterPDA })
. remainingAccounts ( remainingAccounts )
. transaction ();
PDA delegation
Delegation works seamlessly with Program Derived Addresses (PDAs). You must provide the seeds used to derive the PDA:
// Single seed
const COUNTER_SEED : & [ u8 ] = b"counter" ;
ctx . accounts . delegate_pda ( & ctx . accounts . payer, & [ COUNTER_SEED ], config ) ? ;
// Multiple seeds
let seed_1 = b"counter" ;
let seed_2 = initializer . key . as_ref ();
let pda_seeds : & [ & [ u8 ]] = & [ seed_1 , seed_2 ];
delegate_account ( accounts , pda_seeds , config ) ? ;
The SDK uses these seeds to verify PDA ownership and recreate the account after undelegation.
Undelegating accounts
To return an account to the base layer and restore original ownership:
Anchor/TypeScript
Native Rust
Anchor Program
import { createUndelegateInstruction } from "@magicblock-labs/bolt-sdk" ;
const undelegateIx = createUndelegateInstruction ({
payer: provider . wallet . publicKey ,
delegatedAccount: pda ,
ownerProgram: program . programId ,
reimbursement: provider . wallet . publicKey ,
});
let tx = new anchor . web3 . Transaction (). add ( undelegateIx );
await provider . sendAndConfirm ( tx );
use ephemeral_rollups_sdk :: cpi :: undelegate_account;
pub fn process_undelegate (
program_id : & Pubkey ,
accounts : & [ AccountInfo ],
pda_seeds : Vec < Vec < u8 >>,
) -> ProgramResult {
let account_info_iter = & mut accounts . iter ();
let delegated_pda = next_account_info ( account_info_iter ) ? ;
let delegation_buffer = next_account_info ( account_info_iter ) ? ;
let initializer = next_account_info ( account_info_iter ) ? ;
let system_program = next_account_info ( account_info_iter ) ? ;
undelegate_account (
delegated_pda ,
program_id ,
delegation_buffer ,
initializer ,
system_program ,
pda_seeds ,
) ? ;
Ok (())
}
use ephemeral_rollups_sdk :: ephem :: commit_and_undelegate_accounts;
pub fn undelegate ( ctx : Context < IncrementAndCommit >) -> Result <()> {
commit_and_undelegate_accounts (
& ctx . accounts . payer,
vec! [ & ctx . accounts . counter . to_account_info ()],
& ctx . accounts . magic_context,
& ctx . accounts . magic_program,
) ? ;
Ok (())
}
Undelegation automatically commits the latest state to the base layer before returning ownership.
Examples using delegation
Next steps
Transaction execution Learn how to execute transactions in Ephemeral Rollups
Ephemeral Rollups Understand the full ER architecture