The Light-SDK sponsors rent-exemption for your PDAs, token accounts, and mints. Your program logic stays the same.
Before After Rent (avg. DeFi pool) ~$2 ~$0.02
What Changes
Area Change State struct Add compression_info: CompressionInfo field, derive LightPinocchioAccount Program enum Derive LightProgramPinocchio to generate compress/decompress handlers Entrypoint Route generated discriminators alongside your custom ones Init handler Replace spl_token CPIs with light_token_pinocchio CPIs to create rent-free accounts Other instructions No changes
If you use Anchor instead of Pinocchio, see Program Integration .
Step 1: Dependencies
[ dependencies ]
light-account-pinocchio = { version = "0.20" , features = [ "token" , "std" ] }
light-token-pinocchio = "0.20"
pinocchio = "0.9"
pinocchio-pubkey = { version = "0.3" , features = [ "const" ] }
pinocchio-system = "0.3"
borsh = { version = "0.10.4" , default-features = false }
bytemuck = { version = "1.21" , features = [ "derive" ] }
Step 2: State Struct
Add compression_info field and derive LightPinocchioAccount:
use borsh :: { BorshDeserialize , BorshSerialize };
use light_account_pinocchio :: { CompressionInfo , LightPinocchioAccount };
#[derive(
Default , Debug , Copy , Clone , PartialEq ,
BorshSerialize , BorshDeserialize ,
LightPinocchioAccount ,
bytemuck :: Pod , bytemuck :: Zeroable ,
)]
#[repr( C )]
pub struct PoolState {
pub compression_info : CompressionInfo ,
// Your regular state...
pub fee_bps : u16 ,
}
Step 3: Program Enum
Declare your account types with their seed schemas:
use light_account_pinocchio :: {
derive_light_cpi_signer, pubkey_array, CpiSigner , LightProgramPinocchio ,
};
use pinocchio :: pubkey :: Pubkey ;
pub const ID : Pubkey = pubkey_array! ( "YourProgram11111111111111111111111111111111" );
pub const LIGHT_CPI_SIGNER : CpiSigner =
derive_light_cpi_signer! ( "YourProgram11111111111111111111111111111111" );
#[derive( LightProgramPinocchio )]
pub enum ProgramAccounts {
#[light_account(pda :: seeds = [ POOL_SEED , ctx . mint_a, ctx . mint_b], pda :: zero_copy )]
PoolState ( PoolState ),
#[light_account(token :: seeds = [ POOL_VAULT_SEED , ctx . pool, ctx . mint], token :: owner_seeds = [ POOL_AUTHORITY_SEED ])]
Vault ,
#[light_account(associated_token)]
UserToken ,
}
This auto-generates 4 instructions, discriminators, and the LightAccountVariant enum used by the client SDK.
Step 4: Entrypoint
Dispatch the generated handlers in your entrypoint
pinocchio :: entrypoint! ( process_instruction );
pub fn process_instruction (
_program_id : & Pubkey ,
accounts : & [ AccountInfo ],
instruction_data : & [ u8 ],
) -> Result <(), ProgramError > {
if instruction_data . len () < 8 {
return Err ( ProgramError :: InvalidInstructionData );
}
let ( disc , data ) = instruction_data . split_at ( 8 );
let disc : [ u8 ; 8 ] = disc . try_into () . unwrap ();
match disc {
// your custom program logic...
discriminators :: INITIALIZE => process_initialize ( accounts , data ),
discriminators :: SWAP => process_swap ( accounts , data ),
// add this:
ProgramAccounts :: INITIALIZE_COMPRESSION_CONFIG => {
ProgramAccounts :: process_initialize_config ( accounts , data ) // generated
}
ProgramAccounts :: UPDATE_COMPRESSION_CONFIG => {
ProgramAccounts :: process_update_config ( accounts , data )
}
ProgramAccounts :: COMPRESS_ACCOUNTS_IDEMPOTENT => {
ProgramAccounts :: process_compress ( accounts , data )
}
ProgramAccounts :: DECOMPRESS_ACCOUNTS_IDEMPOTENT => {
ProgramAccounts :: process_decompress ( accounts , data )
}
_ => Err ( ProgramError :: InvalidInstructionData ),
}
}
Step 5: Init Handler
Update your init instruction. Use light_token_pinocchio CPI builders to create rent-free token accounts.
Create Token Account
Create Mint
use light_account_pinocchio :: CreateTokenAccountCpi ;
CreateTokenAccountCpi {
payer : ctx . payer,
account : vault ,
mint ,
owner : * pool_authority . key (),
}
. rent_free (
ctx . light_token_config,
ctx . light_token_rent_sponsor,
ctx . system_program,
& crate :: ID ,
)
. invoke_signed ( & [
POOL_VAULT_SEED ,
pool_key . as_ref (),
mint_key . as_ref (),
& [ bump ],
]) ? ;
use light_account_pinocchio :: { CreateMints , CreateMintsStaticAccounts , SingleMintParams };
let sdk_mints : [ SingleMintParams <' _ >; 2 ] = [
SingleMintParams {
decimals : 9 ,
mint_authority : authority_key ,
mint_bump : None ,
freeze_authority : None ,
mint_seed_pubkey : mint_signer_a_key ,
authority_seeds : None ,
mint_signer_seeds : Some ( mint_signer_a_seeds ),
token_metadata : None ,
},
// ...
];
CreateMints {
mints : & sdk_mints ,
proof_data : & params . create_accounts_proof,
mint_seed_accounts : ctx . mint_signers,
mint_accounts : ctx . mints,
static_accounts : CreateMintsStaticAccounts {
fee_payer : ctx . payer,
compressible_config : ctx . light_token_config,
rent_sponsor : ctx . light_token_rent_sponsor,
cpi_authority : ctx . cpi_authority,
},
cpi_context_offset : 1 ,
}
. invoke ( & cpi_accounts ) ? ;
Full Initialize Processor
use light_account_pinocchio :: {
prepare_compressed_account_on_init, CompressedCpiContext , CpiAccounts , CpiAccountsConfig ,
CpiContextWriteAccounts , CreateMints , CreateMintsStaticAccounts , CreateTokenAccountCpi ,
InstructionDataInvokeCpiWithAccountInfo , InvokeLightSystemProgram , LightAccount , LightConfig ,
SingleMintParams ,
};
use pinocchio :: sysvars :: { clock :: Clock , Sysvar };
pub fn process (
ctx : & InitializeAccounts <' _ >,
params : & InitializeParams ,
remaining_accounts : & [ AccountInfo ],
) -> Result <(), LightSdkTypesError > {
// 1. Build CPI accounts
let config = CpiAccountsConfig :: new_with_cpi_context ( crate :: LIGHT_CPI_SIGNER );
let cpi_accounts = CpiAccounts :: new_with_config (
ctx . payer,
& remaining_accounts [ params . create_accounts_proof . system_accounts_offset as usize .. ],
config ,
);
// 2. Get address tree info + config
let address_tree_info = & params . create_accounts_proof . address_tree_info;
let address_tree_pubkey = address_tree_info . get_tree_pubkey ( & cpi_accounts ) ? ;
let light_config = LightConfig :: load_checked ( ctx . compressible_config, & crate :: ID ) ? ;
let current_slot = Clock :: get () ?. slot;
// 3. Create pool PDA (write to CPI context)
{
let cpi_context = CompressedCpiContext :: first ();
let mut new_address_params = Vec :: with_capacity ( 1 );
let mut account_infos = Vec :: with_capacity ( 1 );
let pool_key = * ctx . pool . key ();
prepare_compressed_account_on_init (
& pool_key , & address_tree_pubkey , address_tree_info ,
params . create_accounts_proof . output_state_tree_index,
0 , & crate :: ID ,
& mut new_address_params , & mut account_infos ,
) ? ;
// Initialize pool state (zero-copy)
{
let mut data = ctx . pool . try_borrow_mut_data () ? ;
let pool_state : & mut PoolState = bytemuck :: from_bytes_mut (
& mut data [ 8 .. 8 + core :: mem :: size_of :: < PoolState >()]
);
pool_state . set_decompressed ( & light_config , current_slot );
pool_state . token_a_mint = * ctx . mint_a () . key ();
pool_state . token_b_mint = * ctx . mint_b () . key ();
// ... remaining fields
}
// Write to CPI context
let instruction_data = InstructionDataInvokeCpiWithAccountInfo {
mode : 1 ,
bump : crate :: LIGHT_CPI_SIGNER . bump,
invoking_program_id : crate :: LIGHT_CPI_SIGNER . program_id . into (),
proof : params . create_accounts_proof . proof . 0 ,
new_address_params ,
account_infos ,
// ...
};
instruction_data . invoke_write_to_cpi_context_first (
CpiContextWriteAccounts {
fee_payer : cpi_accounts . fee_payer (),
authority : cpi_accounts . authority () ? ,
cpi_context : cpi_accounts . cpi_context () ? ,
cpi_signer : crate :: LIGHT_CPI_SIGNER ,
}
) ? ;
}
// 4. Create mints
CreateMints { /* ... */ } . invoke ( & cpi_accounts ) ? ;
// 5. Create vaults (rent-free)
CreateTokenAccountCpi { /* ... */ } . rent_free ( /* ... */ ) . invoke_signed ( /* ... */ ) ? ;
Ok (())
}
Client SDK
Implement LightProgramInterface so clients can detect cold accounts and build load instructions.
Example: LightProgramInterface Implementation
use light_client :: interface :: {
AccountInterface , AccountSpec , ColdContext , LightProgramInterface , PdaSpec ,
};
use light_account :: token :: Token ;
use pinocchio_swap :: { LightAccountVariant , PoolState , PoolStateSeeds , VaultSeeds };
/// Flat SDK struct. All fields populated at construction from pool state data.
pub struct SwapSdk {
pub pool_state_pubkey : Pubkey ,
pub token_a_mint : Pubkey ,
pub token_b_mint : Pubkey ,
pub token_a_vault : Pubkey ,
pub token_b_vault : Pubkey ,
pub pool_authority : Pubkey ,
}
impl SwapSdk {
pub fn new ( pool_state_pubkey : Pubkey , pool_data : & [ u8 ]) -> Result < Self , SwapSdkError > {
let pool = PoolState :: deserialize ( & mut & pool_data [ 8 .. ]) ? ;
// ... derive addresses from pool state
Ok ( Self { pool_state_pubkey , /* ... */ })
}
}
impl LightProgramInterface for SwapSdk {
type Variant = LightAccountVariant ;
type Instruction = SwapInstruction ;
fn program_id () -> Pubkey { PROGRAM_ID }
fn instruction_accounts ( & self , ix : & Self :: Instruction ) -> Vec < Pubkey > {
match ix {
SwapInstruction :: Swap => vec! [
self . pool_state_pubkey,
self . token_a_vault,
self . token_b_vault,
self . token_a_mint,
self . token_b_mint,
],
// ...
}
}
fn load_specs (
& self ,
cold_accounts : & [ AccountInterface ],
) -> Result < Vec < AccountSpec < Self :: Variant >>, Box < dyn std :: error :: Error >> {
let mut specs = Vec :: new ();
for account in cold_accounts {
if account . key == self . pool_state_pubkey {
let pool = PoolState :: deserialize ( & mut & account . data ()[ 8 .. ]) ? ;
let variant = LightAccountVariant :: PoolState {
seeds : PoolStateSeeds { /* ... */ },
data : pool ,
};
specs . push ( AccountSpec :: Pda ( PdaSpec :: new ( account . clone (), variant , PROGRAM_ID )));
} else if account . key == self . token_a_vault {
let token : Token = Token :: deserialize ( & mut & account . data ()[ .. ]) ? ;
let variant = LightAccountVariant :: Vault ( TokenDataWithSeeds {
seeds : VaultSeeds { pool : /* ... */ , mint : /* ... */ },
token_data : token ,
});
specs . push ( AccountSpec :: Pda ( PdaSpec :: new ( account . clone (), variant , PROGRAM_ID )));
}
// ... token_b_vault, mints
}
Ok ( specs )
}
}
Resource Link Full SDK implementation sdk.rs
Testing
Example: Full Lifecycle Test
use light_program_test :: { LightProgramTest , Rpc };
use light_client :: interface :: {
create_load_instructions, get_create_accounts_proof,
AccountSpec , CreateAccountsProofInput , LightProgramInterface ,
};
#[tokio :: test]
async fn test_pool_lifecycle () {
let mut rpc = LightProgramTest :: new ( config ) . await . unwrap ();
// 1. Initialize pool (rent-free: pool PDA, 2 mints, 2 vaults)
let proof = get_create_accounts_proof ( & rpc , & program_id , vec! [
CreateAccountsProofInput :: pda ( pool_state ),
CreateAccountsProofInput :: mint ( mint_a_signer ),
CreateAccountsProofInput :: mint ( mint_b_signer ),
]) . await . unwrap ();
rpc . create_and_send_transaction ( & [ init_ix ], & payer . pubkey (), & [ & payer , & authority ])
. await . unwrap ();
// 2. Swap (hot path)
rpc . create_and_send_transaction ( & [ swap_ix ], & user . pubkey (), & [ & user ])
. await . unwrap ();
// 3. Trigger compression for the purpose of the test.
const SLOTS_PER_EPOCH : u64 = 13500 ;
rpc . warp_slot_forward ( SLOTS_PER_EPOCH * 30 ) . await . unwrap ();
// 4. Build SDK from pool state, fetch cold accounts
let pool_iface = rpc . get_account_interface ( & pool_state , None ) . await . unwrap () . value . unwrap ();
assert! ( pool_iface . is_cold ());
let sdk = SwapSdk :: new ( pool_state , pool_iface . data ()) . unwrap ();
let pubkeys = sdk . instruction_accounts ( & SwapInstruction :: Swap );
let accounts = rpc . get_multiple_account_interfaces ( pubkeys . iter () . collect (), None )
. await . unwrap () . value;
let cold : Vec < _ > = accounts . into_iter () . flatten () . filter ( | a | a . is_cold ()) . collect ();
// 5. Load cold accounts
let mut specs = sdk . load_specs ( & cold ) . unwrap ();
// Add user ATAs
let ata_a = rpc . get_associated_token_account_interface ( & user . pubkey (), & mint_a , None )
. await . unwrap () . value . unwrap ();
let ata_b = rpc . get_associated_token_account_interface ( & user . pubkey (), & mint_b , None )
. await . unwrap () . value . unwrap ();
specs . push ( AccountSpec :: Ata ( ata_a ));
specs . push ( AccountSpec :: Ata ( ata_b ));
let load_ixs = create_load_instructions ( & specs , payer . pubkey (), config_pda , & rpc )
. await . unwrap ();
// 6. Load and Swap
let mut all_ixs = load_ixs ;
all_ixs . push ( swap_ix );
rpc . create_and_send_transaction ( & all_ixs , & user . pubkey (), & [ & user ])
. await . unwrap ();
}
Resource Link Full test test_lifecycle.rs
How it works
The SDK pays the rent-exemption cost. After extended inactivity, cold accounts auto-compress. Your program only ever
interacts with hot accounts. Clients can safely load cold accounts back into the
onchain Solana account space when needed via create_load_instructions.
Under the hood, clients use AccountInterface - a superset of Solana’s
Account that unifies hot and cold state. See Router Integration
for details.
Hot (active) Cold (inactive) Storage On-chain Compressed Latency/CU No change +load instruction Your program code No change No change
Existing programs
If you want to migrate your program to rent-free accounts and would like hands-on support, join our tech Discord ,
or email us .
FAQ
Do I have to manually handle compression/decompression?
No. LightProgramPinocchio generates the handlers. Simply add the generated handlers to your entrypoint, and update your init instruction.
How does it prevent re-init attacks?
When creating an
account for the first time, the SDK provides a proof that the account doesn’t
exist in the cold address space. The SVM already verifies this for the onchain
space. Both address spaces are checked before creation, preventing re-init
attacks, even if the account is currently cold.
Who triggers compression?
Miners (Forester nodes) compress accounts that have been inactive for an extended period of time (when their virtual rent balance drops below threshold).
In practice, having to load cold accounts should be rare. The common path (hot) has no extra overhead and does not increase CU or txn size.
How is the SDK able to sponsor rent exemption?
Do rent-free accounts increase CU?
Hot path (e.g. swap, deposit, withdraw): No. Active accounts do not add CU overhead to your instructions.First time init + loading cold accounts: Yes, adds up to 15k-400k CU,
depending on number and type of accounts being initialized or loaded.