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 不能与 init 或 init_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 >,
}
可以直接使用 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, not Account
eph fields must use AccountInfo<'info>, not Account<'info, T>. The account doesn’t exist yet at validation time, so Anchor cannot deserialize it.
Manual serialization is required after create
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.
Cannot combine eph with init
The macro enforces this at compile time. Use the generated create_ephemeral_* method instead of Anchor’s init constraint.
Sponsor must be delegated first
Top up the sponsor before delegation
vault and magic_program are auto-injected
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