#[ephemeral] 로 주입된 callback instruction을 통해 undelegation 트리거 (validator CPI를 통해 Base Layer에서 호출)
The undelegation callback discriminator [196, 28, 41, 206, 48, 37, 51, 167]
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.With [#ephemeral] Anchor macro from MagicBlock’s Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
#[ephemeral]#[program]pub mod public_counter { use super::*; /// Initialize the counter. pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; Ok(()) } /// Increment the counter. pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } /// Delegate the account to the delegation program /// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> { // ... } /// Manually commit the counter state in the Ephemeral Rollup session. pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... } /// Increment the counter and commit in the same instruction. pub fn increment_and_commit(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... } /// Undelegate the account from the delegation program. pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> { // ... }}pub const COUNTER_SEED: &[u8] = b"counter";/// Context for initializing counter#[derive(Accounts)]pub struct Initialize<'info> { #[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>,}/// Context for incrementing counter#[derive(Accounts)]pub struct Increment<'info> { #[account(mut, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>,}/// Counter struct#[account]pub struct Counter { pub count: u64,}/// Other context for delegation
특별한 것은 없고, 단순한 Anchor 카운터 프로그램입니다. 차이점은 undelegation을 위한 ephemeral macro와 delegation program과 상호작용하는 유용한 로직을 주입하는 delegate macro를 추가했다는 점입니다。⬆️ Back to Top
이 공용 검증자들은 개발용으로 지원됩니다. 위임 명령에 해당 ER
검증자를 반드시 추가하세요.
메인넷
아시아 (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
미국 (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
데브넷
아시아 (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
미국 (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
로컬넷
로컬 ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Anchor 기능이 포함된 ephemeral-rollups-sdk 를 프로그램에 추가합니다
cargo add ephemeral-rollups-sdk --features anchor
delegate, commit, ephemeral, DelegateConfig, commit_accounts, commit_and_undelegate_accounts 를 import 합니다:
use ephemeral_rollups_sdk::anchor::{ commit, delegate, ephemeral};use ephemeral_rollups_sdk::cpi::DelegateConfig;use ephemeral_rollups_sdk::ephem::{ commit_accounts, commit_and_undelegate_accounts };
프로그램에 delegate macro와 instruction, ephemeral macro, undelegate instruction을 추가하세요. auto commit이나 특정 ER validator 같은 delegation config도 지정할 수 있습니다:
/// Add delegate function to the context#[delegate]#[derive(Accounts)]pub struct DelegateInput<'info> { pub payer: Signer<'info>, /// CHECK: The pda to delegate #[account(mut, del)] pub pda: AccountInfo<'info>,}
/// Delegate the account to the delegation program/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setuppub fn delegate(ctx: Context<DelegateInput>) -> Result<()> { ctx.accounts.delegate_pda( &ctx.accounts.payer, &[COUNTER_SEED], DelegateConfig { // Optionally set a specific validator from the first remaining account validator: ctx.remaining_accounts.first().map(|acc| acc.key()), ..Default::default() }, )?; Ok(())}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;/// Manually commit the counter state in the Ephemeral Rollup session.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(())}/// Increment the counter and commit the new state in the same instruction.pub fn increment_and_commit(ctx: Context<IncrementAndCommit>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; // Serialize the Anchor account before the CPI sees it counter.exit(&crate::ID)?; 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(())}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;/// Undelegate the account from the delegation program./// Commits the latest state and returns ownership of the PDA back to the owner program.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(())}
Delegation 은 프로그램의 하나 이상의 PDA 소유권을 delegation program으로 이전하는 과정입니다. 그러면 Ephemeral Validators가 이 PDA 를 사용해 SVM runtime에서 트랜잭션을 실행할 수 있습니다.
Commit 은 PDA 의 상태를 ER에서 base layer로 업데이트하는 과정입니다. finalization 이후에도 PDA 는 base layer에서 잠겨 있는 상태로 유지됩니다。
Undelegation 은 PDA 의 소유권을 다시 프로그램으로 되돌리는 과정입니다. undelegation 시 상태가 커밋되고 finalization 프로세스가 시작됩니다. 상태 검증이 완료되면 PDA 가 잠금 해제되어 base layer에서 평소처럼 사용할 수 있습니다。
commit_and_update_leaderboard 커밋 명령은 ER에서 실행됩니다. MagicIntentBundleBuilder를 사용해 커밋과 커밋 후 액션을 모두 magic_context에 예약하며 — ER 트랜잭션이 베이스 레이어로 확정될 때 두 작업이 함께 적용됩니다.
// commit action instruction on ERpub fn commit_and_update_leaderboard(ctx: Context<CommitAndUpdateLeaderboard>) -> Result<()> { // Build the post-commit action that updates the leaderboard on base layer let instruction_data = anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {}); let action_args = ActionArgs::new(instruction_data); let action_accounts = vec![ ShortAccountMeta { pubkey: ctx.accounts.leaderboard.key(), is_writable: true, }, ShortAccountMeta { pubkey: ctx.accounts.counter.key(), is_writable: false, }, ]; let action = CallHandler { destination_program: crate::ID, accounts: action_accounts, args: action_args, // Signer that pays transaction fees for the action from its escrow PDA escrow_authority: ctx.accounts.payer.to_account_info(), compute_units: 200_000, }; // Schedule commit + post-commit action on magic_context 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()]) .add_post_commit_actions([action]) .build_and_invoke()?; Ok(())}// commit action context on ER#[commit]#[derive(Accounts)]pub struct CommitAndUpdateLeaderboard<'info> { #[account(mut)] pub payer: Signer<'info>, #[account(mut, seeds = [COUNTER_SEED], bump)] pub counter: Account<'info, Counter>, /// CHECK: Leaderboard PDA - not mut here, writable set in handler #[account(seeds = [LEADERBOARD_SEED], bump)] pub leaderboard: UncheckedAccount<'info>, /// CHECK: Your program ID pub program_id: AccountInfo<'info>,}
여러 계정을 커밋하고 한 번의 호출로 여러 액션을 연결할 수 있습니다. 액션은 add_post_commit_actions에 전달된 순서대로 순차 실행됩니다.
// Chain several actions — they execute sequentially on base layer after the commit lands.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(), // ... additional committed accounts]).add_post_commit_actions([action_1, action_2, action_3]).build_and_invoke()?;
액션은 위임 해제에도 연결할 수 있습니다 — counter 커밋, 위임 해제, 액션 실행이 모두 동일한 ER 트랜잭션 안에서 원자적으로 처리됩니다.
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.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()]).add_post_commit_actions([action]).build_and_invoke()?;
위임된 계정에 lamports를 충전합니다. 트랜잭션은 base layer에서 제출되며, Ephemeral SPL Token 프로그램이 일회성 lamports PDA를 통해 위임된 잔고로 lamports를 옮깁니다.주요 용도: 위임된 fee payer에 자금을 충당해 기본으로 제공되는 10회의 commit 스폰서십 할당량을 넘어서도 위임된 fee payer 자체가 commit 비용을 지불할 수 있게 합니다.참고 사항:
충전할 때마다 crypto.getRandomValues로 새로운 32바이트 salt를 생성합니다 — salt를 재사용하면 기존 PDA와 충돌합니다.
ER가 아닌 base layer의 RPC에 제출합니다.
대상 계정은 이미 위임되어 있어야 합니다.
import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction,} from "@solana/web3.js";import { lamportsDelegatedTransferIx, deriveLamportsPda,} from "@magicblock-labs/ephemeral-rollups-sdk";/** * Top up a delegated account with lamports. * * The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token * program creates a single-use lamports PDA, funds it from the payer, and * delegates it so the ER credits the destination's delegated balance. */async function topUpDelegatedAccount( connection: Connection, // base-layer connection payer: Keypair, destination: PublicKey, // delegated account to top up amountLamports: bigint,) { // Generate a fresh 32-byte salt per top-up. // Re-using a salt collides with an existing lamports PDA and the call fails. const salt = crypto.getRandomValues(new Uint8Array(32)); const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt); const ix = await lamportsDelegatedTransferIx( payer.publicKey, destination, amountLamports, salt, ); const tx = new Transaction().add(ix); tx.feePayer = payer.publicKey; // CRITICAL: send to the base-layer RPC, not the ER. const sig = await sendAndConfirmTransaction(connection, tx, [payer], { commitment: "confirmed", skipPreflight: true, }); return { sig, lamportsPda };}