跳转到主要内容
Ephemeral accounts 是只存在于 Ephemeral Rollup 内部的账户。一个 sponsor 账户(已委托到 ER)会以 32 lamports/byte 的成本为 ephemeral accounts 代付租金,这比 Solana 基础租金便宜约 109 倍。 关键特性:
  • 完全在 ER 中创建、存在并销毁
  • 归调用它的程序所有(从 CPI 上下文推断)
  • 由 sponsor 账户的 lamports 提供资金
  • 可以被创建、调整大小和关闭

#[ephemeral_accounts] Macro

这个 proc-macro attribute 用于 Anchor 的 Accounts struct。它会识别 #[account(...)] 中的两个自定义标记:
标记作用
sponsor标记为 ephemeral accounts 支付租金的账户
eph将账户标记为 ephemeral(仅存在于 ER)

校验规则

  • 如果存在 eph 字段,则至少需要一个 sponsor
  • 每个 struct 只允许 一个 sponsor
  • eph 不能与 initinit_if_needed 一起使用(请改用生成的方法)
  • 如果 sponsor 是 PDA(而不是 Signer),则必须提供用于 PDA 签名的 seeds

生成的方法

对于名为 conversation 的字段,macro 会生成:
方法签名说明
create_ephemeral_conversation(data_len: u32) -> Result<()>创建 ephemeral account
init_if_needed_ephemeral_conversation(data_len: u32) -> Result<()>仅在 data_len == 0 时创建
resize_ephemeral_conversation(new_data_len: u32) -> Result<()>扩大或缩小账户
close_ephemeral_conversation() -> Result<()>关闭账户,并将租金退还给 sponsor

签名要求

  • Sponsor:在所有操作(create、resize、close)中都必须是 signer
  • Ephemeral仅在 create 时 必须是 signer(防止 pubkey 抢注)。resize 或 close 时不要求。
  • 对于 PDA 账户,macro 会通过 find_program_address 自动推导 signer seeds

租金模型

pub const EPHEMERAL_RENT_PER_BYTE: u64 = 32;
const ACCOUNT_OVERHEAD: u32 = 60;

// rent = (data_len + 60) * 32
pub const fn rent(data_len: u32) -> u64 {
    (data_len as u64 + ACCOUNT_OVERHEAD as u64) * EPHEMERAL_RENT_PER_BYTE
}
  • 扩容:sponsor 向 vault 支付额外租金
  • 缩容:vault 将多余租金退还给 sponsor
  • 关闭:所有租金都从 vault 退还给 sponsor

创建一个 Ephemeral Account

use ephemeral_rollups_sdk::anchor::ephemeral_accounts;

pub fn create_conversation(ctx: Context<CreateConversation>) -> Result<()> {
    ctx.accounts
        .create_ephemeral_conversation((8 + Conversation::space_for_message_count(0)) as u32)?;

    let conversation = Conversation {
        handle_owner: ctx.accounts.profile_owner.handle.clone(),
        handle_other: ctx.accounts.profile_other.handle.clone(),
        bump: ctx.bumps.conversation,
        messages: Vec::new(),
    };
    let mut data = ctx.accounts.conversation.try_borrow_mut_data()?;
    conversation.try_serialize(&mut &mut data[..])?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        mut,
        sponsor,
        seeds = [b"profile", profile_owner.handle.as_bytes()],
        bump = profile_owner.bump,
        has_one = authority
    )]
    pub profile_owner: Account<'info, Profile>,

    #[account(
        seeds = [b"profile", profile_other.handle.as_bytes()],
        bump = profile_other.bump,
    )]
    pub profile_other: Account<'info, Profile>,

    /// CHECK: Ephemeral conversation PDA sponsored by the profile.
    #[account(
        mut,
        eph,
        seeds = [b"conversation", profile_owner.handle.as_bytes(), profile_other.handle.as_bytes()],
        bump
    )]
    pub conversation: AccountInfo<'info>,
    // vault and magic_program are auto-injected by the macro
}
在调用 create_ephemeral_* 之后,你必须手动将数据 struct 序列化到原始账户数据中。该 macro 只会分配空间,并不会初始化数据。

调整 Ephemeral Account 大小

pub fn extend_conversation(
    ctx: Context<ExtendConversation>,
    additional_messages: u32,
) -> Result<()> {
    let current_capacity =
        Conversation::message_capacity(ctx.accounts.conversation.to_account_info().data_len());
    let new_capacity = current_capacity + additional_messages as usize;

    ctx.accounts.resize_ephemeral_conversation(
        (8 + Conversation::space_for_message_count(new_capacity)) as u32,
    )?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct ExtendConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
    pub profile_sender: Account<'info, Profile>,
    #[account(seeds = [...], bump = ...)]
    pub profile_other: Account<'info, Profile>,
    /// CHECK: Ephemeral conversation PDA
    #[account(mut, eph, seeds = [...], bump)]
    pub conversation: AccountInfo<'info>,
}

关闭一个 Ephemeral Account

pub fn close_conversation(ctx: Context<CloseConversation>) -> Result<()> {
    let profile = &mut ctx.accounts.profile_owner;
    profile.active_conversation_count = profile
        .active_conversation_count
        .checked_sub(1)
        .ok_or(ChatError::ConversationCountUnderflow)?;

    ctx.accounts.close_ephemeral_conversation()?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CloseConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
    pub profile_owner: Account<'info, Profile>,
    #[account(seeds = [...], bump = ...)]
    pub profile_other: Account<'info, Profile>,
    /// CHECK: Ephemeral conversation PDA
    #[account(mut, eph, seeds = [...], bump)]
    pub conversation: AccountInfo<'info>,
}

使用钱包作为 Sponsor

可以直接使用 Signer 作为 sponsor,而不是 PDA:
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateGame<'info> {
    #[account(mut, sponsor)]
    pub payer: Signer<'info>,

    /// CHECK: Ephemeral PDA
    #[account(mut, eph, seeds = [b"game", payer.key().as_ref()], bump)]
    pub game_state: AccountInfo<'info>,
}

TypeScript 客户端用法

所有 ephemeral account 操作都发送到 ER connection,而不是 base layer:
// Create
await erProgram.methods
    .createConversation()
    .accounts({
        authority: userA.publicKey,
        profileOwner: profileAPda,
        profileOther: profileBPda,
        conversation: conversationPda,
        systemProgram: SystemProgram.programId,
    })
    .rpc();

// Resize
await erProgram.methods
    .extendConversation(5)
    .accounts({
        authority: userA.publicKey,
        profileSender: profileAPda,
        profileOther: profileBPda,
    })
    .rpc();

// Close
await erProgram.methods
    .closeConversation()
    .accounts({
        authority: userA.publicKey,
        profileOwner: profileAPda,
        profileOther: profileBPda,
        conversation: conversationPda,
    })
    .rpc();

常见坑点

eph fields must use AccountInfo<'info>, not Account<'info, T>. The account doesn’t exist yet at validation time, so Anchor cannot deserialize it.
After calling create_ephemeral_*, you must serialize your data struct into the raw account data yourself. The macro allocates space but does not write any data.
The macro enforces this at compile time. Use the generated create_ephemeral_* method instead of Anchor’s init constraint.
Transfer extra SOL to the sponsor account before delegating it, so it has enough lamports to fund ephemeral accounts on the ER.
You don’t need to declare them in your struct, but they appear in the IDL and must be passed from the client. Anchor resolves them automatically if named correctly.

Learn More

Ephemeral Accounts Demo

Full example program on GitHub

Delegation & Undelegation

How delegation and state synchronization work

Quickstart

Build your first program with Ephemeral Rollups