Skip to main content

session_keys/
lib.rs

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