Fogo会话深入探索

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

在 Ignition 中集成

\ |

会话设置 (意图消息)

一个会话首先从一个 意图消息 开始,这是一个用户使用其主钱包在链下签名的 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,
}

启动会话

为了确保会话有效,使用了指令内省。启动会话所需的交易需要两条指令:

  1. 首先,一个指令发送到 Ed25519 程序,以验证意图签名的有效性。
  2. 其次,向会话管理器程序发送一个 start_session 指令。

指令 1:签名验证 (Ed25519)

第一条指令使用用户的公钥、已签名的意图消息(序列化后)和签名作为输入值,调用 Solana 原生的 Ed25519 程序。由于交易是原子性的,如果此验证失败,则整个交易将回滚。这保证了除非用户的签名经过密码学验证有效,否则后续的 start_session 指令永远无法到达。

指令 2:会话初始化

第二条指令在 会话管理器程序 上调用 start_session。此指令 将签名作为参数。相反,它使用内省向后查看 前一条指令 在当前交易中。

这种机制确保了除非用户以密码学方式签署了程序加载的确切配置参数(即,已签名的消息与提供的会话配置匹配),否则无法创建会话帐户。

start_session 指令执行以下操作:

\ ‍

步骤 1:加载和验证意图
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 验证指令。这将检索用户的签名和已签名的消息,并将它们以密码学方式绑定到此交易。

步骤 2:验证上下文完整性
ctx.accounts.check_session_key(message.session_key)?; // 匹配目标密钥
ctx.accounts.check_chain_id(message.chain_id)?;       // 验证链 ID
// ... additional validation checks

该程序验证:

  • 消息中的 session_key 与正在初始化的会话帐户匹配
  • chain_id 与链上的 ChainId 帐户匹配
  • 域已在域注册表中正确注册
  • 过期时间戳有效
步骤 3:应用 Token 授权
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 的会话,程序:

  1. 将剩余帐户转换为待处理的授权操作
  2. 使用从 SESSION_SETTER_SEED 派生的 特殊 PDA 执行 token 授权
  3. 此 PDA 被 Token 程序识别,允许会话管理器在用户 token 帐户上设置委托金额

对于具有 Tokens::All 的会话,不会设置预先授权,因为 Token 程序将在运行时绕过委派检查。

步骤 4:初始化会话帐户
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)?;

该程序创建一个 会话帐户,其中包含:

  • sponsor: 支付会话租金的帐户(通常是用户或应用程序)
  • user: 创建会话的原始签名者
  • domain_hash: 域的哈希值,用于高效的程序授权检查
  • authorized_programs: 会话可以与之交互的程序 ID 列表
  • authorized_tokens: 具有批准金额的特定 mint 或 All 表示无限制
  • expiration: 会话过期的 Unix 时间戳
  • extra: 用户提供的其他元数据

然后,将会话帐户写入区块链,并将所有权分配给会话管理器程序。

运行时检查

初始化后,会话帐户可以充当集成 Fogo Sessions 的程序的签名者。与标准密钥对不同,其权限是有条件的。Fogo Chain 生态系统在每次交互期间在协议级别强制执行这些条件(过期时间戳、授权程序等)。

Token 操作(修改后的 Token 程序)

标准的 SPL Token 程序 已修改 以原生识别会话帐户。每当交易涉及由会话密钥签名的 token 转移时,程序都会触发 会话Hook 以在继续转移之前 验证 操作:

  1. 过期状态: Hook反序列化会话帐户,以确保当前区块时间戳在过期窗口内,并且会话状态未被撤销
  2. Token 帐户所有者: 它确保会话的用户与 Token 帐户所有者匹配。
  3. 授权程序: 遍历签名者列表,并验证它是否在活动会话中被列为授权程序,并且授权程序使用预期的 PDA 进行了签名。
// 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 范围是 Specific,则会回退到标准的 delegated_amount 检查。
  • 如果会话 token 范围是 All,则 Token 程序将 绕过 标准委派限制,允许根据用户的意图签名继续转移。

非 Token 操作

对于与不需要 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 指令执行完整的清理:

  1. 活跃度检查: 会话不得处于活动状态(已过期或已撤销)。!session.is_live()? 约束 确保了这一点。
  2. Token 委派清理: 对于具有 Tokens::Specific 的会话,该指令 撤销所有 token 授权,再次使用 SESSION_SETTER_SEED PDA 来授权撤销。
  3. 帐户关闭: 这是使用 close Anchor 约束 进行管理的

注意:

  • 具有 Tokens::All 的会话不设置预先委派,因此不需要 token 撤销
  • 具有 Tokens::Specific 的旧会话版本 (V1/V2) 由于存储的 mint 信息中可能存在不一致而无法关闭
  • 关闭操作仅适用于最初为会话创建提供资金的 sponsor

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 实际上属于授权会话的用户。

程序签名者 PDA 验证

在执行任何会话 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将验证:

  • PDA 与会话帐户中的 authorized_programs 之一匹配
  • 进行 CPI 的程序确实是拥有此 PDA 的程序

会话 Token 转移

提取用户并验证程序签名者后,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:

  1. 验证会话是否有效且未撤销
  2. 确认 token 帐户所有者与会话的用户匹配
  3. 检查 program_signer_info 是否与会话中的授权程序对应
  4. 验证支出限额(如果会话具有 Tokens::All,则绕过它们)

相同的模式适用于提款期间的 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Adevar labs
Adevar labs
Blockchain Security Firm | Rust, Solidity, Move, and beyond. Vulnerability discovery, practical remediation, and complete audit reports | Ship Safely.