本文介绍了Fogo Sessions在Ignition中的集成,展示了如何利用会话密钥机制,允许用户在保持安全边界的同时,执行协议交互而无需每次交易签名。文章详细阐述了会话的设置、启动、运行时检查以及生命周期管理,并深入探讨了Ignition如何通过会话特定的指令、用户提取、程序签名者PDA验证以及会话Token传输来实现安全保障。
\ |

一个会话首先从一个 意图消息 开始,这是一个用户使用其主钱包在链下签名的 payload。它充当会话的“配置文件”,明确定义了权限的边界。Message 结构在链上进行解析,并用于强制执行以下约束:
// programs/session-manager/src/message.rs
pub struct Message {
pub version: Version,
pub chain_id: String,
pub domain: Domain,
pub expires: DateTime<FixedOffset>,
pub session_key: Pubkey,
pub tokens: Tokens,
pub extra: HashMap<String, String>,
}
pub struct Domain(String);
pub enum Tokens {
Specific(Vec<(SymbolOrMint, UiTokenAmount)>),
All,
}

为了确保会话有效,使用了指令内省。启动会话所需的交易需要两条指令:
第一条指令使用用户的公钥、已签名的意图消息(序列化后)和签名作为输入值,调用 Solana 原生的 Ed25519 程序。由于交易是原子性的,如果此验证失败,则整个交易将回滚。这保证了除非用户的签名经过密码学验证有效,否则后续的 start_session 指令永远无法到达。
第二条指令在 会话管理器程序 上调用 start_session。此指令 不 将签名作为参数。相反,它使用内省向后查看 前一条指令 在当前交易中。
这种机制确保了除非用户以密码学方式签署了程序加载的确切配置参数(即,已签名的消息与提供的会话配置匹配),否则无法创建会话帐户。
start_session 指令执行以下操作:
\
let Intent {
signer,
message: Message {
version,
chain_id,
domain,
expires,
session_key,
tokens,
extra
}
} = Intent::load(&ctx.accounts.sysvar_instructions)
impl<...> Intent<M> {
pub fn load(sysvar_instructions: &AccountInfo<'_>) -> Result<...> {
get_instruction_relative(-1, sysvar_instructions)?.try_into()
}
}
该指令内省 sysvar_instructions 帐户,以查找索引为 -1 的 Ed25519 验证指令。这将检索用户的签名和已签名的消息,并将它们以密码学方式绑定到此交易。
ctx.accounts.check_session_key(message.session_key)?; // 匹配目标密钥
ctx.accounts.check_chain_id(message.chain_id)?; // 验证链 ID
// ... additional validation checks
该程序验证:
let authorized_tokens_with_mints = match tokens {
Tokens::Specific(tokens) => {
// 将剩余帐户转换为待处理的授权
let pending_approvals = convert_remaning_accounts_..._pending_approvals(
ctx.remaining_accounts,
tokens,
&signer,
)?;
// 提取授权的 mint
let authorized_tokens_with_mints = AuthorizedTokensWithMints::Specific(
pending_approvals.iter().map(|p| p.mint()).collect(),
);
// 使用 SESSION_SETTER_SEED PDA 执行 token 授权
ctx.accounts.approve_tokens(pending_approvals, ctx.bumps.session_setter)?;
authorized_tokens_with_mints
}
Tokens::All => AuthorizedTokensWithMints::All,
};
对于具有 Tokens::Specific 的会话,程序:
对于具有 Tokens::All 的会话,不会设置预先授权,因为 Token 程序将在运行时绕过委派检查。
let session = Session {
sponsor: ctx.accounts.sponsor.key(),
major,
session_info: SessionInfo::V4(V4::Active(ActiveSessionInfoWithDomainHash {
domain_hash: domain.get_domain_hash(),
active_session_info: ActiveSessionInfo {
user: signer,
authorized_programs: AuthorizedPrograms::Specific(program_domains),
authorized_tokens: authorized_tokens_with_mints,
extra: extra.into(),
expiration,
},
})),
};
ctx.accounts.initialize_and_store_session(&session)?;
该程序创建一个 会话帐户,其中包含:
然后,将会话帐户写入区块链,并将所有权分配给会话管理器程序。
初始化后,会话帐户可以充当集成 Fogo Sessions 的程序的签名者。与标准密钥对不同,其权限是有条件的。Fogo Chain 生态系统在每次交互期间在协议级别强制执行这些条件(过期时间戳、授权程序等)。
标准的 SPL Token 程序 已修改 以原生识别会话帐户。每当交易涉及由会话密钥签名的 token 转移时,程序都会触发 会话Hook 以在继续转移之前 验证 操作:
// packages/sessions-sdk-rs/src/session/token_program.rs
pub fn get_token_permissions_checked(
&self,
user: &Pubkey,
signers: &[AccountInfo],
) -> Result<AuthorizedTokens, SessionError> {
self.check_is_live_and_unrevoked()?;
self.check_user(user)?;
self.check_authorized_program_signer(signers)?;
Ok(self.authorized_tokens()?.clone())
}
最后,检查 支出限额:
对于与不需要 token 转移的外部应用程序的交互,通过 Fogo Sessions SDK 强制执行安全性,该 SDK 用于从会话帐户中提取用户:
// packages/sessions-sdk-rs/src/session/mod.rs
pub fn extract_user_from_signer_or_session(
info: &AccountInfo,
program_id: &Pubkey,
) -> Result<Pubkey, SessionError> {
if !info.is_signer {
return Err(SessionError::MissingRequiredSignature);
}
// 确保会话帐户归会话管理器程序所有
if info.owner == &SESSION_MANAGER_ID {
let session = Session::try_deserialize(...)?;
session.get_user_checked(program_id)
} else {
Ok(*info.key)
}
}
pub fn get_user_checked(&self, program_id: &Pubkey) -> Result<...> {
self.check_is_live_and_unrevoked()?;
self.check_authorized_program(program_id)?;
Ok(*self.user()?)
}
该函数:
当区块时间戳超过 expires 字段时,会话自然会过期。出于安全目的,不需要链上交易来“关闭”会话;它只是停止运行。Token 程序的 JIT 验证将拒绝任何尝试使用过期会话的交易。
用户可以通过调用 revoke_session 指令在会话过期之前强制使其无效。撤销后,会话状态从 Active 转换为 Revoked。Token 程序Hook在每次转移之前检查此状态,确保立即终止访问。
会话过期或被撤销后,可以完全关闭会话帐户以回收租金。close_session 指令执行完整的清理:
注意:
Ignition 是 FOGO 原生 token 的流动性质押协议,由 Tempest Labs 开发,并从原始 SPL stake-pool 程序 分叉而来。该协议被修改为集成 Fogo Sessions 和 wFOGO 包装/解包,这是一种会话密钥机制,允许用户在执行协议交互时无需每次交易都进行签名,同时保持定义的安全边界。
本节将展示 Ignition 如何在其原生 Solana 程序中实现会话支持,演示如何实际利用前面描述的安全保证。
你还可以在Fogo Foundation 的 fogo-session 存储库中找到一个 Anchor 示例。
Ignition 为基于会话的操作实现了专用指令变体。与标准指令相比,这些会话变体需要一个额外的帐户:program_signer PDA。
正如前一节所述,修改后的 Token 程序通过验证授权程序的 PDA 是否已对交易进行签名来强制执行程序绑定。这可以防止恶意应用程序劫持用户的会话来耗尽其 token。因此,Ignition 必须在执行 token 转移的任何指令中包含此 PDA:
pub fn deposit_wsol_with_session(
program_id: &Pubkey,
stake_pool: &Pubkey,
withdraw_authority: &Pubkey,
reserve_stake: &Pubkey,
session_signer: &Pubkey,
pool_token_account: &Pubkey,
manager_fee_account: &Pubkey,
referrer_pool_account: &Pubkey,
pool_mint: &Pubkey,
token_program_id: &Pubkey,
wsol_token_account: &Pubkey,
transient_wsol_account: &Pubkey,
program_signer: &Pubkey,
payer: &Pubkey,
sol_deposit_authority: Option<&Pubkey>,
amount: u64,
) -> Instruction {
let accounts = vec![\
// 账户提取自参数
// ...\
AccountMeta::new_readonly(*session_signer, true), // 会话必须签名
AccountMeta::new(*program_signer, false), // 程序签名者 PDA
AccountMeta::new_readonly(*payer, true),\
];
// ...
}
session_signer 是在会话初始化期间创建的会话帐户。program_signer 是 Ignition 的 PDA,它将共同签署 token 转移,向 Token 程序证明该操作源自授权的应用程序。
集成会话时,一个关键的安全考虑因素是正确识别交易背后的实际用户。会话帐户代表用户签名,但 Ignition 需要知道真实用户的身份,以验证 token 帐户所有权和其他用户特定的约束。
SDK 提供了 extract_user_from_signer_or_session 来实现此目的。此函数执行必要的验证:检查会话是否有效、未撤销且已获得调用程序的授权:
fn process_deposit_wsol_with_session(
program_id: &Pubkey,
accounts: &[AccountInfo],
deposit_lamports: u64,
minimum_pool_tokens_out: Option<u64>,
) -> ProgramResult {
use fogo_sessions_sdk::{session::Session, token::PROGRAM_SIGNER_SEED};
let account_info_iter = &mut accounts.iter();
// ... 账户解析 ...
let signer_or_session_info = next_account_info(account_info_iter)?;
// ...
// 从会话中提取真实用户
// 这还会验证到期、撤销状态和程序授权
let user_pubkey = Session::extract_user_from_signer_or_session(
signer_or_session_info,
program_id
)?;
// 使用提取的用户来验证 token 帐户所有权
let expected_wsol_ata = get_associated_token_address_with_program_id(
&user_pubkey,
wsol_mint_info.key,
token_program_info.key,
);
if *wsol_token_info.key != expected_wsol_ata {
msg!("`wsol_token` 不是用户预期的 ATA");
return Err(ProgramError::InvalidAccountData);
}
// ...
}
通过从会话的用户派生预期的 ATA,Ignition 确保存入的 wSOL 实际上属于授权会话的用户。
在执行任何会话 token 转移之前,Ignition 必须验证提供的 program_signer 帐户是否是正确的 PDA。Token 程序的会话Hook调用 check_authorized_program_signer,它验证 PDA 是否派生自预期的 seed (PROGRAM_SIGNER_SEED) 和授权程序的 ID。
Ignition 执行相同的派生以确保一致性:
let (expected_program_signer, program_signer_bump) =
Pubkey::find_program_address(&[PROGRAM_SIGNER_SEED], program_id);
if *program_signer_info.key != expected_program_signer {
msg!("`program_signer` 与预期地址不匹配");
return Err(ProgramError::InvalidSeeds);
}
let program_signer_seeds: &[&[u8]] = &[PROGRAM_SIGNER_SEED, &[program_signer_bump]];
program_signer_seeds 存储起来供以后在对 Token 程序进行 CPI 时使用。当 Ignition 调用 token 转移时,它会使用此 PDA 进行签名,并且 Token 程序的会话Hook将验证:
提取用户并验证程序签名者后,Ignition 可以执行 token 转移。SDK 提供了标准 SPL token 指令(transfer_checked、burn 等)的会话感知版本,这些指令接受可选的 program_signer 参数。
如果提供了此参数,则该指令会将程序签名者作为额外的签名者包含在内,修改后的 Token 程序需要该签名者才能进行基于会话的转移:
use fogo_sessions_sdk::token::instruction::transfer_checked;
invoke_signed(
&transfer_checked(
token_program_info.key,
wsol_token_info.key, // 源
wsol_mint_info.key,
wsol_transient_info.key, // 目标
signer_or_session_info.key, // 权限:会话帐户
Some(program_signer_info.key), // 程序签名者:证明授权程序
deposit_lamports,
native_mint::DECIMALS,
)?,
&[\
token_program_info.clone(),\
wsol_token_info.clone(),\
wsol_mint_info.clone(),\
wsol_transient_info.clone(),\
signer_or_session_info.clone(),\
program_signer_info.clone(),\
],
&[program_signer_seeds], // Ignition 使用其 PDA 签名
)?;
当 Token 程序处理此转移时,它会检测到权限 (signer_or_session_info) 是由会话管理器拥有的会话帐户。这会触发会话Hook,该Hook:
相同的模式适用于提款期间的 token 销毁:
use fogo_sessions_sdk::token::instruction::burn;
invoke_signed(
&burn(
token_program_info.key,
burn_from_pool_info.key,
pool_mint_info.key,
user_transfer_authority_info.key,
program_signer_info.key, // 用于会话绑定的程序签名者
pool_tokens_burnt,
)?,
&[\
burn_from_pool_info.clone(),\
pool_mint_info.clone(),\
user_transfer_authority_info.clone(),\
program_signer_info.clone(),\
],
&[program_signer_seeds],
)?;
Ignition 还支持直接用户交互(没有会话),以实现向后兼容性。对于某些指令,程序通过检查程序签名者帐户是否存在来检测要采用的路径:
// 通过程序签名者帐户的存在来检测会话路径
if let Ok(program_signer_info) = next_account_info(account_info_iter) {
use fogo_sessions_sdk::token::instruction::{burn, transfer_checked};
use fogo_sessions_sdk::token::PROGRAM_SIGNER_SEED;
// 验证程序签名者 PDA
let (expected_program_signer, program_signer_bump) =
Pubkey::find_program_address(&[PROGRAM_SIGNER_SEED], program_id);
if expected_program_signer != *program_signer_info.key {
msg!("程序签名者帐户无效");
return Err(ProgramError::InvalidProgram);
}
let program_signer_seeds: &[&[u8]] = &[PROGRAM_SIGNER_SEED, &[program_signer_bump]];
// 使用具有双重签名的会话感知 token 操作...
} else {
// 直接用户路径:没有程序签名者,使用标准 SPL token 操作
// 用户直接签名,无需会话验证
}
这种设计允许 Ignition 为启用会话的客户端和传统客户端(他们单独签署每笔交易)提供服务。安全模型在这两种情况下都保持不变:会话用户受到 Token 程序的运行时Hook的保护,而直接用户保持对他们签署的每笔交易的完全控制。
Fogo Sessions 为区块链系统中最大的可用性挑战之一提供了一个协议级别的解决方案:重复的用户签名。通过将权限绑定到已签名的意图、通过链上验证强制执行范围以及直接与 Token 程序集成,会话可以在不扩大信任范围的情况下实现更流畅的用户流。
Ignition 集成展示了这些保证如何在真实协议中保持有效。基于会话的执行维护了程序授权、token 所有权和到期周围的清晰边界,同时仍然允许应用程序提供快速、低摩擦的交互。随着越来越多的应用程序采用会话,这种模式为在 Fogo 上构建响应迅速、用户友好的系统奠定了坚实的基础,而不会影响安全假设。
我们发布实用的、高价值的深度分析,如基于实际审计工作和实践协议分析的这篇文章。如果你正在构建接近底层的组件并且关心正确性,请在 X 和 LinkedIn 上关注 Adevar Labs,以获取有关智能合约安全的未来文章。
- 原文链接: adevarlabs.com/blog/a-de...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!