session_keys/
lib.rs

1#![allow(unexpected_cfgs)]
2
3use anchor_lang::prelude::*;
4use anchor_lang::system_program;
5
6const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
7
8#[cfg(feature = "no-entrypoint")]
9pub use session_keys_macros::*;
10
11declare_id!("KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5");
12
13#[cfg(not(feature = "no-entrypoint"))]
14solana_security_txt::security_txt! {
15    name: "session_keys",
16    project_url: "https://magicblock.gg",
17    contacts: "email:dev@magicblock.gg,twitter:@magicblock",
18    policy: "",
19    preferred_languages: "en",
20    source_code: "https://github.com/magicblock-labs"
21}
22
23#[program]
24pub mod gpl_session {
25    use super::*;
26
27    // create a session token
28    pub fn create_session(
29        ctx: Context<CreateSessionToken>,
30        top_up: Option<bool>,
31        valid_until: Option<i64>,
32        lamports: Option<u64>,
33    ) -> Result<()> {
34        let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
35        create_session_token_handler(ctx, top_up, valid_until, lamports)
36    }
37
38    pub fn create_session_with_payer(
39        ctx: Context<CreateSessionTokenWithPayer>,
40        top_up: Option<bool>,
41        valid_until: Option<i64>,
42        lamports: Option<u64>,
43    ) -> Result<()> {
44        let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
45        create_session_token_with_payer_handler(ctx, top_up, valid_until, lamports)
46    }
47    // revoke a session token
48    pub fn revoke_session(ctx: Context<RevokeSessionToken>) -> Result<()> {
49        revoke_session_token_handler(ctx)
50    }
51
52    // V2 instructions
53    //
54    // Added the V2 instructions to support the new session token format.
55    // The new format allows session to be created with a payer which on revoking
56    // would send the lamports back to the payer.
57    pub fn create_session_v2(
58        ctx: Context<CreateSessionTokenV2>,
59        top_up: Option<bool>,
60        valid_until: Option<i64>,
61        lamports: Option<u64>,
62    ) -> Result<()> {
63        let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
64        create_session_token_handler_v2(ctx, top_up, valid_until, lamports)
65    }
66
67    pub fn revoke_session_v2(ctx: Context<RevokeSessionTokenV2>) -> Result<()> {
68        revoke_session_token_handler_v2(ctx)
69    }
70}
71
72fn process_session_params(top_up: Option<bool>, valid_until: Option<i64>) -> Result<(bool, i64)> {
73    let top_up = top_up.unwrap_or(false);
74    let valid_until = valid_until.unwrap_or(Clock::get()?.unix_timestamp + 60 * 60);
75    Ok((top_up, valid_until))
76}
77
78// Create a SessionToken account
79#[derive(Accounts)]
80pub struct CreateSessionToken<'info> {
81    #[account(
82        init,
83        seeds = [
84            SessionToken::SEED_PREFIX.as_bytes(),
85            target_program.key().as_ref(),
86            session_signer.key().as_ref(),
87            authority.key().as_ref()
88        ],
89        bump,
90        payer = authority,
91        space = SessionToken::LEN
92    )]
93    pub session_token: Account<'info, SessionToken>,
94
95    #[account(mut)]
96    pub session_signer: Signer<'info>,
97    #[account(mut)]
98    pub authority: Signer<'info>,
99
100    /// CHECK the target program is actually a program.
101    #[account(executable)]
102    pub target_program: AccountInfo<'info>,
103
104    pub system_program: Program<'info, System>,
105}
106
107struct CreateSessionTokenParams {
108    authority: Pubkey,
109    target_program: Pubkey,
110    session_signer: Pubkey,
111    top_up: bool,
112    valid_until: i64,
113    lamports: Option<u64>,
114}
115
116fn create_session_token_internal<'info>(
117    session_token: &mut Account<'info, SessionToken>,
118    params: CreateSessionTokenParams,
119    system_program: AccountInfo<'info>,
120    payer: AccountInfo<'info>,
121    session_signer_account: AccountInfo<'info>,
122) -> Result<()> {
123    let authority = params.authority;
124    let target_program = params.target_program;
125    let session_signer = params.session_signer;
126    let top_up = params.top_up;
127    let valid_until = params.valid_until;
128    let lamports = params.lamports;
129    // Valid until can't be greater than a week
130    require!(
131        valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
132        SessionError::ValidityTooLong
133    );
134
135    session_token.set_inner(SessionToken {
136        authority,
137        target_program,
138        session_signer,
139        valid_until,
140    });
141
142    // Top up the session signer account with some lamports to pay for the transaction fees
143    if top_up {
144        system_program::transfer(
145            CpiContext::new(
146                system_program,
147                system_program::Transfer {
148                    from: payer,
149                    to: session_signer_account,
150                },
151            ),
152            lamports.unwrap_or(LAMPORTS_PER_SOL / 100),
153        )?;
154    }
155
156    Ok(())
157}
158
159// Handler to create a session token account
160pub fn create_session_token_handler(
161    ctx: Context<CreateSessionToken>,
162    top_up: bool,
163    valid_until: i64,
164    lamports: Option<u64>,
165) -> Result<()> {
166    create_session_token_internal(
167        &mut ctx.accounts.session_token,
168        CreateSessionTokenParams {
169            authority: ctx.accounts.authority.key(),
170            target_program: ctx.accounts.target_program.key(),
171            session_signer: ctx.accounts.session_signer.key(),
172            top_up,
173            valid_until,
174            lamports,
175        },
176        ctx.accounts.system_program.to_account_info(),
177        ctx.accounts.authority.to_account_info(),
178        ctx.accounts.session_signer.to_account_info(),
179    )
180}
181
182// Create a SessionToken account
183#[derive(Accounts)]
184pub struct CreateSessionTokenWithPayer<'info> {
185    #[account(
186        init,
187        seeds = [
188            SessionToken::SEED_PREFIX.as_bytes(),
189            target_program.key().as_ref(),
190            session_signer.key().as_ref(),
191            authority.key().as_ref()
192        ],
193        bump,
194        payer = payer,
195        space = SessionToken::LEN
196    )]
197    pub session_token: Account<'info, SessionToken>,
198
199    #[account(mut)]
200    pub session_signer: Signer<'info>,
201    #[account(mut)]
202    pub payer: Signer<'info>,
203    pub authority: Signer<'info>,
204
205    /// CHECK the target program is actually a program.
206    #[account(executable)]
207    pub target_program: AccountInfo<'info>,
208
209    pub system_program: Program<'info, System>,
210}
211
212// Handler to create a session token account
213pub fn create_session_token_with_payer_handler(
214    ctx: Context<CreateSessionTokenWithPayer>,
215    top_up: bool,
216    valid_until: i64,
217    lamports: Option<u64>,
218) -> Result<()> {
219    create_session_token_internal(
220        &mut ctx.accounts.session_token,
221        CreateSessionTokenParams {
222            authority: ctx.accounts.authority.key(),
223            target_program: ctx.accounts.target_program.key(),
224            session_signer: ctx.accounts.session_signer.key(),
225            top_up,
226            valid_until,
227            lamports,
228        },
229        ctx.accounts.system_program.to_account_info(),
230        ctx.accounts.payer.to_account_info(),
231        ctx.accounts.session_signer.to_account_info(),
232    )
233}
234
235// Revoke a session token
236// We allow *anyone* to revoke a session token. This is because the session token is designed to
237// expire on it's own after a certain amount of time. However, if the session token is compromised
238// anyone can revoke it immediately.
239//
240// One attack vector here to consider, however is that a malicious actor could enumerate all the tokens
241// created using the program and revoke them all or keep revoking them as they are created. It is a
242// nuisance but not a security risk. We can easily address this by whitelisting a revoker.
243#[derive(Accounts)]
244pub struct RevokeSessionToken<'info> {
245    #[account(
246        mut,
247        seeds = [
248            SessionToken::SEED_PREFIX.as_bytes(),
249            session_token.target_program.key().as_ref(),
250            session_token.session_signer.key().as_ref(),
251            session_token.authority.key().as_ref()
252        ],
253        bump,
254        has_one = authority,
255        close = authority,
256    )]
257    pub session_token: Account<'info, SessionToken>,
258
259    #[account(mut)]
260    // Only the token authority can reclaim the rent
261    pub authority: SystemAccount<'info>,
262
263    pub system_program: Program<'info, System>,
264}
265
266// Handler to revoke a session token
267pub fn revoke_session_token_handler(_: Context<RevokeSessionToken>) -> Result<()> {
268    Ok(())
269}
270
271// V2 Accounts and Handlers
272
273// Create a SessionTokenV2 account
274#[derive(Accounts)]
275pub struct CreateSessionTokenV2<'info> {
276    #[account(
277        init,
278        seeds = [
279            SessionTokenV2::SEED_PREFIX.as_bytes(),
280            target_program.key().as_ref(),
281            session_signer.key().as_ref(),
282            authority.key().as_ref()
283        ],
284        bump,
285        payer = fee_payer,
286        space = SessionTokenV2::LEN
287    )]
288    pub session_token: Account<'info, SessionTokenV2>,
289
290    #[account(mut)]
291    pub session_signer: Signer<'info>,
292    #[account(mut)]
293    pub fee_payer: Signer<'info>,
294    pub authority: Signer<'info>,
295
296    /// CHECK the target program is actually a program.
297    #[account(executable)]
298    pub target_program: AccountInfo<'info>,
299
300    pub system_program: Program<'info, System>,
301}
302
303struct CreateSessionTokenV2Params {
304    authority: Pubkey,
305    target_program: Pubkey,
306    session_signer: Pubkey,
307    fee_payer: Pubkey,
308    top_up: bool,
309    valid_until: i64,
310    lamports: Option<u64>,
311}
312
313fn create_session_token_v2_internal<'info>(
314    session_token: &mut Account<'info, SessionTokenV2>,
315    params: CreateSessionTokenV2Params,
316    system_program: AccountInfo<'info>,
317    payer: AccountInfo<'info>,
318    session_signer_account: AccountInfo<'info>,
319) -> Result<()> {
320    let authority = params.authority;
321    let target_program = params.target_program;
322    let session_signer = params.session_signer;
323    let fee_payer = params.fee_payer;
324    let top_up = params.top_up;
325    let valid_until = params.valid_until;
326    let lamports = params.lamports;
327    // Valid until can't be greater than a week
328    require!(
329        valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
330        SessionError::ValidityTooLong
331    );
332
333    session_token.set_inner(SessionTokenV2 {
334        authority,
335        target_program,
336        session_signer,
337        fee_payer,
338        valid_until,
339    });
340
341    // Top up the session signer account with some lamports to pay for the transaction fees
342    if top_up {
343        system_program::transfer(
344            CpiContext::new(
345                system_program,
346                system_program::Transfer {
347                    from: payer,
348                    to: session_signer_account,
349                },
350            ),
351            lamports.unwrap_or(LAMPORTS_PER_SOL / 100),
352        )?;
353    }
354
355    Ok(())
356}
357
358// Handler to create a session token v2 account
359pub fn create_session_token_handler_v2(
360    ctx: Context<CreateSessionTokenV2>,
361    top_up: bool,
362    valid_until: i64,
363    lamports: Option<u64>,
364) -> Result<()> {
365    create_session_token_v2_internal(
366        &mut ctx.accounts.session_token,
367        CreateSessionTokenV2Params {
368            authority: ctx.accounts.authority.key(),
369            target_program: ctx.accounts.target_program.key(),
370            session_signer: ctx.accounts.session_signer.key(),
371            fee_payer: ctx.accounts.fee_payer.key(),
372            top_up,
373            valid_until,
374            lamports,
375        },
376        ctx.accounts.system_program.to_account_info(),
377        ctx.accounts.fee_payer.to_account_info(),
378        ctx.accounts.session_signer.to_account_info(),
379    )
380}
381
382// Revoke a session token V2
383//
384// Anybody can revoke session but only the fee payer will receive the lamports back.
385#[derive(Accounts)]
386pub struct RevokeSessionTokenV2<'info> {
387    #[account(
388        mut,
389        seeds = [
390            SessionTokenV2::SEED_PREFIX.as_bytes(),
391            session_token.target_program.key().as_ref(),
392            session_token.session_signer.key().as_ref(),
393            session_token.authority.key().as_ref()
394        ],
395        bump,
396        has_one = fee_payer,
397        has_one = authority,
398        close = fee_payer,
399    )]
400    pub session_token: Account<'info, SessionTokenV2>,
401
402    #[account(mut)]
403    // Lamports are sent back to the fee payer
404    pub fee_payer: SystemAccount<'info>,
405
406    // Requires to be a signer if session is still active
407    pub authority: SystemAccount<'info>,
408
409    pub system_program: Program<'info, System>,
410}
411
412// Handler to revoke a session token V2
413pub fn revoke_session_token_handler_v2(ctx: Context<RevokeSessionTokenV2>) -> Result<()> {
414    // If the session is still active, the authority must be a signer
415    if !ctx.accounts.session_token.is_expired()? {
416        require!(
417            ctx.accounts.authority.is_signer,
418            SessionError::InvalidAuthority
419        );
420    }
421    Ok(())
422}
423
424pub struct ValidityChecker<'info> {
425    pub session_token: Account<'info, SessionToken>,
426    pub session_signer: Signer<'info>,
427    pub authority: Pubkey,
428    pub target_program: Pubkey,
429}
430
431pub struct ValidityCheckerV2<'info> {
432    pub session_token: Account<'info, SessionTokenV2>,
433    pub session_signer: Signer<'info>,
434    pub authority: Pubkey,
435    pub target_program: Pubkey,
436}
437
438// SessionToken Account
439#[account]
440#[derive(Copy)]
441pub struct SessionToken {
442    pub authority: Pubkey,
443    pub target_program: Pubkey,
444    pub session_signer: Pubkey,
445    pub valid_until: i64,
446}
447
448#[account]
449#[derive(Copy)]
450pub struct SessionTokenV2 {
451    pub authority: Pubkey,
452    pub target_program: Pubkey,
453    pub session_signer: Pubkey,
454    // account that paid for initialization and receives lamports back on revoking
455    pub fee_payer: Pubkey,
456    pub valid_until: i64,
457}
458
459impl SessionToken {
460    pub const LEN: usize = 8 + std::mem::size_of::<Self>();
461    pub const SEED_PREFIX: &'static str = "session_token";
462
463    fn is_expired(&self) -> Result<bool> {
464        let now = Clock::get()?.unix_timestamp;
465        Ok(now < self.valid_until)
466    }
467
468    // validate the token
469    pub fn validate(&self, ctx: ValidityChecker) -> Result<bool> {
470        let target_program = ctx.target_program;
471        let session_signer = ctx.session_signer.key();
472        let authority = ctx.authority.key();
473
474        // Check the PDA seeds
475        let seeds = &[
476            SessionToken::SEED_PREFIX.as_bytes(),
477            target_program.as_ref(),
478            session_signer.as_ref(),
479            authority.as_ref(),
480        ];
481
482        let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
483
484        require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
485
486        // Check if the token has expired
487        self.is_expired()
488    }
489}
490
491impl SessionTokenV2 {
492    pub const LEN: usize = 8 + std::mem::size_of::<Self>();
493    pub const SEED_PREFIX: &'static str = "session_token_v2";
494}
495
496impl SessionTokenV2 {
497    pub fn is_expired(&self) -> Result<bool> {
498        let now = Clock::get()?.unix_timestamp;
499        Ok(now > self.valid_until)
500    }
501
502    // validate the token
503    pub fn validate(&self, ctx: ValidityCheckerV2) -> Result<bool> {
504        let target_program = ctx.target_program;
505        let session_signer = ctx.session_signer.key();
506        let authority = ctx.authority.key();
507
508        // Check the PDA seeds
509        let seeds = &[
510            SessionTokenV2::SEED_PREFIX.as_bytes(),
511            target_program.as_ref(),
512            session_signer.as_ref(),
513            authority.as_ref(),
514        ];
515
516        let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
517
518        require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
519
520        // Check if the token has expired
521        self.is_expired()
522    }
523}
524
525pub trait Session<'info> {
526    fn session_token(&self) -> Option<Account<'info, SessionToken>>;
527    fn session_signer(&self) -> Signer<'info>;
528    fn session_authority(&self) -> Pubkey;
529    fn target_program(&self) -> Pubkey;
530
531    fn is_valid(&self) -> Result<bool> {
532        let session_token = self.session_token().ok_or(SessionError::NoToken)?;
533        let validity_ctx = ValidityChecker {
534            session_token: session_token.clone(),
535            session_signer: self.session_signer(),
536            authority: self.session_authority(),
537            target_program: self.target_program(),
538        };
539        // Check if the token is valid
540        session_token.validate(validity_ctx)
541    }
542}
543
544pub trait SessionV2<'info> {
545    fn session_token(&self) -> Option<Account<'info, SessionTokenV2>>;
546    fn session_signer(&self) -> Signer<'info>;
547    fn session_authority(&self) -> Pubkey;
548    fn target_program(&self) -> Pubkey;
549
550    fn is_valid(&self) -> Result<bool> {
551        let session_token = self.session_token().ok_or(SessionError::NoToken)?;
552        let validity_ctx = ValidityCheckerV2 {
553            session_token: session_token.clone(),
554            session_signer: self.session_signer(),
555            authority: self.session_authority(),
556            target_program: self.target_program(),
557        };
558        // Check if the token is valid
559        session_token.validate(validity_ctx)
560    }
561}
562
563#[error_code]
564pub enum SessionError {
565    #[msg("Requested validity is too long")]
566    ValidityTooLong,
567    #[msg("Invalid session token")]
568    InvalidToken,
569    #[msg("No session token provided")]
570    NoToken,
571    #[msg("Invalid authority")]
572    InvalidAuthority,
573}