From 551f0f14a7753549456b8a365fd9fce4a746ddf1 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 2 Jul 2025 23:08:29 -0400 Subject: [PATCH 01/16] wip --- .../sdk-test/src/compress_from_pda.rs | 141 +++++++++++++ .../sdk-test/src/decompress_to_pda.rs | 189 ++++++++++++++++++ program-tests/sdk-test/src/lib.rs | 18 ++ .../sdk-test/src/update_decompressed_pda.rs | 84 ++++++++ 4 files changed, 432 insertions(+) create mode 100644 program-tests/sdk-test/src/compress_from_pda.rs create mode 100644 program-tests/sdk-test/src/decompress_to_pda.rs create mode 100644 program-tests/sdk-test/src/update_decompressed_pda.rs diff --git a/program-tests/sdk-test/src/compress_from_pda.rs b/program-tests/sdk-test/src/compress_from_pda.rs new file mode 100644 index 0000000000..1a27556da9 --- /dev/null +++ b/program-tests/sdk-test/src/compress_from_pda.rs @@ -0,0 +1,141 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + error::LightSdkError, + instruction::ValidityProof, +}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, + sysvar::Sysvar, +}; + +use crate::{create_pda::MyCompressedAccount, decompress_to_pda::DecompressedPdaAccount}; + +/// Compresses a PDA back into a compressed account +/// Anyone can call this after the timeout period has elapsed +pub fn compress_from_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + msg!("Compressing PDA back to compressed account"); + + let mut instruction_data = instruction_data; + let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let fee_payer = &accounts[0]; + let pda_account = &accounts[1]; + let rent_recipient = &accounts[2]; // Hardcoded by caller program + let _system_program = &accounts[3]; + + // Verify the PDA account is owned by our program + if pda_account.owner != &crate::ID { + msg!("PDA account not owned by this program"); + return Err(LightSdkError::ConstraintViolation); + } + + // Read and deserialize PDA data + let pda_data = pda_account.try_borrow_data()?; + + // Check discriminator + if &pda_data[..8] != b"decomppd" { + msg!("Invalid PDA discriminator"); + return Err(LightSdkError::ConstraintViolation); + } + + let decompressed_pda = DecompressedPdaAccount::deserialize(&mut &pda_data[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Check if enough time has passed + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + let current_slot = clock.slot; + let slots_elapsed = current_slot.saturating_sub(decompressed_pda.last_written_slot); + + if slots_elapsed < decompressed_pda.slots_until_compression { + msg!( + "Cannot compress yet. {} slots remaining", + decompressed_pda + .slots_until_compression + .saturating_sub(slots_elapsed) + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Derive PDA to verify it matches + let (pda_pubkey, _pda_bump) = Pubkey::find_program_address( + &[ + b"decompressed_pda", + &decompressed_pda.compressed_address, + &instruction_data.additional_seed, + ], + &crate::ID, + ); + + if pda_pubkey != *pda_account.key { + msg!("PDA derivation mismatch"); + return Err(LightSdkError::ConstraintViolation); + } + + // Drop the borrow before we close the account + drop(pda_data); + + // Close the PDA account and send rent to recipient + let pda_lamports = pda_account.lamports(); + **pda_account.try_borrow_mut_lamports()? = 0; + **rent_recipient.try_borrow_mut_lamports()? = rent_recipient + .lamports() + .checked_add(pda_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + // Clear the PDA data + pda_account.try_borrow_mut_data()?.fill(0); + + // Now create the compressed account with the latest data + let mut compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( + &crate::ID, + Some(decompressed_pda.compressed_address), + instruction_data.output_merkle_tree_index, + ); + + compressed_account.data = decompressed_pda.data; + + // Set up CPI accounts for light system program + let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + config.sol_pool_pda = true; // We're compressing SOL + + let cpi_accounts = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + config, + ); + + // Create CPI inputs + let mut cpi_inputs = CpiInputs::new_with_address( + instruction_data.proof, + vec![compressed_account.to_account_info()?], + vec![instruction_data.new_address_params], + ); + + // Set compression parameters + // We're compressing the lamports that were in the PDA + cpi_inputs.compress_or_decompress_lamports = Some(instruction_data.lamports_to_compress); + cpi_inputs.is_compress = true; + + // Invoke light system program + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + msg!("Successfully compressed PDA back to compressed account"); + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct CompressFromPdaInstructionData { + pub proof: ValidityProof, + pub new_address_params: light_sdk::address::PackedNewAddressParams, + pub output_merkle_tree_index: u8, + pub additional_seed: [u8; 32], // Must match the seed used in decompression + pub lamports_to_compress: u64, + pub system_accounts_offset: u8, +} diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs new file mode 100644 index 0000000000..949fb63e3d --- /dev/null +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -0,0 +1,189 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + error::LightSdkError, + instruction::{ + account_meta::{CompressedAccountMeta, CompressedAccountMetaTrait}, + ValidityProof, + }, + LightDiscriminator, LightHasher, +}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, program::invoke_signed, pubkey::Pubkey, + rent::Rent, system_instruction, sysvar::Sysvar, +}; + +pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; + +/// Account structure for the decompressed PDA +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct DecompressedPdaAccount { + /// The compressed account address this PDA was derived from + pub compressed_address: [u8; 32], + /// Slot when this account was last written + pub last_written_slot: u64, + /// Number of slots until this account can be compressed again + pub slots_until_compression: u64, + /// The actual account data + pub data: [u8; 31], + /// Flag to indicate if this is a decompressed account + pub is_decompressed: bool, +} + +/// Compressed account structure with decompression flag +#[derive( + Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize, +)] +pub struct DecompressedMarkerAccount { + /// Flag to indicate this account has been decompressed + pub is_decompressed: bool, +} + +/// Decompresses a compressed account into a PDA +/// The PDA is derived from the compressed account's address and other seeds +pub fn decompress_to_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + msg!("Decompressing compressed account to PDA"); + + let mut instruction_data = instruction_data; + let instruction_data = DecompressToPdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let fee_payer = &accounts[0]; + let pda_account = &accounts[1]; + let rent_payer = &accounts[2]; // Account that pays for PDA rent + let system_program = &accounts[3]; + + // Derive PDA from compressed address + let compressed_address = instruction_data.compressed_account.meta.address; + let (pda_pubkey, pda_bump) = Pubkey::find_program_address( + &[ + b"decompressed_pda", + &compressed_address, + &instruction_data.additional_seed, + ], + &crate::ID, + ); + + // Verify PDA matches + if pda_pubkey != *pda_account.key { + msg!("Invalid PDA pubkey"); + return Err(LightSdkError::ConstraintViolation); + } + + // Get current slot + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + let current_slot = clock.slot; + + // Calculate space needed for PDA + let space = std::mem::size_of::() + 8; // +8 for discriminator + + // Get minimum rent + let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; + let minimum_balance = rent.minimum_balance(space); + + // Create PDA account (rent payer pays for the PDA creation) + let create_account_ix = system_instruction::create_account( + rent_payer.key, + pda_account.key, + minimum_balance, + space as u64, + &crate::ID, + ); + + let signer_seeds = &[ + b"decompressed_pda".as_ref(), + compressed_address.as_ref(), + instruction_data.additional_seed.as_ref(), + &[pda_bump], + ]; + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + pda_account.clone(), + system_program.clone(), + ], + &[signer_seeds], + )?; + + // Initialize PDA with decompressed data + let decompressed_pda = DecompressedPdaAccount { + compressed_address, + last_written_slot: current_slot, + slots_until_compression: SLOTS_UNTIL_COMPRESSION, + data: instruction_data.compressed_account.data, + is_decompressed: true, + }; + + // Write data to PDA + decompressed_pda + .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Write discriminator + pda_account.try_borrow_mut_data()?[..8].copy_from_slice(b"decomppd"); + + // Now handle the compressed account side + // Create a marker account that indicates this compressed account has been decompressed + let marker_account = LightAccount::<'_, DecompressedMarkerAccount>::new_mut( + &crate::ID, + &instruction_data.compressed_account.meta, + DecompressedMarkerAccount { + is_decompressed: true, + }, + )?; + + // Set up CPI accounts for light system program + let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + config.sol_pool_pda = false; + config.sol_compression_recipient = true; // We need to decompress SOL to the PDA + + let cpi_accounts = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + config, + ); + + // Create CPI inputs with decompression + let mut cpi_inputs = CpiInputs::new( + instruction_data.proof, + vec![marker_account.to_account_info()?], + ); + + // Set decompression parameters + // Transfer all lamports from compressed account to the PDA + let lamports_to_decompress = instruction_data + .compressed_account + .meta + .get_lamports() + .unwrap_or(0); + + cpi_inputs.compress_or_decompress_lamports = Some(lamports_to_decompress); + cpi_inputs.is_compress = false; // This is decompression + + // Invoke light system program + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + msg!("Successfully decompressed account to PDA"); + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressToPdaInstructionData { + pub proof: ValidityProof, + pub compressed_account: DecompressMyCompressedAccount, + pub additional_seed: [u8; 32], // Additional seed for PDA derivation + pub system_accounts_offset: u8, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressMyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: [u8; 31], +} diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index 8fb2b71b2c..33c62c8256 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -4,7 +4,10 @@ use solana_program::{ account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, }; +pub mod compress_from_pda; pub mod create_pda; +pub mod decompress_to_pda; +pub mod update_decompressed_pda; pub mod update_pda; pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); @@ -17,6 +20,9 @@ entrypoint!(process_instruction); pub enum InstructionType { CreatePdaBorsh = 0, UpdatePdaBorsh = 1, + DecompressToPda = 2, + CompressFromPda = 3, + UpdateDecompressedPda = 4, } impl TryFrom for InstructionType { @@ -26,6 +32,9 @@ impl TryFrom for InstructionType { match value { 0 => Ok(InstructionType::CreatePdaBorsh), 1 => Ok(InstructionType::UpdatePdaBorsh), + 2 => Ok(InstructionType::DecompressToPda), + 3 => Ok(InstructionType::CompressFromPda), + 4 => Ok(InstructionType::UpdateDecompressedPda), _ => panic!("Invalid instruction discriminator."), } } @@ -44,6 +53,15 @@ pub fn process_instruction( InstructionType::UpdatePdaBorsh => { update_pda::update_pda::(accounts, &instruction_data[1..]) } + InstructionType::DecompressToPda => { + decompress_to_pda::decompress_to_pda(accounts, &instruction_data[1..]) + } + InstructionType::CompressFromPda => { + compress_from_pda::compress_from_pda(accounts, &instruction_data[1..]) + } + InstructionType::UpdateDecompressedPda => { + update_decompressed_pda::update_decompressed_pda(accounts, &instruction_data[1..]) + } }?; Ok(()) } diff --git a/program-tests/sdk-test/src/update_decompressed_pda.rs b/program-tests/sdk-test/src/update_decompressed_pda.rs new file mode 100644 index 0000000000..8b1d655176 --- /dev/null +++ b/program-tests/sdk-test/src/update_decompressed_pda.rs @@ -0,0 +1,84 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::error::LightSdkError; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, pubkey::Pubkey, sysvar::Sysvar, +}; + +use crate::decompress_to_pda::DecompressedPdaAccount; + +/// Updates the data in a decompressed PDA +/// This also updates the last_written_slot to the current slot +pub fn update_decompressed_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + msg!("Updating decompressed PDA data"); + + let mut instruction_data = instruction_data; + let instruction_data = UpdateDecompressedPdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let authority = &accounts[0]; // Must be a signer + let pda_account = &accounts[1]; + + // Verify authority is signer + if !authority.is_signer { + msg!("Authority must be a signer"); + return Err(LightSdkError::ConstraintViolation); + } + + // Verify the PDA account is owned by our program + if pda_account.owner != &crate::ID { + msg!("PDA account not owned by this program"); + return Err(LightSdkError::ConstraintViolation); + } + + // Read and deserialize PDA data + let mut pda_data = pda_account.try_borrow_mut_data()?; + + // Check discriminator + if &pda_data[..8] != b"decomppd" { + msg!("Invalid PDA discriminator"); + return Err(LightSdkError::ConstraintViolation); + } + + let mut decompressed_pda = DecompressedPdaAccount::deserialize(&mut &pda_data[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Derive PDA to verify it matches + let (pda_pubkey, _pda_bump) = Pubkey::find_program_address( + &[ + b"decompressed_pda", + &decompressed_pda.compressed_address, + &instruction_data.additional_seed, + ], + &crate::ID, + ); + + if pda_pubkey != *pda_account.key { + msg!("PDA derivation mismatch"); + return Err(LightSdkError::ConstraintViolation); + } + + // Update the data + decompressed_pda.data = instruction_data.new_data; + + // Update the last_written_slot to current slot + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + decompressed_pda.last_written_slot = clock.slot; + + // Write updated data back + decompressed_pda + .serialize(&mut &mut pda_data[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + msg!("Successfully updated decompressed PDA data"); + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct UpdateDecompressedPdaInstructionData { + pub new_data: [u8; 31], + pub additional_seed: [u8; 32], // Must match the seed used in decompression +} From d2d0948d44e376089a9613579842942d72f70c98 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Jul 2025 15:45:27 -0400 Subject: [PATCH 02/16] add compress_pda helper --- .../sdk-test/src/compress_from_pda.rs | 123 +++--------------- program-tests/sdk-test/src/lib.rs | 1 + .../sdk-test/src/sdk/compress_pda.rs | 93 +++++++++++++ program-tests/sdk-test/src/sdk/mod.rs | 1 + 4 files changed, 116 insertions(+), 102 deletions(-) create mode 100644 program-tests/sdk-test/src/sdk/compress_pda.rs create mode 100644 program-tests/sdk-test/src/sdk/mod.rs diff --git a/program-tests/sdk-test/src/compress_from_pda.rs b/program-tests/sdk-test/src/compress_from_pda.rs index 1a27556da9..c3ddcc19c3 100644 --- a/program-tests/sdk-test/src/compress_from_pda.rs +++ b/program-tests/sdk-test/src/compress_from_pda.rs @@ -1,21 +1,22 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ - account::LightAccount, - cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, error::LightSdkError, - instruction::ValidityProof, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, }; use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, - sysvar::Sysvar, + account_info::AccountInfo, clock::Clock, msg, pubkey::Pubkey, sysvar::Sysvar, }; -use crate::{create_pda::MyCompressedAccount, decompress_to_pda::DecompressedPdaAccount}; +use crate::{ + create_pda::MyCompressedAccount, decompress_to_pda::DecompressedPdaAccount, + sdk::compress_pda::compress_pda, +}; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed -pub fn compress_from_pda( - accounts: &[AccountInfo], +/// pda check missing yet. +pub fn compress_from_pda<'a>( + accounts: &'a [AccountInfo<'a>], instruction_data: &[u8], ) -> Result<(), LightSdkError> { msg!("Compressing PDA back to compressed account"); @@ -27,8 +28,7 @@ pub fn compress_from_pda( // Get accounts let fee_payer = &accounts[0]; let pda_account = &accounts[1]; - let rent_recipient = &accounts[2]; // Hardcoded by caller program - let _system_program = &accounts[3]; + let rent_recipient = &accounts[2]; // can be hardcoded by caller program // Verify the PDA account is owned by our program if pda_account.owner != &crate::ID { @@ -36,95 +36,17 @@ pub fn compress_from_pda( return Err(LightSdkError::ConstraintViolation); } - // Read and deserialize PDA data - let pda_data = pda_account.try_borrow_data()?; - - // Check discriminator - if &pda_data[..8] != b"decomppd" { - msg!("Invalid PDA discriminator"); - return Err(LightSdkError::ConstraintViolation); - } - - let decompressed_pda = DecompressedPdaAccount::deserialize(&mut &pda_data[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - // Check if enough time has passed - let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; - let current_slot = clock.slot; - let slots_elapsed = current_slot.saturating_sub(decompressed_pda.last_written_slot); - - if slots_elapsed < decompressed_pda.slots_until_compression { - msg!( - "Cannot compress yet. {} slots remaining", - decompressed_pda - .slots_until_compression - .saturating_sub(slots_elapsed) - ); - return Err(LightSdkError::ConstraintViolation); - } - - // Derive PDA to verify it matches - let (pda_pubkey, _pda_bump) = Pubkey::find_program_address( - &[ - b"decompressed_pda", - &decompressed_pda.compressed_address, - &instruction_data.additional_seed, - ], - &crate::ID, - ); - - if pda_pubkey != *pda_account.key { - msg!("PDA derivation mismatch"); - return Err(LightSdkError::ConstraintViolation); - } - - // Drop the borrow before we close the account - drop(pda_data); - - // Close the PDA account and send rent to recipient - let pda_lamports = pda_account.lamports(); - **pda_account.try_borrow_mut_lamports()? = 0; - **rent_recipient.try_borrow_mut_lamports()? = rent_recipient - .lamports() - .checked_add(pda_lamports) - .ok_or(ProgramError::ArithmeticOverflow)?; - - // Clear the PDA data - pda_account.try_borrow_mut_data()?.fill(0); - - // Now create the compressed account with the latest data - let mut compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( - &crate::ID, - Some(decompressed_pda.compressed_address), - instruction_data.output_merkle_tree_index, - ); - - compressed_account.data = decompressed_pda.data; - - // Set up CPI accounts for light system program - let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - config.sol_pool_pda = true; // We're compressing SOL - - let cpi_accounts = CpiAccounts::new_with_config( + compress_pda::( + pda_account, + &instruction_data.compressed_account_meta, + Some(instruction_data.proof), + accounts, + instruction_data.system_accounts_offset, fee_payer, - &accounts[instruction_data.system_accounts_offset as usize..], - config, - ); - - // Create CPI inputs - let mut cpi_inputs = CpiInputs::new_with_address( - instruction_data.proof, - vec![compressed_account.to_account_info()?], - vec![instruction_data.new_address_params], - ); - - // Set compression parameters - // We're compressing the lamports that were in the PDA - cpi_inputs.compress_or_decompress_lamports = Some(instruction_data.lamports_to_compress); - cpi_inputs.is_compress = true; - - // Invoke light system program - cpi_inputs.invoke_light_system_program(cpi_accounts)?; + crate::LIGHT_CPI_SIGNER, + &crate::ID, + rent_recipient, + )?; msg!("Successfully compressed PDA back to compressed account"); Ok(()) @@ -133,9 +55,6 @@ pub fn compress_from_pda( #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] pub struct CompressFromPdaInstructionData { pub proof: ValidityProof, - pub new_address_params: light_sdk::address::PackedNewAddressParams, - pub output_merkle_tree_index: u8, - pub additional_seed: [u8; 32], // Must match the seed used in decompression - pub lamports_to_compress: u64, + pub compressed_account_meta: CompressedAccountMeta, pub system_accounts_offset: u8, } diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index 33c62c8256..6638767661 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -7,6 +7,7 @@ use solana_program::{ pub mod compress_from_pda; pub mod create_pda; pub mod decompress_to_pda; +pub mod sdk; pub mod update_decompressed_pda; pub mod update_pda; diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs new file mode 100644 index 0000000000..518758f38c --- /dev/null +++ b/program-tests/sdk-test/src/sdk/compress_pda.rs @@ -0,0 +1,93 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::{DataHasher, Hasher}; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs, CpiSigner}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, + LightDiscriminator, +}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +/// Helper function to compress a PDA and reclaim rent. +/// +/// 1. closes onchain PDA +/// 2. transfers PDA lamports to rent_recipient +/// 3. updates the empty compressed PDA with onchain PDA data +/// +/// This requires the compressed PDA that is tied to the onchain PDA to already +/// exist. +/// +/// # Arguments +/// * `pda_account` - The PDA account to compress (will be closed) +/// * `compressed_account_meta` - Metadata for the compressed account (must be +/// empty but have an address) +/// * `proof` - Optional validity proof +/// * `cpi_accounts` - Accounts needed for CPI starting from +/// system_accounts_offset +/// * `system_accounts_offset` - Offset where CPI accounts start +/// * `fee_payer` - The fee payer account +/// * `cpi_signer` - The CPI signer for the calling program +/// * `owner_program` - The program that will own the compressed account +/// * `rent_recipient` - The account to receive the PDA's rent +// +// TODO: +// - rent recipient check, eg hardcoded in caller program +// - check if any explicit checks required for compressed account? +// - check that the account is owned by the owner program, and derived from the correct seeds. +// - consider adding check here that the cAccount belongs to Account via seeds. +pub fn compress_pda<'a, A>( + pda_account: &AccountInfo<'a>, + compressed_account_meta: &CompressedAccountMeta, + proof: Option, + cpi_accounts: &'a [AccountInfo<'a>], + system_accounts_offset: u8, + fee_payer: &AccountInfo<'a>, + cpi_signer: CpiSigner, + owner_program: &Pubkey, + rent_recipient: &AccountInfo<'a>, +) -> Result<(), LightSdkError> +where + A: DataHasher + LightDiscriminator + BorshSerialize + BorshDeserialize + Default, +{ + // Get the PDA lamports before we close it + let pda_lamports = pda_account.lamports(); + + // Always use default/empty data since we're updating an existing compressed account + let compressed_account = + LightAccount::<'_, A>::new_mut(owner_program, compressed_account_meta, A::default())?; + + // Set up CPI configuration + let config = CpiAccountsConfig::new(cpi_signer); + + // Create CPI accounts structure + let cpi_accounts_struct = CpiAccounts::new_with_config( + fee_payer, + &cpi_accounts[system_accounts_offset as usize..], + config, + ); + + // Create CPI inputs + let cpi_inputs = CpiInputs::new( + proof.unwrap_or_default(), + vec![compressed_account.to_account_info()?], + ); + + // Invoke light system program to create the compressed account + cpi_inputs.invoke_light_system_program(cpi_accounts_struct)?; + + // Close the PDA account + // 1. Transfer all lamports to the rent recipient + let dest_starting_lamports = rent_recipient.lamports(); + **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports + .checked_add(pda_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + // 2. Decrement source account lamports + **pda_account.try_borrow_mut_lamports()? = 0; + // 3. Clear all account data + pda_account.try_borrow_mut_data()?.fill(0); + // 4. Assign ownership back to the system program + pda_account.assign(&solana_program::system_program::ID); + + Ok(()) +} diff --git a/program-tests/sdk-test/src/sdk/mod.rs b/program-tests/sdk-test/src/sdk/mod.rs new file mode 100644 index 0000000000..19b6974298 --- /dev/null +++ b/program-tests/sdk-test/src/sdk/mod.rs @@ -0,0 +1 @@ +pub mod compress_pda; From 88e043df110d8003979c5ce623595cb316721462 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Jul 2025 19:29:32 -0400 Subject: [PATCH 03/16] compress_pda compiling --- .../sdk-test/src/compress_from_pda.rs | 46 ++++---- .../sdk-test/src/decompress_to_pda.rs | 28 ++++- .../sdk-test/src/sdk/compress_pda.rs | 110 ++++++++++++++---- 3 files changed, 135 insertions(+), 49 deletions(-) diff --git a/program-tests/sdk-test/src/compress_from_pda.rs b/program-tests/sdk-test/src/compress_from_pda.rs index c3ddcc19c3..77d767f026 100644 --- a/program-tests/sdk-test/src/compress_from_pda.rs +++ b/program-tests/sdk-test/src/compress_from_pda.rs @@ -1,54 +1,51 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ + cpi::CpiAccounts, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, }; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, pubkey::Pubkey, sysvar::Sysvar, -}; +use light_sdk_types::CpiAccountsConfig; +use solana_program::account_info::AccountInfo; -use crate::{ - create_pda::MyCompressedAccount, decompress_to_pda::DecompressedPdaAccount, - sdk::compress_pda::compress_pda, -}; +use crate::{decompress_to_pda::DecompressedPdaAccount, sdk::compress_pda::compress_pda}; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed /// pda check missing yet. -pub fn compress_from_pda<'a>( - accounts: &'a [AccountInfo<'a>], +pub fn compress_from_pda( + accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { - msg!("Compressing PDA back to compressed account"); - let mut instruction_data = instruction_data; let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; - // Get accounts - let fee_payer = &accounts[0]; + // based on program... + let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; + let pda_account = &accounts[1]; let rent_recipient = &accounts[2]; // can be hardcoded by caller program - // Verify the PDA account is owned by our program - if pda_account.owner != &crate::ID { - msg!("PDA account not owned by this program"); - return Err(LightSdkError::ConstraintViolation); - } + // Cpi accounts + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts_struct = CpiAccounts::new_with_config( + &accounts[0], + &accounts[instruction_data.system_accounts_offset as usize..], + config, + ); - compress_pda::( + compress_pda::( pda_account, &instruction_data.compressed_account_meta, Some(instruction_data.proof), - accounts, - instruction_data.system_accounts_offset, - fee_payer, - crate::LIGHT_CPI_SIGNER, + cpi_accounts_struct, &crate::ID, rent_recipient, + &custom_seeds, )?; - msg!("Successfully compressed PDA back to compressed account"); + // any other program logic here... + Ok(()) } @@ -56,5 +53,6 @@ pub fn compress_from_pda<'a>( pub struct CompressFromPdaInstructionData { pub proof: ValidityProof, pub compressed_account_meta: CompressedAccountMeta, + pub additional_seed: [u8; 32], // Must match the seed used in decompression pub system_accounts_offset: u8, } diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index 949fb63e3d..be6b5ca92e 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::{DataHasher, Hasher}; use light_sdk::{ account::LightAccount, cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, @@ -17,7 +18,7 @@ use solana_program::{ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// Account structure for the decompressed PDA -#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] pub struct DecompressedPdaAccount { /// The compressed account address this PDA was derived from pub compressed_address: [u8; 32], @@ -187,3 +188,28 @@ pub struct DecompressMyCompressedAccount { pub meta: CompressedAccountMeta, pub data: [u8; 31], } + +// Implement required traits for DecompressedPdaAccount +impl DataHasher for DecompressedPdaAccount { + fn hash(&self) -> Result<[u8; 32], light_hasher::HasherError> { + let mut bytes = vec![]; + self.serialize(&mut bytes).unwrap(); + H::hashv(&[&bytes]) + } +} + +impl LightDiscriminator for DecompressedPdaAccount { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0xDE, 0xC0, 0x11, 0x9D, 0xA0, 0x00, 0x00, 0x00]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = + &[0xDE, 0xC0, 0x11, 0x9D, 0xA0, 0x00, 0x00, 0x00]; +} + +impl crate::sdk::compress_pda::PdaTimingData for DecompressedPdaAccount { + fn last_touched_slot(&self) -> u64 { + self.last_written_slot + } + + fn slots_buffer(&self) -> u64 { + self.slots_until_compression + } +} diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs index 518758f38c..94199558a4 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda.rs @@ -7,7 +7,51 @@ use light_sdk::{ instruction::{account_meta::CompressedAccountMeta, ValidityProof}, LightDiscriminator, }; -use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::sysvar::Sysvar; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, +}; + +/// Trait for PDA accounts that can be compressed +pub trait PdaTimingData { + fn last_touched_slot(&self) -> u64; + fn slots_buffer(&self) -> u64; +} + +const DECOMP_SEED: &[u8] = b"decomp"; + +/// Check that the PDA account is owned by the caller program and derived from the correct seeds. +/// +/// # Arguments +/// * `custom_seeds` - Custom seeds to check against +/// * `c_pda_address` - The address of the compressed PDA +/// * `pda_account` - The address of the PDA account +/// * `caller_program` - The program that owns the PDA. +pub fn check_pda( + custom_seeds: &[&[u8]], + c_pda_address: &[u8; 32], + pda_account: &Pubkey, + caller_program: &Pubkey, +) -> Result<(), ProgramError> { + // Create seeds array: [custom_seeds..., c_pda_address, "decomp"] + let mut seeds: Vec<&[u8]> = custom_seeds.to_vec(); + seeds.push(c_pda_address); + seeds.push(DECOMP_SEED); + + let derived_pda = + Pubkey::create_program_address(&seeds, caller_program).expect("Invalid PDA seeds."); + + if derived_pda != *pda_account { + msg!( + "Invalid PDA provided. Expected: {}. Found: {}.", + derived_pda, + pda_account + ); + return Err(ProgramError::InvalidArgument); + } + + Ok(()) +} /// Helper function to compress a PDA and reclaim rent. /// @@ -32,40 +76,58 @@ use solana_program::{account_info::AccountInfo, program_error::ProgramError, pub /// * `rent_recipient` - The account to receive the PDA's rent // // TODO: -// - rent recipient check, eg hardcoded in caller program // - check if any explicit checks required for compressed account? -// - check that the account is owned by the owner program, and derived from the correct seeds. -// - consider adding check here that the cAccount belongs to Account via seeds. -pub fn compress_pda<'a, A>( - pda_account: &AccountInfo<'a>, +// - consider multiple accounts per ix. +pub fn compress_pda( + pda_account: &AccountInfo, compressed_account_meta: &CompressedAccountMeta, proof: Option, - cpi_accounts: &'a [AccountInfo<'a>], - system_accounts_offset: u8, - fee_payer: &AccountInfo<'a>, - cpi_signer: CpiSigner, + cpi_accounts: CpiAccounts, owner_program: &Pubkey, - rent_recipient: &AccountInfo<'a>, + rent_recipient: &AccountInfo, + custom_seeds: &[&[u8]], ) -> Result<(), LightSdkError> where - A: DataHasher + LightDiscriminator + BorshSerialize + BorshDeserialize + Default, + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData, { + // Check that the PDA account is owned by the caller program and derived from the address of the compressed PDA. + check_pda( + custom_seeds, + &compressed_account_meta.address, + pda_account.key, + owner_program, + )?; + + let current_slot = Clock::get()?.slot; + + // Deserialize the PDA data to check timing fields + let pda_data = pda_account.try_borrow_data()?; + let pda_account_data = A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; + drop(pda_data); + + let last_touched_slot = pda_account_data.last_touched_slot(); + let slots_buffer = pda_account_data.slots_buffer(); + + if current_slot < last_touched_slot + slots_buffer { + msg!( + "Cannot compress yet. {} slots remaining", + (last_touched_slot + slots_buffer).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation); + } + // Get the PDA lamports before we close it let pda_lamports = pda_account.lamports(); - // Always use default/empty data since we're updating an existing compressed account - let compressed_account = + let mut compressed_account = LightAccount::<'_, A>::new_mut(owner_program, compressed_account_meta, A::default())?; - // Set up CPI configuration - let config = CpiAccountsConfig::new(cpi_signer); - - // Create CPI accounts structure - let cpi_accounts_struct = CpiAccounts::new_with_config( - fee_payer, - &cpi_accounts[system_accounts_offset as usize..], - config, - ); + compressed_account.account = pda_account_data; // Create CPI inputs let cpi_inputs = CpiInputs::new( @@ -74,7 +136,7 @@ where ); // Invoke light system program to create the compressed account - cpi_inputs.invoke_light_system_program(cpi_accounts_struct)?; + cpi_inputs.invoke_light_system_program(cpi_accounts)?; // Close the PDA account // 1. Transfer all lamports to the rent recipient From 152b6e23a2fe8ee3ff5e47808ed9e732f2209c13 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Jul 2025 23:20:56 -0400 Subject: [PATCH 04/16] decompress_idempotent.rs --- .../sdk-test/src/compress_from_pda.rs | 10 +- .../sdk-test/src/decompress_to_pda.rs | 209 ++++----------- program-tests/sdk-test/src/lib.rs | 7 +- .../sdk-test/src/sdk/compress_pda.rs | 12 +- .../sdk-test/src/sdk/decompress_idempotent.rs | 237 ++++++++++++++++++ program-tests/sdk-test/src/sdk/mod.rs | 1 + .../sdk-test/src/update_decompressed_pda.rs | 84 ------- 7 files changed, 296 insertions(+), 264 deletions(-) create mode 100644 program-tests/sdk-test/src/sdk/decompress_idempotent.rs delete mode 100644 program-tests/sdk-test/src/update_decompressed_pda.rs diff --git a/program-tests/sdk-test/src/compress_from_pda.rs b/program-tests/sdk-test/src/compress_from_pda.rs index 77d767f026..dd33315922 100644 --- a/program-tests/sdk-test/src/compress_from_pda.rs +++ b/program-tests/sdk-test/src/compress_from_pda.rs @@ -7,11 +7,12 @@ use light_sdk::{ use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; -use crate::{decompress_to_pda::DecompressedPdaAccount, sdk::compress_pda::compress_pda}; +use crate::{decompress_to_pda::MyPdaAccount, sdk::compress_pda::compress_pda}; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed /// pda check missing yet. +// TODO: add macro that create the full instruction. and takes: programid, account and seeds, rent_recipient (to hardcode). low code solution. pub fn compress_from_pda( accounts: &[AccountInfo], instruction_data: &[u8], @@ -20,7 +21,7 @@ pub fn compress_from_pda( let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; - // based on program... + // Custom seeds for PDA derivation (must match decompress_idempotent) let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; let pda_account = &accounts[1]; @@ -34,10 +35,10 @@ pub fn compress_from_pda( config, ); - compress_pda::( + compress_pda::( pda_account, &instruction_data.compressed_account_meta, - Some(instruction_data.proof), + instruction_data.proof, cpi_accounts_struct, &crate::ID, rent_recipient, @@ -53,6 +54,5 @@ pub fn compress_from_pda( pub struct CompressFromPdaInstructionData { pub proof: ValidityProof, pub compressed_account_meta: CompressedAccountMeta, - pub additional_seed: [u8; 32], // Must match the seed used in decompression pub system_accounts_offset: u8, } diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index be6b5ca92e..1b0b5e5183 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -1,54 +1,21 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::{DataHasher, Hasher}; use light_sdk::{ - account::LightAccount, - cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + cpi::{CpiAccounts, CpiAccountsConfig}, error::LightSdkError, - instruction::{ - account_meta::{CompressedAccountMeta, CompressedAccountMetaTrait}, - ValidityProof, - }, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, LightDiscriminator, LightHasher, }; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, program::invoke_signed, pubkey::Pubkey, - rent::Rent, system_instruction, sysvar::Sysvar, -}; - -pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; +use solana_program::account_info::AccountInfo; -/// Account structure for the decompressed PDA -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct DecompressedPdaAccount { - /// The compressed account address this PDA was derived from - pub compressed_address: [u8; 32], - /// Slot when this account was last written - pub last_written_slot: u64, - /// Number of slots until this account can be compressed again - pub slots_until_compression: u64, - /// The actual account data - pub data: [u8; 31], - /// Flag to indicate if this is a decompressed account - pub is_decompressed: bool, -} +use crate::sdk::decompress_idempotent::decompress_idempotent; -/// Compressed account structure with decompression flag -#[derive( - Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize, -)] -pub struct DecompressedMarkerAccount { - /// Flag to indicate this account has been decompressed - pub is_decompressed: bool, -} +pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; -/// Decompresses a compressed account into a PDA -/// The PDA is derived from the compressed account's address and other seeds +/// Decompresses a compressed account into a PDA idempotently. pub fn decompress_to_pda( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { - msg!("Decompressing compressed account to PDA"); - let mut instruction_data = instruction_data; let instruction_data = DecompressToPdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; @@ -59,152 +26,66 @@ pub fn decompress_to_pda( let rent_payer = &accounts[2]; // Account that pays for PDA rent let system_program = &accounts[3]; - // Derive PDA from compressed address - let compressed_address = instruction_data.compressed_account.meta.address; - let (pda_pubkey, pda_bump) = Pubkey::find_program_address( - &[ - b"decompressed_pda", - &compressed_address, - &instruction_data.additional_seed, - ], - &crate::ID, - ); - - // Verify PDA matches - if pda_pubkey != *pda_account.key { - msg!("Invalid PDA pubkey"); - return Err(LightSdkError::ConstraintViolation); - } - - // Get current slot - let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; - let current_slot = clock.slot; - - // Calculate space needed for PDA - let space = std::mem::size_of::() + 8; // +8 for discriminator - - // Get minimum rent - let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; - let minimum_balance = rent.minimum_balance(space); - - // Create PDA account (rent payer pays for the PDA creation) - let create_account_ix = system_instruction::create_account( - rent_payer.key, - pda_account.key, - minimum_balance, - space as u64, - &crate::ID, - ); - - let signer_seeds = &[ - b"decompressed_pda".as_ref(), - compressed_address.as_ref(), - instruction_data.additional_seed.as_ref(), - &[pda_bump], - ]; - - invoke_signed( - &create_account_ix, - &[ - rent_payer.clone(), - pda_account.clone(), - system_program.clone(), - ], - &[signer_seeds], - )?; - - // Initialize PDA with decompressed data - let decompressed_pda = DecompressedPdaAccount { - compressed_address, - last_written_slot: current_slot, - slots_until_compression: SLOTS_UNTIL_COMPRESSION, - data: instruction_data.compressed_account.data, - is_decompressed: true, - }; - - // Write data to PDA - decompressed_pda - .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - // Write discriminator - pda_account.try_borrow_mut_data()?[..8].copy_from_slice(b"decomppd"); - - // Now handle the compressed account side - // Create a marker account that indicates this compressed account has been decompressed - let marker_account = LightAccount::<'_, DecompressedMarkerAccount>::new_mut( - &crate::ID, - &instruction_data.compressed_account.meta, - DecompressedMarkerAccount { - is_decompressed: true, - }, - )?; - - // Set up CPI accounts for light system program - let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - config.sol_pool_pda = false; - config.sol_compression_recipient = true; // We need to decompress SOL to the PDA - + // Cpi accounts let cpi_accounts = CpiAccounts::new_with_config( fee_payer, &accounts[instruction_data.system_accounts_offset as usize..], - config, + CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), ); - // Create CPI inputs with decompression - let mut cpi_inputs = CpiInputs::new( - instruction_data.proof, - vec![marker_account.to_account_info()?], - ); + // Custom seeds for PDA derivation + let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; - // Set decompression parameters - // Transfer all lamports from compressed account to the PDA - let lamports_to_decompress = instruction_data - .compressed_account - .meta - .get_lamports() - .unwrap_or(0); - - cpi_inputs.compress_or_decompress_lamports = Some(lamports_to_decompress); - cpi_inputs.is_compress = false; // This is decompression + // Call the SDK function to decompress idempotently + // this inits pda_account if not already initialized + decompress_idempotent::( + pda_account, + Some(&instruction_data.compressed_account.meta), + &instruction_data.compressed_account.data, + instruction_data.proof, + cpi_accounts, + &crate::ID, + rent_payer, + system_program, + &custom_seeds, + &instruction_data.additional_seed, + )?; - // Invoke light system program - cpi_inputs.invoke_light_system_program(cpi_accounts)?; + // do something with pda_account... - msg!("Successfully decompressed account to PDA"); Ok(()) } #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] pub struct DecompressToPdaInstructionData { pub proof: ValidityProof, - pub compressed_account: DecompressMyCompressedAccount, - pub additional_seed: [u8; 32], // Additional seed for PDA derivation + pub compressed_account: MyCompressedAccount, + pub additional_seed: [u8; 32], // ... some seed pub system_accounts_offset: u8, } +// just a wrapper #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct DecompressMyCompressedAccount { +pub struct MyCompressedAccount { pub meta: CompressedAccountMeta, - pub data: [u8; 31], + pub data: MyPdaAccount, } -// Implement required traits for DecompressedPdaAccount -impl DataHasher for DecompressedPdaAccount { - fn hash(&self) -> Result<[u8; 32], light_hasher::HasherError> { - let mut bytes = vec![]; - self.serialize(&mut bytes).unwrap(); - H::hashv(&[&bytes]) - } -} - -impl LightDiscriminator for DecompressedPdaAccount { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0xDE, 0xC0, 0x11, 0x9D, 0xA0, 0x00, 0x00, 0x00]; - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = - &[0xDE, 0xC0, 0x11, 0x9D, 0xA0, 0x00, 0x00, 0x00]; +/// Account structure for the PDA +#[derive( + Clone, Debug, LightHasher, LightDiscriminator, Default, BorshDeserialize, BorshSerialize, +)] +pub struct MyPdaAccount { + /// Slot when this account was last written + pub last_written_slot: u64, + /// Number of slots after last_written_slot until this account can be compressed again + pub slots_until_compression: u64, + /// The actual account data + pub data: [u8; 31], } -impl crate::sdk::compress_pda::PdaTimingData for DecompressedPdaAccount { +// We require this trait to be implemented for the custom PDA account. +impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { fn last_touched_slot(&self) -> u64 { self.last_written_slot } @@ -212,4 +93,8 @@ impl crate::sdk::compress_pda::PdaTimingData for DecompressedPdaAccount { fn slots_buffer(&self) -> u64 { self.slots_until_compression } + + fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } } diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index 6638767661..545816eea8 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -8,7 +8,7 @@ pub mod compress_from_pda; pub mod create_pda; pub mod decompress_to_pda; pub mod sdk; -pub mod update_decompressed_pda; + pub mod update_pda; pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); @@ -23,7 +23,6 @@ pub enum InstructionType { UpdatePdaBorsh = 1, DecompressToPda = 2, CompressFromPda = 3, - UpdateDecompressedPda = 4, } impl TryFrom for InstructionType { @@ -35,7 +34,6 @@ impl TryFrom for InstructionType { 1 => Ok(InstructionType::UpdatePdaBorsh), 2 => Ok(InstructionType::DecompressToPda), 3 => Ok(InstructionType::CompressFromPda), - 4 => Ok(InstructionType::UpdateDecompressedPda), _ => panic!("Invalid instruction discriminator."), } } @@ -60,9 +58,6 @@ pub fn process_instruction( InstructionType::CompressFromPda => { compress_from_pda::compress_from_pda(accounts, &instruction_data[1..]) } - InstructionType::UpdateDecompressedPda => { - update_decompressed_pda::update_decompressed_pda(accounts, &instruction_data[1..]) - } }?; Ok(()) } diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs index 94199558a4..1c5a9702f7 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda.rs @@ -1,8 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::{DataHasher, Hasher}; +use light_hasher::DataHasher; use light_sdk::{ account::LightAccount, - cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs, CpiSigner}, + cpi::{CpiAccounts, CpiInputs}, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, LightDiscriminator, @@ -16,6 +16,7 @@ use solana_program::{ pub trait PdaTimingData { fn last_touched_slot(&self) -> u64; fn slots_buffer(&self) -> u64; + fn set_last_written_slot(&mut self, slot: u64); } const DECOMP_SEED: &[u8] = b"decomp"; @@ -81,7 +82,7 @@ pub fn check_pda( pub fn compress_pda( pda_account: &AccountInfo, compressed_account_meta: &CompressedAccountMeta, - proof: Option, + proof: ValidityProof, cpi_accounts: CpiAccounts, owner_program: &Pubkey, rent_recipient: &AccountInfo, @@ -130,10 +131,7 @@ where compressed_account.account = pda_account_data; // Create CPI inputs - let cpi_inputs = CpiInputs::new( - proof.unwrap_or_default(), - vec![compressed_account.to_account_info()?], - ); + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); // Invoke light system program to create the compressed account cpi_inputs.invoke_light_system_program(cpi_accounts)?; diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs new file mode 100644 index 0000000000..44829d5262 --- /dev/null +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -0,0 +1,237 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::DataHasher; +use light_sdk::{ + account::LightAccount, + cpi::{CpiAccounts, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, + LightDiscriminator, +}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, program::invoke_signed, pubkey::Pubkey, + rent::Rent, system_instruction, sysvar::Sysvar, +}; + +use crate::sdk::compress_pda::PdaTimingData; + +pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; + +/// Helper function to decompress a compressed account into a PDA idempotently. +/// +/// This function is idempotent, meaning it can be called multiple times with the same compressed account +/// and it will only decompress it once. If the PDA already exists and is initialized, it returns early. +/// +/// # Arguments +/// * `pda_account` - The PDA account to decompress into +/// * `compressed_account_meta` - Optional metadata for the compressed account (None if PDA already exists) +/// * `compressed_account_data` - The data to write to the PDA +/// * `proof` - Optional validity proof (None if PDA already exists) +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the PDA +/// * `rent_payer` - The account to pay for PDA rent +/// * `system_program` - The system program +/// * `custom_seeds` - Custom seeds for PDA derivation (without the compressed address) +/// * `additional_seed` - Additional seed for PDA derivation +/// +/// # Returns +/// * `Ok(())` if the compressed account was decompressed successfully or PDA already exists +/// * `Err(LightSdkError)` if there was an error +pub fn decompress_idempotent<'info, A>( + pda_account: &AccountInfo<'info>, + compressed_account_meta: Option<&CompressedAccountMeta>, + compressed_account_data: &A, + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + custom_seeds: &[&[u8]], + additional_seed: &[u8; 32], +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + Clone + + PdaTimingData, +{ + // Check if PDA is already initialized + if pda_account.data_len() > 0 { + msg!("PDA already initialized, skipping decompression"); + return Ok(()); + } + + // we zero out the compressed account. + let mut compressed_account = LightAccount::<'_, A>::new_mut( + owner_program, + compressed_account_meta.ok_or(LightSdkError::ConstraintViolation)?, + compressed_account_data.clone(), // TODO: try avoid clone + )?; + + // Get compressed address + let compressed_address = compressed_account + .address() + .ok_or(LightSdkError::ConstraintViolation)?; + + // Derive onchain PDA + // CHECK: PDA is derived from compressed account address. + let mut seeds: Vec<&[u8]> = custom_seeds.to_vec(); + seeds.push(&compressed_address); + seeds.push(additional_seed); + let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); // TODO: consider passing the bump. + + // Verify PDA matches + if pda_pubkey != *pda_account.key { + msg!("Invalid PDA pubkey"); + return Err(LightSdkError::ConstraintViolation); + } + + // Get current slot + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + let current_slot = clock.slot; + + // Calculate space needed for PDA + let space = std::mem::size_of::() + 8; // +8 for discriminator + + // Get minimum rent + let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; + let minimum_balance = rent.minimum_balance(space); + + // Create PDA account + let create_account_ix = system_instruction::create_account( + rent_payer.key, + pda_account.key, + minimum_balance, + space as u64, + owner_program, + ); + + // Add bump to seeds for signing + let bump_seed = [pda_bump]; + let mut signer_seeds = seeds.clone(); + signer_seeds.push(&bump_seed); + let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect(); + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + pda_account.clone(), + system_program.clone(), + ], + &[&signer_seeds_refs], + )?; + + // Serialize the account data + let mut data_bytes = vec![]; + compressed_account_data + .serialize(&mut data_bytes) + .map_err(|_| LightSdkError::Borsh)?; + + // Initialize PDA with decompressed data + let mut decompressed_pda: A = compressed_account.account; + decompressed_pda.set_last_written_slot(current_slot); + + // Write data to PDA + decompressed_pda + .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Zero the compressed account with CPI + compressed_account.account = A::default(); + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + drop(pda_account.try_borrow_mut_data()?); // todo: check if this is needed. + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressToPdaInstructionData { + pub proof: ValidityProof, + pub compressed_account: DecompressMyCompressedAccount, + pub additional_seed: [u8; 32], // Additional seed for PDA derivation + pub system_accounts_offset: u8, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressMyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: [u8; 31], +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decompress_to_pda::MyPdaAccount; + use light_sdk::cpi::CpiAccountsConfig; + + /// Test instruction that demonstrates idempotent decompression + /// This can be called multiple times with the same compressed account + pub fn test_decompress_idempotent( + accounts: &[AccountInfo], + instruction_data: &[u8], + ) -> Result<(), LightSdkError> { + msg!("Testing idempotent decompression"); + + #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] + struct TestInstructionData { + pub proof: ValidityProof, + pub compressed_account_meta: Option, + pub data: [u8; 31], + pub additional_seed: [u8; 32], + pub system_accounts_offset: u8, + } + + let mut instruction_data = instruction_data; + let instruction_data = TestInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let fee_payer = &accounts[0]; + let pda_account = &accounts[1]; + let rent_payer = &accounts[2]; + let system_program = &accounts[3]; + + // Set up CPI accounts + let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + config.sol_pool_pda = false; + config.sol_compression_recipient = false; + + let cpi_accounts = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + config, + ); + + // Prepare account data + let account_data = MyPdaAccount { + last_written_slot: 0, + slots_until_compression: SLOTS_UNTIL_COMPRESSION, + data: instruction_data.data, + }; + + // Custom seeds + let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; + + // Call decompress_idempotent - this should work whether PDA exists or not + decompress_idempotent::( + pda_account, + instruction_data.compressed_account_meta.as_ref(), + &account_data, + instruction_data.proof, + cpi_accounts, + &crate::ID, + rent_payer, + system_program, + &custom_seeds, + &instruction_data.additional_seed, + )?; + + msg!("Idempotent decompression completed successfully"); + Ok(()) + } +} diff --git a/program-tests/sdk-test/src/sdk/mod.rs b/program-tests/sdk-test/src/sdk/mod.rs index 19b6974298..42844b86ea 100644 --- a/program-tests/sdk-test/src/sdk/mod.rs +++ b/program-tests/sdk-test/src/sdk/mod.rs @@ -1 +1,2 @@ pub mod compress_pda; +pub mod decompress_idempotent; diff --git a/program-tests/sdk-test/src/update_decompressed_pda.rs b/program-tests/sdk-test/src/update_decompressed_pda.rs deleted file mode 100644 index 8b1d655176..0000000000 --- a/program-tests/sdk-test/src/update_decompressed_pda.rs +++ /dev/null @@ -1,84 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use light_sdk::error::LightSdkError; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, pubkey::Pubkey, sysvar::Sysvar, -}; - -use crate::decompress_to_pda::DecompressedPdaAccount; - -/// Updates the data in a decompressed PDA -/// This also updates the last_written_slot to the current slot -pub fn update_decompressed_pda( - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> Result<(), LightSdkError> { - msg!("Updating decompressed PDA data"); - - let mut instruction_data = instruction_data; - let instruction_data = UpdateDecompressedPdaInstructionData::deserialize(&mut instruction_data) - .map_err(|_| LightSdkError::Borsh)?; - - // Get accounts - let authority = &accounts[0]; // Must be a signer - let pda_account = &accounts[1]; - - // Verify authority is signer - if !authority.is_signer { - msg!("Authority must be a signer"); - return Err(LightSdkError::ConstraintViolation); - } - - // Verify the PDA account is owned by our program - if pda_account.owner != &crate::ID { - msg!("PDA account not owned by this program"); - return Err(LightSdkError::ConstraintViolation); - } - - // Read and deserialize PDA data - let mut pda_data = pda_account.try_borrow_mut_data()?; - - // Check discriminator - if &pda_data[..8] != b"decomppd" { - msg!("Invalid PDA discriminator"); - return Err(LightSdkError::ConstraintViolation); - } - - let mut decompressed_pda = DecompressedPdaAccount::deserialize(&mut &pda_data[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - // Derive PDA to verify it matches - let (pda_pubkey, _pda_bump) = Pubkey::find_program_address( - &[ - b"decompressed_pda", - &decompressed_pda.compressed_address, - &instruction_data.additional_seed, - ], - &crate::ID, - ); - - if pda_pubkey != *pda_account.key { - msg!("PDA derivation mismatch"); - return Err(LightSdkError::ConstraintViolation); - } - - // Update the data - decompressed_pda.data = instruction_data.new_data; - - // Update the last_written_slot to current slot - let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; - decompressed_pda.last_written_slot = clock.slot; - - // Write updated data back - decompressed_pda - .serialize(&mut &mut pda_data[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - msg!("Successfully updated decompressed PDA data"); - Ok(()) -} - -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct UpdateDecompressedPdaInstructionData { - pub new_data: [u8; 31], - pub additional_seed: [u8; 32], // Must match the seed used in decompression -} From 567d9f69f168d20abe470e480c53bb0839fbf49f Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Jul 2025 23:39:03 -0400 Subject: [PATCH 05/16] wip --- program-tests/sdk-test/src/decompress_to_pda.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index 1b0b5e5183..5c7635a0f7 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -9,7 +9,7 @@ use solana_program::account_info::AccountInfo; use crate::sdk::decompress_idempotent::decompress_idempotent; -pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; +pub const SLOTS_UNTIL_COMPRESSION: u64 = 10_000; /// Decompresses a compressed account into a PDA idempotently. pub fn decompress_to_pda( @@ -23,7 +23,7 @@ pub fn decompress_to_pda( // Get accounts let fee_payer = &accounts[0]; let pda_account = &accounts[1]; - let rent_payer = &accounts[2]; // Account that pays for PDA rent + let rent_payer = &accounts[2]; // Anyone can pay. let system_program = &accounts[3]; // Cpi accounts @@ -34,6 +34,7 @@ pub fn decompress_to_pda( ); // Custom seeds for PDA derivation + // Caller program should provide the seeds used for their onchain PDA. let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; // Call the SDK function to decompress idempotently From 117d3549c9296643aec029274fa381b532baee03 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 3 Jul 2025 23:54:41 -0400 Subject: [PATCH 06/16] wip --- .../sdk-test/src/decompress_to_pda.rs | 10 ++++-- .../sdk-test/src/sdk/decompress_idempotent.rs | 33 +++++++------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index 5c7635a0f7..877155a30f 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -1,5 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ + account::LightAccount, cpi::{CpiAccounts, CpiAccountsConfig}, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, @@ -32,6 +33,12 @@ pub fn decompress_to_pda( &accounts[instruction_data.system_accounts_offset as usize..], CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), ); + // we zero out the compressed account. + let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + &crate::ID, + &instruction_data.compressed_account.meta, + instruction_data.compressed_account.data, + )?; // Custom seeds for PDA derivation // Caller program should provide the seeds used for their onchain PDA. @@ -41,8 +48,7 @@ pub fn decompress_to_pda( // this inits pda_account if not already initialized decompress_idempotent::( pda_account, - Some(&instruction_data.compressed_account.meta), - &instruction_data.compressed_account.data, + compressed_account, instruction_data.proof, cpi_accounts, &crate::ID, diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs index 44829d5262..0d44d9cd8c 100644 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -38,8 +38,7 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// * `Err(LightSdkError)` if there was an error pub fn decompress_idempotent<'info, A>( pda_account: &AccountInfo<'info>, - compressed_account_meta: Option<&CompressedAccountMeta>, - compressed_account_data: &A, + mut compressed_account: LightAccount<'_, A>, proof: ValidityProof, cpi_accounts: CpiAccounts<'_, 'info>, owner_program: &Pubkey, @@ -63,13 +62,6 @@ where return Ok(()); } - // we zero out the compressed account. - let mut compressed_account = LightAccount::<'_, A>::new_mut( - owner_program, - compressed_account_meta.ok_or(LightSdkError::ConstraintViolation)?, - compressed_account_data.clone(), // TODO: try avoid clone - )?; - // Get compressed address let compressed_address = compressed_account .address() @@ -124,23 +116,17 @@ where &[&signer_seeds_refs], )?; - // Serialize the account data - let mut data_bytes = vec![]; - compressed_account_data - .serialize(&mut data_bytes) - .map_err(|_| LightSdkError::Borsh)?; - - // Initialize PDA with decompressed data - let mut decompressed_pda: A = compressed_account.account; + // Initialize PDA with decompressed data and update slot + let mut decompressed_pda = compressed_account.account.clone(); decompressed_pda.set_last_written_slot(current_slot); - // Write data to PDA decompressed_pda .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) .map_err(|_| LightSdkError::Borsh)?; - // Zero the compressed account with CPI + // Zero the compressed account compressed_account.account = A::default(); + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); cpi_inputs.invoke_light_system_program(cpi_accounts)?; @@ -214,14 +200,19 @@ mod tests { data: instruction_data.data, }; + let mut compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + &crate::ID, + &instruction_data.compressed_account_meta.unwrap(), + account_data, + )?; + // Custom seeds let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; // Call decompress_idempotent - this should work whether PDA exists or not decompress_idempotent::( pda_account, - instruction_data.compressed_account_meta.as_ref(), - &account_data, + compressed_account, instruction_data.proof, cpi_accounts, &crate::ID, From 9d78913f96e035a35e5515259158c8372b957a46 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 4 Jul 2025 00:06:19 -0400 Subject: [PATCH 07/16] decompress batch idempotent --- program-tests/sdk-test/src/decompress_to_pda.rs | 1 - program-tests/sdk-test/src/sdk/decompress_idempotent.rs | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index 877155a30f..2c5f58f432 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -55,7 +55,6 @@ pub fn decompress_to_pda( rent_payer, system_program, &custom_seeds, - &instruction_data.additional_seed, )?; // do something with pda_account... diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs index 0d44d9cd8c..dd0c060398 100644 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -45,7 +45,6 @@ pub fn decompress_idempotent<'info, A>( rent_payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, custom_seeds: &[&[u8]], - additional_seed: &[u8; 32], ) -> Result<(), LightSdkError> where A: DataHasher @@ -71,7 +70,7 @@ where // CHECK: PDA is derived from compressed account address. let mut seeds: Vec<&[u8]> = custom_seeds.to_vec(); seeds.push(&compressed_address); - seeds.push(additional_seed); + let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); // TODO: consider passing the bump. // Verify PDA matches @@ -200,7 +199,7 @@ mod tests { data: instruction_data.data, }; - let mut compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( &crate::ID, &instruction_data.compressed_account_meta.unwrap(), account_data, @@ -219,7 +218,6 @@ mod tests { rent_payer, system_program, &custom_seeds, - &instruction_data.additional_seed, )?; msg!("Idempotent decompression completed successfully"); From baf768f7d58ce54543c312775ec1b6764f1a5550 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 4 Jul 2025 00:12:42 -0400 Subject: [PATCH 08/16] wip --- .../sdk-test/src/decompress_to_pda.rs | 73 ++++++ .../sdk-test/src/sdk/decompress_idempotent.rs | 207 ++++++++++++------ 2 files changed, 209 insertions(+), 71 deletions(-) diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_to_pda.rs index 2c5f58f432..8dbc5e9dd2 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_to_pda.rs @@ -104,3 +104,76 @@ impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { self.last_written_slot = slot; } } + +/// Example: Decompresses multiple compressed accounts into PDAs in a single transaction. +pub fn decompress_multiple_to_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + use crate::sdk::decompress_idempotent::decompress_multiple_idempotent; + + #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] + pub struct DecompressMultipleInstructionData { + pub proof: ValidityProof, + pub compressed_accounts: Vec, + pub system_accounts_offset: u8, + } + + let mut instruction_data = instruction_data; + let instruction_data = DecompressMultipleInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get fixed accounts + let fee_payer = &accounts[0]; + let rent_payer = &accounts[1]; + let system_program = &accounts[2]; + + // Calculate where PDA accounts start + let pda_accounts_start = 3; + let num_accounts = instruction_data.compressed_accounts.len(); + + // Get PDA accounts + let pda_accounts = &accounts[pda_accounts_start..pda_accounts_start + num_accounts]; + + // Cpi accounts + let cpi_accounts = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + ); + + // Custom seeds for PDA derivation (same for all accounts in this example) + let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; + + // Build inputs for batch decompression + let mut compressed_accounts = Vec::new(); + let mut seeds_list = Vec::new(); + let mut pda_account_refs = Vec::new(); + + for (i, compressed_account_data) in instruction_data.compressed_accounts.into_iter().enumerate() + { + let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( + &crate::ID, + &compressed_account_data.meta, + compressed_account_data.data, + )?; + + compressed_accounts.push(compressed_account); + seeds_list.push(custom_seeds.clone()); + pda_account_refs.push(&pda_accounts[i]); + } + + // Decompress all accounts in one CPI call + decompress_multiple_idempotent::( + &pda_account_refs, + compressed_accounts, + &seeds_list, + instruction_data.proof, + cpi_accounts, + &crate::ID, + rent_payer, + system_program, + )?; + + Ok(()) +} diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs index dd0c060398..948dd69bf9 100644 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -23,9 +23,8 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// /// # Arguments /// * `pda_account` - The PDA account to decompress into -/// * `compressed_account_meta` - Optional metadata for the compressed account (None if PDA already exists) -/// * `compressed_account_data` - The data to write to the PDA -/// * `proof` - Optional validity proof (None if PDA already exists) +/// * `compressed_account` - The compressed account to decompress +/// * `proof` - Validity proof /// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the PDA /// * `rent_payer` - The account to pay for PDA rent @@ -38,7 +37,7 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// * `Err(LightSdkError)` if there was an error pub fn decompress_idempotent<'info, A>( pda_account: &AccountInfo<'info>, - mut compressed_account: LightAccount<'_, A>, + compressed_account: LightAccount<'_, A>, proof: ValidityProof, cpi_accounts: CpiAccounts<'_, 'info>, owner_program: &Pubkey, @@ -55,81 +54,147 @@ where + Clone + PdaTimingData, { - // Check if PDA is already initialized - if pda_account.data_len() > 0 { - msg!("PDA already initialized, skipping decompression"); - return Ok(()); - } + decompress_multiple_idempotent( + &[pda_account], + vec![compressed_account], + &[custom_seeds.to_vec()], + proof, + cpi_accounts, + owner_program, + rent_payer, + system_program, + ) +} - // Get compressed address - let compressed_address = compressed_account - .address() - .ok_or(LightSdkError::ConstraintViolation)?; +/// Helper function to decompress multiple compressed accounts into PDAs idempotently. +/// +/// This function is idempotent, meaning it can be called multiple times with the same compressed accounts +/// and it will only decompress them once. If a PDA already exists and is initialized, it skips that account. +/// +/// # Arguments +/// * `decompress_inputs` - Vector of tuples containing (pda_account, compressed_account, custom_seeds, additional_seed) +/// * `proof` - Single validity proof for all accounts +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the PDAs +/// * `rent_payer` - The account to pay for PDA rent +/// * `system_program` - The system program +/// +/// # Returns +/// * `Ok(())` if all compressed accounts were decompressed successfully or PDAs already exist +/// * `Err(LightSdkError)` if there was an error +pub fn decompress_multiple_idempotent<'info, A>( + pda_accounts: &[&AccountInfo<'info>], + compressed_accounts: Vec>, + custom_seeds_list: &[Vec<&[u8]>], + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + Clone + + PdaTimingData, +{ + // Get current slot and rent once for all accounts + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + let current_slot = clock.slot; + let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; - // Derive onchain PDA - // CHECK: PDA is derived from compressed account address. - let mut seeds: Vec<&[u8]> = custom_seeds.to_vec(); - seeds.push(&compressed_address); + // Calculate space needed for PDA (same for all accounts of type A) + let space = std::mem::size_of::() + 8; // +8 for discriminator + let minimum_balance = rent.minimum_balance(space); - let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); // TODO: consider passing the bump. + // Collect compressed accounts for CPI + let mut compressed_accounts_for_cpi = Vec::new(); + + for ((pda_account, mut compressed_account), custom_seeds) in pda_accounts + .iter() + .zip(compressed_accounts.into_iter()) + .zip(custom_seeds_list.iter()) + .map(|((pda, ca), seeds)| ((pda, ca), seeds.clone())) + { + // Check if PDA is already initialized + if pda_account.data_len() > 0 { + msg!( + "PDA {} already initialized, skipping decompression", + pda_account.key + ); + continue; + } - // Verify PDA matches - if pda_pubkey != *pda_account.key { - msg!("Invalid PDA pubkey"); - return Err(LightSdkError::ConstraintViolation); - } + // Get compressed address + let compressed_address = compressed_account + .address() + .ok_or(LightSdkError::ConstraintViolation)?; - // Get current slot - let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; - let current_slot = clock.slot; + // Derive onchain PDA + let mut seeds: Vec<&[u8]> = custom_seeds; + seeds.push(&compressed_address); - // Calculate space needed for PDA - let space = std::mem::size_of::() + 8; // +8 for discriminator + let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); - // Get minimum rent - let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; - let minimum_balance = rent.minimum_balance(space); + // Verify PDA matches + if pda_pubkey != *pda_account.key { + msg!("Invalid PDA pubkey for account {}", pda_account.key); + return Err(LightSdkError::ConstraintViolation); + } - // Create PDA account - let create_account_ix = system_instruction::create_account( - rent_payer.key, - pda_account.key, - minimum_balance, - space as u64, - owner_program, - ); - - // Add bump to seeds for signing - let bump_seed = [pda_bump]; - let mut signer_seeds = seeds.clone(); - signer_seeds.push(&bump_seed); - let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect(); - - invoke_signed( - &create_account_ix, - &[ - rent_payer.clone(), - pda_account.clone(), - system_program.clone(), - ], - &[&signer_seeds_refs], - )?; - - // Initialize PDA with decompressed data and update slot - let mut decompressed_pda = compressed_account.account.clone(); - decompressed_pda.set_last_written_slot(current_slot); - // Write data to PDA - decompressed_pda - .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - // Zero the compressed account - compressed_account.account = A::default(); - - let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); - cpi_inputs.invoke_light_system_program(cpi_accounts)?; - - drop(pda_account.try_borrow_mut_data()?); // todo: check if this is needed. + // Create PDA account + let create_account_ix = system_instruction::create_account( + rent_payer.key, + pda_account.key, + minimum_balance, + space as u64, + owner_program, + ); + + // Add bump to seeds for signing + let bump_seed = [pda_bump]; + let mut signer_seeds = seeds.clone(); + signer_seeds.push(&bump_seed); + let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect(); + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + (*pda_account).clone(), + system_program.clone(), + ], + &[&signer_seeds_refs], + )?; + + // Initialize PDA with decompressed data and update slot + let mut decompressed_pda = compressed_account.account.clone(); + decompressed_pda.set_last_written_slot(current_slot); + + // Write discriminator + let discriminator = A::LIGHT_DISCRIMINATOR; + pda_account.try_borrow_mut_data()?[..8].copy_from_slice(&discriminator); + + // Write data to PDA + decompressed_pda + .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Zero the compressed account + compressed_account.account = A::default(); + + // Add to CPI batch + compressed_accounts_for_cpi.push(compressed_account.to_account_info()?); + } + + // Make single CPI call with all compressed accounts + if !compressed_accounts_for_cpi.is_empty() { + let cpi_inputs = CpiInputs::new(proof, compressed_accounts_for_cpi); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + } Ok(()) } From ec3b731fa54f91dbc4b9ece61aa7ff173ee310b0 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 4 Jul 2025 13:24:45 -0400 Subject: [PATCH 09/16] add compress_pda_new and compress_multiple_pdas_new --- .../sdk-test/src/compress_from_pda_new.rs | 74 +++++ program-tests/sdk-test/src/lib.rs | 6 + .../sdk-test/src/sdk/compress_pda_new.rs | 256 ++++++++++++++++++ program-tests/sdk-test/src/sdk/mod.rs | 1 + 4 files changed, 337 insertions(+) create mode 100644 program-tests/sdk-test/src/compress_from_pda_new.rs create mode 100644 program-tests/sdk-test/src/sdk/compress_pda_new.rs diff --git a/program-tests/sdk-test/src/compress_from_pda_new.rs b/program-tests/sdk-test/src/compress_from_pda_new.rs new file mode 100644 index 0000000000..baf5f351a4 --- /dev/null +++ b/program-tests/sdk-test/src/compress_from_pda_new.rs @@ -0,0 +1,74 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{ + address::v1::derive_address, + cpi::CpiAccounts, + error::LightSdkError, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; +use light_sdk_types::CpiAccountsConfig; +use solana_program::account_info::AccountInfo; + +use crate::{decompress_to_pda::MyPdaAccount, sdk::compress_pda_new::compress_pda_new}; + +/// Compresses a PDA into a new compressed account +/// This creates a new compressed account with address derived from the PDA address +pub fn compress_from_pda_new( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = CompressFromPdaNewInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + let fee_payer = &accounts[0]; + let pda_account = &accounts[1]; + let rent_recipient = &accounts[2]; // can be hardcoded by caller program + + // Cpi accounts + let cpi_accounts_struct = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + ); + + // Get the address tree pubkey + let address_tree_pubkey = instruction_data + .address_tree_info + .get_tree_pubkey(&cpi_accounts_struct)?; + + // TODO: consider ENFORCING on our end that the cPDA is derived from the pda. + // this would simplify. + // Can do offchain! + let (address, address_seed) = derive_address( + &[pda_account.key.as_ref()], + &address_tree_pubkey, + &crate::ID, + ); + + // Can do offchain! + let new_address_params = instruction_data + .address_tree_info + .into_new_address_params_packed(address_seed); + + // Compress the PDA + compress_pda_new::( + pda_account, + address, + new_address_params, + instruction_data.output_state_tree_index, + instruction_data.proof, + cpi_accounts_struct, + &crate::ID, + rent_recipient, + )?; + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct CompressFromPdaNewInstructionData { + pub proof: ValidityProof, + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + pub system_accounts_offset: u8, +} diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index 545816eea8..a430739aa0 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -5,6 +5,7 @@ use solana_program::{ }; pub mod compress_from_pda; +pub mod compress_from_pda_new; pub mod create_pda; pub mod decompress_to_pda; pub mod sdk; @@ -23,6 +24,7 @@ pub enum InstructionType { UpdatePdaBorsh = 1, DecompressToPda = 2, CompressFromPda = 3, + CompressFromPdaNew = 4, } impl TryFrom for InstructionType { @@ -34,6 +36,7 @@ impl TryFrom for InstructionType { 1 => Ok(InstructionType::UpdatePdaBorsh), 2 => Ok(InstructionType::DecompressToPda), 3 => Ok(InstructionType::CompressFromPda), + 4 => Ok(InstructionType::CompressFromPdaNew), _ => panic!("Invalid instruction discriminator."), } } @@ -58,6 +61,9 @@ pub fn process_instruction( InstructionType::CompressFromPda => { compress_from_pda::compress_from_pda(accounts, &instruction_data[1..]) } + InstructionType::CompressFromPdaNew => { + compress_from_pda_new::compress_from_pda_new(accounts, &instruction_data[1..]) + } }?; Ok(()) } diff --git a/program-tests/sdk-test/src/sdk/compress_pda_new.rs b/program-tests/sdk-test/src/sdk/compress_pda_new.rs new file mode 100644 index 0000000000..50a8e3c263 --- /dev/null +++ b/program-tests/sdk-test/src/sdk/compress_pda_new.rs @@ -0,0 +1,256 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::DataHasher; +use light_sdk::{ + account::LightAccount, + address::{v1::derive_address, PackedNewAddressParams}, + cpi::{CpiAccounts, CpiInputs}, + error::LightSdkError, + instruction::ValidityProof, + LightDiscriminator, +}; +use solana_program::{ + account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, + sysvar::Sysvar, +}; + +use crate::sdk::compress_pda::PdaTimingData; + +/// Helper function to compress an onchain PDA into a new compressed account. +/// +/// This function handles the entire compression operation: creates a compressed account, +/// copies the PDA data, and closes the onchain PDA. +/// +/// # Arguments +/// * `pda_account` - The PDA account to compress (will be closed) +/// * `address` - The address for the compressed account +/// * `new_address_params` - Address parameters for the compressed account +/// * `output_state_tree_index` - Output state tree index for the compressed account +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed account +/// * `rent_recipient` - The account to receive the PDA's rent +/// +/// # Returns +/// * `Ok(())` if the PDA was compressed successfully +/// * `Err(LightSdkError)` if there was an error +pub fn compress_pda_new<'info, A>( + pda_account: &AccountInfo<'info>, + address: [u8; 32], + new_address_params: PackedNewAddressParams, + output_state_tree_index: u8, + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_recipient: &AccountInfo<'info>, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData + + Clone, +{ + compress_multiple_pdas_new::( + &[pda_account], + &[address], + vec![new_address_params], + &[output_state_tree_index], + proof, + cpi_accounts, + owner_program, + rent_recipient, + ) +} + +/// Helper function to compress multiple onchain PDAs into new compressed accounts. +/// +/// This function handles the entire compression operation for multiple PDAs. +/// +/// # Arguments +/// * `pda_accounts` - The PDA accounts to compress (will be closed) +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed accounts +/// * `proof` - Single validity proof for all accounts +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed accounts +/// * `rent_recipient` - The account to receive the PDAs' rent +/// +/// # Returns +/// * `Ok(())` if all PDAs were compressed successfully +/// * `Err(LightSdkError)` if there was an error +pub fn compress_multiple_pdas_new<'info, A>( + pda_accounts: &[&AccountInfo<'info>], + addresses: &[[u8; 32]], + new_address_params: Vec, + output_state_tree_indices: &[u8], + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_recipient: &AccountInfo<'info>, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData + + Clone, +{ + if pda_accounts.len() != addresses.len() + || pda_accounts.len() != new_address_params.len() + || pda_accounts.len() != output_state_tree_indices.len() + { + return Err(LightSdkError::ConstraintViolation); + } + + let current_slot = Clock::get()?.slot; + let mut total_lamports = 0u64; + let mut compressed_account_infos = Vec::new(); + + for ((pda_account, &address), &output_state_tree_index) in pda_accounts + .iter() + .zip(addresses.iter()) + .zip(output_state_tree_indices.iter()) + { + // Check that the PDA account is owned by the caller program + if pda_account.owner != owner_program { + msg!( + "Invalid PDA owner for {}. Expected: {}. Found: {}.", + pda_account.key, + owner_program, + pda_account.owner + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Deserialize the PDA data to check timing fields + let pda_data = pda_account.try_borrow_data()?; + let pda_account_data = + A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; + drop(pda_data); + + let last_touched_slot = pda_account_data.last_touched_slot(); + let slots_buffer = pda_account_data.slots_buffer(); + + if current_slot < last_touched_slot + slots_buffer { + msg!( + "Cannot compress {} yet. {} slots remaining", + pda_account.key, + (last_touched_slot + slots_buffer).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Create the compressed account with the PDA data + let mut compressed_account = + LightAccount::<'_, A>::new_init(owner_program, Some(address), output_state_tree_index); + compressed_account.account = pda_account_data; + + compressed_account_infos.push(compressed_account.to_account_info()?); + + // Accumulate lamports + total_lamports = total_lamports + .checked_add(pda_account.lamports()) + .ok_or(ProgramError::ArithmeticOverflow)?; + } + + // Create CPI inputs with all compressed accounts and new addresses + let cpi_inputs = + CpiInputs::new_with_address(proof, compressed_account_infos, new_address_params); + + // Invoke light system program to create all compressed accounts + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + // Close all PDA accounts + let dest_starting_lamports = rent_recipient.lamports(); + **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports + .checked_add(total_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + for pda_account in pda_accounts { + // Decrement source account lamports + **pda_account.try_borrow_mut_lamports()? = 0; + // Clear all account data + pda_account.try_borrow_mut_data()?.fill(0); + // Assign ownership back to the system program + pda_account.assign(&solana_program::system_program::ID); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decompress_to_pda::MyPdaAccount; + use light_sdk::cpi::CpiAccountsConfig; + use light_sdk::instruction::PackedAddressTreeInfo; + + /// Test instruction that demonstrates compressing an onchain PDA into a new compressed account + pub fn test_compress_pda_new( + accounts: &[AccountInfo], + instruction_data: &[u8], + ) -> Result<(), LightSdkError> { + msg!("Testing compress PDA into new compressed account"); + + #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] + struct TestInstructionData { + pub proof: ValidityProof, + pub address_tree_info: PackedAddressTreeInfo, + pub output_state_tree_index: u8, + pub system_accounts_offset: u8, + } + + let mut instruction_data = instruction_data; + let instruction_data = TestInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + + // Get accounts + let fee_payer = &accounts[0]; + let pda_account = &accounts[1]; + let rent_recipient = &accounts[2]; + + // Set up CPI accounts + let cpi_accounts = CpiAccounts::new_with_config( + fee_payer, + &accounts[instruction_data.system_accounts_offset as usize..], + CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + ); + + // Get the address tree pubkey + let address_tree_pubkey = instruction_data + .address_tree_info + .get_tree_pubkey(&cpi_accounts)?; + + // This can happen offchain too! + let (address, address_seed) = derive_address( + &[pda_account.key.as_ref()], + &address_tree_pubkey, + &crate::ID, + ); + + // Create new address params + let new_address_params = instruction_data + .address_tree_info + .into_new_address_params_packed(address_seed); + + // Compress the PDA - this handles everything internally + compress_pda_new::( + pda_account, + address, + new_address_params, + instruction_data.output_state_tree_index, + instruction_data.proof, + cpi_accounts, + &crate::ID, + rent_recipient, + )?; + + msg!("PDA compressed successfully into new compressed account"); + Ok(()) + } +} diff --git a/program-tests/sdk-test/src/sdk/mod.rs b/program-tests/sdk-test/src/sdk/mod.rs index 42844b86ea..4c94591dd8 100644 --- a/program-tests/sdk-test/src/sdk/mod.rs +++ b/program-tests/sdk-test/src/sdk/mod.rs @@ -1,2 +1,3 @@ pub mod compress_pda; +pub mod compress_pda_new; pub mod decompress_idempotent; From adfcf9f108c63934d574d295d54996644343696e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 00:31:54 -0400 Subject: [PATCH 10/16] native program with decompress done --- ...ss_from_pda.rs => compress_dynamic_pda.rs} | 4 +- ..._from_pda_new.rs => create_dynamic_pda.rs} | 53 +++++++++---------- ...ss_to_pda.rs => decompress_dynamic_pda.rs} | 4 +- program-tests/sdk-test/src/lib.rs | 12 ++--- .../sdk-test/src/sdk/compress_pda_new.rs | 23 +++++++- .../sdk-test/src/sdk/decompress_idempotent.rs | 2 +- 6 files changed, 58 insertions(+), 40 deletions(-) rename program-tests/sdk-test/src/{compress_from_pda.rs => compress_dynamic_pda.rs} (94%) rename program-tests/sdk-test/src/{compress_from_pda_new.rs => create_dynamic_pda.rs} (50%) rename program-tests/sdk-test/src/{decompress_to_pda.rs => decompress_dynamic_pda.rs} (98%) diff --git a/program-tests/sdk-test/src/compress_from_pda.rs b/program-tests/sdk-test/src/compress_dynamic_pda.rs similarity index 94% rename from program-tests/sdk-test/src/compress_from_pda.rs rename to program-tests/sdk-test/src/compress_dynamic_pda.rs index dd33315922..c5aa5cc422 100644 --- a/program-tests/sdk-test/src/compress_from_pda.rs +++ b/program-tests/sdk-test/src/compress_dynamic_pda.rs @@ -7,13 +7,13 @@ use light_sdk::{ use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; -use crate::{decompress_to_pda::MyPdaAccount, sdk::compress_pda::compress_pda}; +use crate::{decompress_dynamic_pda::MyPdaAccount, sdk::compress_pda::compress_pda}; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed /// pda check missing yet. // TODO: add macro that create the full instruction. and takes: programid, account and seeds, rent_recipient (to hardcode). low code solution. -pub fn compress_from_pda( +pub fn compress_dynamic_pda( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { diff --git a/program-tests/sdk-test/src/compress_from_pda_new.rs b/program-tests/sdk-test/src/create_dynamic_pda.rs similarity index 50% rename from program-tests/sdk-test/src/compress_from_pda_new.rs rename to program-tests/sdk-test/src/create_dynamic_pda.rs index baf5f351a4..eb45796fb7 100644 --- a/program-tests/sdk-test/src/compress_from_pda_new.rs +++ b/program-tests/sdk-test/src/create_dynamic_pda.rs @@ -1,74 +1,71 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use light_macros::pubkey; use light_sdk::{ - address::v1::derive_address, cpi::CpiAccounts, error::LightSdkError, instruction::{PackedAddressTreeInfo, ValidityProof}, }; use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; -use crate::{decompress_to_pda::MyPdaAccount, sdk::compress_pda_new::compress_pda_new}; +use crate::{decompress_dynamic_pda::MyPdaAccount, sdk::compress_pda_new::compress_pda_new}; -/// Compresses a PDA into a new compressed account -/// This creates a new compressed account with address derived from the PDA address -pub fn compress_from_pda_new( +pub const ADDRESS_SPACE: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +/// INITS a PDA and compresses it into a new compressed account. +pub fn create_dynamic_pda( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { let mut instruction_data = instruction_data; - let instruction_data = CompressFromPdaNewInstructionData::deserialize(&mut instruction_data) + let instruction_data = CreateDynamicPdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; let fee_payer = &accounts[0]; + + // UNCHECKED: ...caller program checks this. let pda_account = &accounts[1]; - let rent_recipient = &accounts[2]; // can be hardcoded by caller program + + // CHECK: hardcoded rent recipient. + let rent_recipient = &accounts[2]; + if rent_recipient.key != &RENT_RECIPIENT { + return Err(LightSdkError::ConstraintViolation); + } // Cpi accounts let cpi_accounts_struct = CpiAccounts::new_with_config( fee_payer, - &accounts[instruction_data.system_accounts_offset as usize..], + &accounts[3..], CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), ); - // Get the address tree pubkey - let address_tree_pubkey = instruction_data - .address_tree_info - .get_tree_pubkey(&cpi_accounts_struct)?; - - // TODO: consider ENFORCING on our end that the cPDA is derived from the pda. - // this would simplify. - // Can do offchain! - let (address, address_seed) = derive_address( - &[pda_account.key.as_ref()], - &address_tree_pubkey, - &crate::ID, - ); - - // Can do offchain! + // the onchain PDA is the seed for the cPDA. this way devs don't have to + // change their onchain PDA checks. let new_address_params = instruction_data .address_tree_info - .into_new_address_params_packed(address_seed); + .into_new_address_params_packed(pda_account.key.to_bytes()); - // Compress the PDA compress_pda_new::( pda_account, - address, + instruction_data.compressed_address, new_address_params, instruction_data.output_state_tree_index, instruction_data.proof, cpi_accounts_struct, &crate::ID, rent_recipient, + &ADDRESS_SPACE, // TODO: consider passing a slice of pubkeys, and extend to read_only_address_proofs. )?; Ok(()) } #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct CompressFromPdaNewInstructionData { +pub struct CreateDynamicPdaInstructionData { pub proof: ValidityProof, + pub compressed_address: [u8; 32], pub address_tree_info: PackedAddressTreeInfo, pub output_state_tree_index: u8, - pub system_accounts_offset: u8, } diff --git a/program-tests/sdk-test/src/decompress_to_pda.rs b/program-tests/sdk-test/src/decompress_dynamic_pda.rs similarity index 98% rename from program-tests/sdk-test/src/decompress_to_pda.rs rename to program-tests/sdk-test/src/decompress_dynamic_pda.rs index 8dbc5e9dd2..ff641a41ba 100644 --- a/program-tests/sdk-test/src/decompress_to_pda.rs +++ b/program-tests/sdk-test/src/decompress_dynamic_pda.rs @@ -13,7 +13,7 @@ use crate::sdk::decompress_idempotent::decompress_idempotent; pub const SLOTS_UNTIL_COMPRESSION: u64 = 10_000; /// Decompresses a compressed account into a PDA idempotently. -pub fn decompress_to_pda( +pub fn decompress_dynamic_pda( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { @@ -106,7 +106,7 @@ impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { } /// Example: Decompresses multiple compressed accounts into PDAs in a single transaction. -pub fn decompress_multiple_to_pda( +pub fn decompress_multiple_dynamic_pdas( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index a430739aa0..98bdc1cf4a 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -4,10 +4,10 @@ use solana_program::{ account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, }; -pub mod compress_from_pda; -pub mod compress_from_pda_new; +pub mod compress_dynamic_pda; +pub mod create_dynamic_pda; pub mod create_pda; -pub mod decompress_to_pda; +pub mod decompress_dynamic_pda; pub mod sdk; pub mod update_pda; @@ -56,13 +56,13 @@ pub fn process_instruction( update_pda::update_pda::(accounts, &instruction_data[1..]) } InstructionType::DecompressToPda => { - decompress_to_pda::decompress_to_pda(accounts, &instruction_data[1..]) + decompress_dynamic_pda::decompress_dynamic_pda(accounts, &instruction_data[1..]) } InstructionType::CompressFromPda => { - compress_from_pda::compress_from_pda(accounts, &instruction_data[1..]) + compress_dynamic_pda::compress_dynamic_pda(accounts, &instruction_data[1..]) } InstructionType::CompressFromPdaNew => { - compress_from_pda_new::compress_from_pda_new(accounts, &instruction_data[1..]) + create_dynamic_pda::create_dynamic_pda(accounts, &instruction_data[1..]) } }?; Ok(()) diff --git a/program-tests/sdk-test/src/sdk/compress_pda_new.rs b/program-tests/sdk-test/src/sdk/compress_pda_new.rs index 50a8e3c263..a23e50f392 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda_new.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda_new.rs @@ -6,6 +6,7 @@ use light_sdk::{ cpi::{CpiAccounts, CpiInputs}, error::LightSdkError, instruction::ValidityProof, + light_account_checks::AccountInfoTrait, LightDiscriminator, }; use solana_program::{ @@ -29,6 +30,7 @@ use crate::sdk::compress_pda::PdaTimingData; /// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the compressed account /// * `rent_recipient` - The account to receive the PDA's rent +/// * `expected_address_space` - Optional expected address space pubkey to validate against /// /// # Returns /// * `Ok(())` if the PDA was compressed successfully @@ -42,6 +44,7 @@ pub fn compress_pda_new<'info, A>( cpi_accounts: CpiAccounts<'_, 'info>, owner_program: &Pubkey, rent_recipient: &AccountInfo<'info>, + expected_address_space: &Pubkey, ) -> Result<(), LightSdkError> where A: DataHasher @@ -61,6 +64,7 @@ where cpi_accounts, owner_program, rent_recipient, + expected_address_space, ) } @@ -77,6 +81,7 @@ where /// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the compressed accounts /// * `rent_recipient` - The account to receive the PDAs' rent +/// * `expected_address_space` - Optional expected address space pubkey to validate against /// /// # Returns /// * `Ok(())` if all PDAs were compressed successfully @@ -90,6 +95,7 @@ pub fn compress_multiple_pdas_new<'info, A>( cpi_accounts: CpiAccounts<'_, 'info>, owner_program: &Pubkey, rent_recipient: &AccountInfo<'info>, + expected_address_space: &Pubkey, ) -> Result<(), LightSdkError> where A: DataHasher @@ -107,6 +113,20 @@ where return Err(LightSdkError::ConstraintViolation); } + // CHECK: address space. + for params in &new_address_params { + let address_tree_account = cpi_accounts + .get_tree_account_info(params.address_merkle_tree_account_index as usize)?; + if address_tree_account.pubkey() != *expected_address_space { + msg!( + "Invalid address space. Expected: {}. Found: {}.", + expected_address_space, + address_tree_account.pubkey() + ); + return Err(LightSdkError::ConstraintViolation); + } + } + let current_slot = Clock::get()?.slot; let mut total_lamports = 0u64; let mut compressed_account_infos = Vec::new(); @@ -186,7 +206,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::decompress_to_pda::MyPdaAccount; + use crate::decompress_dynamic_pda::MyPdaAccount; use light_sdk::cpi::CpiAccountsConfig; use light_sdk::instruction::PackedAddressTreeInfo; @@ -248,6 +268,7 @@ mod tests { cpi_accounts, &crate::ID, rent_recipient, + &crate::create_dynamic_pda::ADDRESS_SPACE, )?; msg!("PDA compressed successfully into new compressed account"); diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs index 948dd69bf9..3a26aae7bc 100644 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -216,7 +216,7 @@ pub struct DecompressMyCompressedAccount { #[cfg(test)] mod tests { use super::*; - use crate::decompress_to_pda::MyPdaAccount; + use crate::decompress_dynamic_pda::MyPdaAccount; use light_sdk::cpi::CpiAccountsConfig; /// Test instruction that demonstrates idempotent decompression From 43e87ca0ee4625e6a319ef36f326e59597877c1b Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 00:52:29 -0400 Subject: [PATCH 11/16] compress_dynamic, decompress_dynamic --- .../sdk-test/src/compress_dynamic_pda.rs | 17 +++--- .../sdk-test/src/decompress_dynamic_pda.rs | 11 ---- .../sdk-test/src/sdk/compress_pda.rs | 60 ++++--------------- .../sdk-test/src/sdk/decompress_idempotent.rs | 26 +++----- 4 files changed, 28 insertions(+), 86 deletions(-) diff --git a/program-tests/sdk-test/src/compress_dynamic_pda.rs b/program-tests/sdk-test/src/compress_dynamic_pda.rs index c5aa5cc422..bf83af88e8 100644 --- a/program-tests/sdk-test/src/compress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/compress_dynamic_pda.rs @@ -7,11 +7,13 @@ use light_sdk::{ use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; -use crate::{decompress_dynamic_pda::MyPdaAccount, sdk::compress_pda::compress_pda}; +use crate::{ + create_dynamic_pda::RENT_RECIPIENT, decompress_dynamic_pda::MyPdaAccount, + sdk::compress_pda::compress_pda, +}; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed -/// pda check missing yet. // TODO: add macro that create the full instruction. and takes: programid, account and seeds, rent_recipient (to hardcode). low code solution. pub fn compress_dynamic_pda( accounts: &[AccountInfo], @@ -21,11 +23,13 @@ pub fn compress_dynamic_pda( let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; - // Custom seeds for PDA derivation (must match decompress_idempotent) - let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; - let pda_account = &accounts[1]; - let rent_recipient = &accounts[2]; // can be hardcoded by caller program + + // CHECK: hardcoded rent recipient. + let rent_recipient = &accounts[2]; + if rent_recipient.key != &RENT_RECIPIENT { + return Err(LightSdkError::ConstraintViolation); + } // Cpi accounts let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); @@ -42,7 +46,6 @@ pub fn compress_dynamic_pda( cpi_accounts_struct, &crate::ID, rent_recipient, - &custom_seeds, )?; // any other program logic here... diff --git a/program-tests/sdk-test/src/decompress_dynamic_pda.rs b/program-tests/sdk-test/src/decompress_dynamic_pda.rs index ff641a41ba..f5e69162f2 100644 --- a/program-tests/sdk-test/src/decompress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/decompress_dynamic_pda.rs @@ -40,10 +40,6 @@ pub fn decompress_dynamic_pda( instruction_data.compressed_account.data, )?; - // Custom seeds for PDA derivation - // Caller program should provide the seeds used for their onchain PDA. - let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; - // Call the SDK function to decompress idempotently // this inits pda_account if not already initialized decompress_idempotent::( @@ -54,7 +50,6 @@ pub fn decompress_dynamic_pda( &crate::ID, rent_payer, system_program, - &custom_seeds, )?; // do something with pda_account... @@ -142,12 +137,8 @@ pub fn decompress_multiple_dynamic_pdas( CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), ); - // Custom seeds for PDA derivation (same for all accounts in this example) - let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; - // Build inputs for batch decompression let mut compressed_accounts = Vec::new(); - let mut seeds_list = Vec::new(); let mut pda_account_refs = Vec::new(); for (i, compressed_account_data) in instruction_data.compressed_accounts.into_iter().enumerate() @@ -159,7 +150,6 @@ pub fn decompress_multiple_dynamic_pdas( )?; compressed_accounts.push(compressed_account); - seeds_list.push(custom_seeds.clone()); pda_account_refs.push(&pda_accounts[i]); } @@ -167,7 +157,6 @@ pub fn decompress_multiple_dynamic_pdas( decompress_multiple_idempotent::( &pda_account_refs, compressed_accounts, - &seeds_list, instruction_data.proof, cpi_accounts, &crate::ID, diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs index 1c5a9702f7..371ea98458 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda.rs @@ -19,41 +19,6 @@ pub trait PdaTimingData { fn set_last_written_slot(&mut self, slot: u64); } -const DECOMP_SEED: &[u8] = b"decomp"; - -/// Check that the PDA account is owned by the caller program and derived from the correct seeds. -/// -/// # Arguments -/// * `custom_seeds` - Custom seeds to check against -/// * `c_pda_address` - The address of the compressed PDA -/// * `pda_account` - The address of the PDA account -/// * `caller_program` - The program that owns the PDA. -pub fn check_pda( - custom_seeds: &[&[u8]], - c_pda_address: &[u8; 32], - pda_account: &Pubkey, - caller_program: &Pubkey, -) -> Result<(), ProgramError> { - // Create seeds array: [custom_seeds..., c_pda_address, "decomp"] - let mut seeds: Vec<&[u8]> = custom_seeds.to_vec(); - seeds.push(c_pda_address); - seeds.push(DECOMP_SEED); - - let derived_pda = - Pubkey::create_program_address(&seeds, caller_program).expect("Invalid PDA seeds."); - - if derived_pda != *pda_account { - msg!( - "Invalid PDA provided. Expected: {}. Found: {}.", - derived_pda, - pda_account - ); - return Err(ProgramError::InvalidArgument); - } - - Ok(()) -} - /// Helper function to compress a PDA and reclaim rent. /// /// 1. closes onchain PDA @@ -67,12 +32,8 @@ pub fn check_pda( /// * `pda_account` - The PDA account to compress (will be closed) /// * `compressed_account_meta` - Metadata for the compressed account (must be /// empty but have an address) -/// * `proof` - Optional validity proof -/// * `cpi_accounts` - Accounts needed for CPI starting from -/// system_accounts_offset -/// * `system_accounts_offset` - Offset where CPI accounts start -/// * `fee_payer` - The fee payer account -/// * `cpi_signer` - The CPI signer for the calling program +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the compressed account /// * `rent_recipient` - The account to receive the PDA's rent // @@ -86,7 +47,6 @@ pub fn compress_pda( cpi_accounts: CpiAccounts, owner_program: &Pubkey, rent_recipient: &AccountInfo, - custom_seeds: &[&[u8]], ) -> Result<(), LightSdkError> where A: DataHasher @@ -96,13 +56,15 @@ where + Default + PdaTimingData, { - // Check that the PDA account is owned by the caller program and derived from the address of the compressed PDA. - check_pda( - custom_seeds, - &compressed_account_meta.address, - pda_account.key, - owner_program, - )?; + // Check that the PDA account is owned by the caller program + if pda_account.owner != owner_program { + msg!( + "Invalid PDA owner. Expected: {}. Found: {}.", + owner_program, + pda_account.owner + ); + return Err(LightSdkError::ConstraintViolation); + } let current_slot = Clock::get()?.slot; diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs index 3a26aae7bc..a2add4a480 100644 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs @@ -29,8 +29,6 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// * `owner_program` - The program that will own the PDA /// * `rent_payer` - The account to pay for PDA rent /// * `system_program` - The system program -/// * `custom_seeds` - Custom seeds for PDA derivation (without the compressed address) -/// * `additional_seed` - Additional seed for PDA derivation /// /// # Returns /// * `Ok(())` if the compressed account was decompressed successfully or PDA already exists @@ -43,7 +41,6 @@ pub fn decompress_idempotent<'info, A>( owner_program: &Pubkey, rent_payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, - custom_seeds: &[&[u8]], ) -> Result<(), LightSdkError> where A: DataHasher @@ -57,7 +54,6 @@ where decompress_multiple_idempotent( &[pda_account], vec![compressed_account], - &[custom_seeds.to_vec()], proof, cpi_accounts, owner_program, @@ -72,7 +68,8 @@ where /// and it will only decompress them once. If a PDA already exists and is initialized, it skips that account. /// /// # Arguments -/// * `decompress_inputs` - Vector of tuples containing (pda_account, compressed_account, custom_seeds, additional_seed) +/// * `pda_accounts` - The PDA accounts to decompress into +/// * `compressed_accounts` - The compressed accounts to decompress /// * `proof` - Single validity proof for all accounts /// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the PDAs @@ -85,7 +82,6 @@ where pub fn decompress_multiple_idempotent<'info, A>( pda_accounts: &[&AccountInfo<'info>], compressed_accounts: Vec>, - custom_seeds_list: &[Vec<&[u8]>], proof: ValidityProof, cpi_accounts: CpiAccounts<'_, 'info>, owner_program: &Pubkey, @@ -113,11 +109,8 @@ where // Collect compressed accounts for CPI let mut compressed_accounts_for_cpi = Vec::new(); - for ((pda_account, mut compressed_account), custom_seeds) in pda_accounts - .iter() - .zip(compressed_accounts.into_iter()) - .zip(custom_seeds_list.iter()) - .map(|((pda, ca), seeds)| ((pda, ca), seeds.clone())) + for (pda_account, mut compressed_account) in + pda_accounts.iter().zip(compressed_accounts.into_iter()) { // Check if PDA is already initialized if pda_account.data_len() > 0 { @@ -128,14 +121,13 @@ where continue; } - // Get compressed address + // Get the compressed account address let compressed_address = compressed_account .address() .ok_or(LightSdkError::ConstraintViolation)?; - // Derive onchain PDA - let mut seeds: Vec<&[u8]> = custom_seeds; - seeds.push(&compressed_address); + // Derive onchain PDA using the compressed address as seed + let seeds: Vec<&[u8]> = vec![&compressed_address]; let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); @@ -270,9 +262,6 @@ mod tests { account_data, )?; - // Custom seeds - let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"]; - // Call decompress_idempotent - this should work whether PDA exists or not decompress_idempotent::( pda_account, @@ -282,7 +271,6 @@ mod tests { &crate::ID, rent_payer, system_program, - &custom_seeds, )?; msg!("Idempotent decompression completed successfully"); From f34254e48cb9e90737c3b923268e9271cddf9e7a Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 01:20:58 -0400 Subject: [PATCH 12/16] wip --- program-tests/sdk-test/src/compress_dynamic_pda.rs | 4 ++-- .../sdk-test/src/decompress_dynamic_pda.rs | 13 ++++++++----- program-tests/sdk-test/src/sdk/compress_pda.rs | 12 ++++++------ program-tests/sdk-test/src/sdk/compress_pda_new.rs | 8 ++++---- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/program-tests/sdk-test/src/compress_dynamic_pda.rs b/program-tests/sdk-test/src/compress_dynamic_pda.rs index bf83af88e8..71453119b8 100644 --- a/program-tests/sdk-test/src/compress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/compress_dynamic_pda.rs @@ -33,7 +33,7 @@ pub fn compress_dynamic_pda( // Cpi accounts let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - let cpi_accounts_struct = CpiAccounts::new_with_config( + let cpi_accounts = CpiAccounts::new_with_config( &accounts[0], &accounts[instruction_data.system_accounts_offset as usize..], config, @@ -43,7 +43,7 @@ pub fn compress_dynamic_pda( pda_account, &instruction_data.compressed_account_meta, instruction_data.proof, - cpi_accounts_struct, + cpi_accounts, &crate::ID, rent_recipient, )?; diff --git a/program-tests/sdk-test/src/decompress_dynamic_pda.rs b/program-tests/sdk-test/src/decompress_dynamic_pda.rs index f5e69162f2..0e89bb4e44 100644 --- a/program-tests/sdk-test/src/decompress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/decompress_dynamic_pda.rs @@ -61,7 +61,6 @@ pub fn decompress_dynamic_pda( pub struct DecompressToPdaInstructionData { pub proof: ValidityProof, pub compressed_account: MyCompressedAccount, - pub additional_seed: [u8; 32], // ... some seed pub system_accounts_offset: u8, } @@ -79,7 +78,8 @@ pub struct MyCompressedAccount { pub struct MyPdaAccount { /// Slot when this account was last written pub last_written_slot: u64, - /// Number of slots after last_written_slot until this account can be compressed again + /// Number of slots after last_written_slot until this account can be + /// compressed again pub slots_until_compression: u64, /// The actual account data pub data: [u8; 31], @@ -87,11 +87,11 @@ pub struct MyPdaAccount { // We require this trait to be implemented for the custom PDA account. impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { - fn last_touched_slot(&self) -> u64 { + fn last_written_slot(&self) -> u64 { self.last_written_slot } - fn slots_buffer(&self) -> u64 { + fn slots_until_compression(&self) -> u64 { self.slots_until_compression } @@ -100,7 +100,7 @@ impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { } } -/// Example: Decompresses multiple compressed accounts into PDAs in a single transaction. +// TODO: do this properly. pub fn decompress_multiple_dynamic_pdas( accounts: &[AccountInfo], instruction_data: &[u8], @@ -131,6 +131,9 @@ pub fn decompress_multiple_dynamic_pdas( let pda_accounts = &accounts[pda_accounts_start..pda_accounts_start + num_accounts]; // Cpi accounts + // TODO: currently all cPDAs would have to have the same CPI_ACCOUNTS in the same order. + // - must support flexible CPI_ACCOUNTS eg for token accounts + // - must support flexible trees. let cpi_accounts = CpiAccounts::new_with_config( fee_payer, &accounts[instruction_data.system_accounts_offset as usize..], diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs index 371ea98458..71f82085c3 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda.rs @@ -14,8 +14,8 @@ use solana_program::{ /// Trait for PDA accounts that can be compressed pub trait PdaTimingData { - fn last_touched_slot(&self) -> u64; - fn slots_buffer(&self) -> u64; + fn last_written_slot(&self) -> u64; + fn slots_until_compression(&self) -> u64; fn set_last_written_slot(&mut self, slot: u64); } @@ -73,13 +73,13 @@ where let pda_account_data = A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; drop(pda_data); - let last_touched_slot = pda_account_data.last_touched_slot(); - let slots_buffer = pda_account_data.slots_buffer(); + let last_written_slot = pda_account_data.last_written_slot(); + let slots_until_compression = pda_account_data.slots_until_compression(); - if current_slot < last_touched_slot + slots_buffer { + if current_slot < last_written_slot + slots_until_compression { msg!( "Cannot compress yet. {} slots remaining", - (last_touched_slot + slots_buffer).saturating_sub(current_slot) + (last_written_slot + slots_until_compression).saturating_sub(current_slot) ); return Err(LightSdkError::ConstraintViolation); } diff --git a/program-tests/sdk-test/src/sdk/compress_pda_new.rs b/program-tests/sdk-test/src/sdk/compress_pda_new.rs index a23e50f392..fe5cc77172 100644 --- a/program-tests/sdk-test/src/sdk/compress_pda_new.rs +++ b/program-tests/sdk-test/src/sdk/compress_pda_new.rs @@ -153,14 +153,14 @@ where A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; drop(pda_data); - let last_touched_slot = pda_account_data.last_touched_slot(); - let slots_buffer = pda_account_data.slots_buffer(); + let last_written_slot = pda_account_data.last_written_slot(); + let slots_until_compression = pda_account_data.slots_until_compression(); - if current_slot < last_touched_slot + slots_buffer { + if current_slot < last_written_slot + slots_until_compression { msg!( "Cannot compress {} yet. {} slots remaining", pda_account.key, - (last_touched_slot + slots_buffer).saturating_sub(current_slot) + (last_written_slot + slots_until_compression).saturating_sub(current_slot) ); return Err(LightSdkError::ConstraintViolation); } From 7679d8be1b3dd1bb645df016f5484e92ad498e75 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 01:53:39 -0400 Subject: [PATCH 13/16] adding anchor --- Cargo.lock | 1 + .../anchor-compressible-user/Anchor.toml | 19 + .../anchor-compressible-user/README.md | 114 +++++ .../anchor-compressible-user/package.json | 19 + .../anchor-compressible-user/Cargo.toml | 30 ++ .../anchor-compressible-user/Xargo.toml | 2 + .../anchor-compressible-user/src/lib.rs | 409 ++++++++++++++++++ .../anchor-compressible-user/tests/test.rs | 291 +++++++++++++ .../anchor-compressible-user/tsconfig.json | 10 + sdk-libs/sdk/Cargo.toml | 1 + sdk-libs/sdk/src/compressible/compress_pda.rs | 118 +++++ .../sdk/src/compressible/compress_pda_new.rs | 211 +++++++++ .../src/compressible/decompress_idempotent.rs | 198 +++++++++ sdk-libs/sdk/src/compressible/mod.rs | 9 + sdk-libs/sdk/src/lib.rs | 2 + 15 files changed, 1434 insertions(+) create mode 100644 program-tests/anchor-compressible-user/Anchor.toml create mode 100644 program-tests/anchor-compressible-user/README.md create mode 100644 program-tests/anchor-compressible-user/package.json create mode 100644 program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml create mode 100644 program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml create mode 100644 program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs create mode 100644 program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs create mode 100644 program-tests/anchor-compressible-user/tsconfig.json create mode 100644 sdk-libs/sdk/src/compressible/compress_pda.rs create mode 100644 sdk-libs/sdk/src/compressible/compress_pda_new.rs create mode 100644 sdk-libs/sdk/src/compressible/decompress_idempotent.rs create mode 100644 sdk-libs/sdk/src/compressible/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 03057b4669..c8e981fdf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3629,6 +3629,7 @@ dependencies = [ "solana-msg", "solana-program-error", "solana-pubkey", + "solana-system-interface", "thiserror 2.0.12", ] diff --git a/program-tests/anchor-compressible-user/Anchor.toml b/program-tests/anchor-compressible-user/Anchor.toml new file mode 100644 index 0000000000..cd3f9ab2ed --- /dev/null +++ b/program-tests/anchor-compressible-user/Anchor.toml @@ -0,0 +1,19 @@ +[features] +resolution = true +skip-lint = false + +[programs.localnet] +anchor_compressible_user = "CompUser11111111111111111111111111111111111" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + +[test] +startup_wait = 5000 \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/README.md b/program-tests/anchor-compressible-user/README.md new file mode 100644 index 0000000000..c1f1617fb5 --- /dev/null +++ b/program-tests/anchor-compressible-user/README.md @@ -0,0 +1,114 @@ +# Anchor Compressible User Records + +A comprehensive example demonstrating how to use Light Protocol's compressible SDK with Anchor framework, including the SDK helper functions for compressing and decompressing PDAs. + +## Overview + +This program demonstrates: + +- Creating compressed user records based on the signer's public key +- Using Anchor's account constraints with compressed accounts +- Updating compressed records +- Decompressing records to regular PDAs using SDK helpers +- Compressing PDAs back to compressed accounts using SDK helpers +- Using the `PdaTimingData` trait for time-based compression controls + +## Key Features + +### 1. **Deterministic Addressing** + +User records are created at deterministic addresses derived from: + +```rust +seeds = [b"user_record", user_pubkey] +``` + +This ensures each user can only have one record and it can be found without scanning. + +### 2. **Compressed Account Structure with Timing** + +```rust +#[event] +#[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] +pub struct UserRecord { + #[hash] + pub owner: Pubkey, // The user who owns this record + pub name: String, // User's display name + pub bio: String, // User's bio + pub score: i64, // Some score/reputation + pub created_at: i64, // Creation timestamp + pub updated_at: i64, // Last update timestamp + // PDA timing data for compression/decompression + pub last_written_slot: u64, + pub slots_until_compression: u64, +} +``` + +### 3. **Five Main Instructions** + +#### Create User Record + +- Creates a new compressed account for the user +- Uses the user's pubkey as a seed for deterministic addressing +- Initializes with name, bio, timestamps, and timing data + +#### Update User Record + +- Updates an existing compressed user record +- Verifies ownership before allowing updates +- Can update name, bio, or increment/decrement score +- Updates the last_written_slot for timing controls + +#### Decompress User Record + +- Uses `decompress_idempotent` SDK helper +- Converts a compressed account to a regular on-chain PDA +- Idempotent - can be called multiple times safely +- Preserves all data during decompression + +#### Compress User Record PDA + +- Uses `compress_pda` SDK helper +- Compresses an existing PDA back to a compressed account +- Requires the compressed account to already exist +- Enforces timing constraints (slots_until_compression) + +#### Compress User Record PDA New + +- Uses `compress_pda_new` SDK helper +- Compresses a PDA into a new compressed account with a specific address +- Creates the compressed account and closes the PDA in one operation +- Also enforces timing constraints + +## Integration with Light SDK Helpers + +The program uses Light SDK's PDA helper functions: + +1. **`decompress_idempotent`**: Safely decompresses accounts, handling the case where the PDA might already exist +2. **`compress_pda`**: Compresses an existing PDA into an existing compressed account +3. **`compress_pda_new`**: Compresses a PDA into a new compressed account with a derived address +4. **`PdaTimingData` trait**: Implements timing controls for when PDAs can be compressed + +## PDA Timing Controls + +The program implements the `PdaTimingData` trait to control when PDAs can be compressed: + +- `last_written_slot`: Tracks when the PDA was last modified +- `slots_until_compression`: Number of slots that must pass before compression is allowed +- This prevents immediate compression after decompression, allowing for transaction finality + +## Testing + +The test file demonstrates: + +- Creating a user record +- Updating the record +- Decompressing to a regular PDA +- Compressing back to a compressed account + +Run tests with: + +```bash +cd program-tests/anchor-compressible-user +cargo test-sbf +``` diff --git a/program-tests/anchor-compressible-user/package.json b/program-tests/anchor-compressible-user/package.json new file mode 100644 index 0000000000..ca054f09d4 --- /dev/null +++ b/program-tests/anchor-compressible-user/package.json @@ -0,0 +1,19 @@ +{ + "name": "anchor-compressible-user", + "version": "1.0.0", + "description": "Anchor program demonstrating compressible accounts", + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0" + }, + "devDependencies": { + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml new file mode 100644 index 0000000000..65e49cdbc6 --- /dev/null +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "anchor-compressible-user" +version = "0.1.0" +description = "Simple Anchor program demonstrating compressible accounts with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "anchor_compressible_user" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { workspace = true } +light-sdk = { workspace = true } +light-sdk-types = { workspace = true } +light-hasher = { workspace = true } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"] } +borsh = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +tokio = { workspace = true } +solana-sdk = { workspace = true } \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml new file mode 100644 index 0000000000..9e7d95be7f --- /dev/null +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs new file mode 100644 index 0000000000..625f53f904 --- /dev/null +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs @@ -0,0 +1,409 @@ +#![allow(unexpected_cfgs)] + +use anchor_lang::{prelude::*, Discriminator}; +use light_sdk::{ + account::LightAccount, + address::v1::derive_address, + cpi::{CpiAccounts, CpiInputs, CpiSigner}, + derive_light_cpi_signer, + instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof}, + pda::{compress_pda, compress_pda_new, decompress_idempotent, PdaTimingData}, + LightDiscriminator, LightHasher, +}; + +declare_id!("CompUser11111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("CompUser11111111111111111111111111111111111"); + +#[program] +pub mod anchor_compressible_user { + use super::*; + + /// Creates a new compressed user record based on the signer's pubkey + pub fn create_user_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecord<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_tree_index: u8, + name: String, + bio: String, + ) -> Result<()> { + let user = ctx.accounts.user.key(); + + // Derive address using user's pubkey as seed + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.user.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + let (address, address_seed) = derive_address( + &[b"user_record", user.as_ref()], + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, + &crate::ID, + ); + + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); + + // Create the compressed user record + let mut user_record = + LightAccount::<'_, UserRecord>::new_init(&crate::ID, Some(address), output_tree_index); + + user_record.owner = user; + user_record.name = name; + user_record.bio = bio; + user_record.score = 0; + user_record.created_at = Clock::get()?.unix_timestamp; + user_record.updated_at = Clock::get()?.unix_timestamp; + user_record.last_written_slot = Clock::get()?.slot; + user_record.slots_until_compression = 100; // Can be compressed after 100 slots + + let cpi_inputs = CpiInputs::new_with_address( + proof, + vec![user_record.to_account_info().map_err(ProgramError::from)?], + vec![new_address_params], + ); + + cpi_inputs + .invoke_light_system_program(light_cpi_accounts) + .map_err(ProgramError::from)?; + + emit!(UserRecordCreated { + user, + address, + name: user_record.name.clone(), + }); + + Ok(()) + } + + /// Updates an existing compressed user record + pub fn update_user_record<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateUserRecord<'info>>, + proof: ValidityProof, + account_meta: CompressedAccountMeta, + current_record: UserRecord, + new_name: Option, + new_bio: Option, + score_delta: Option, + ) -> Result<()> { + let user = ctx.accounts.user.key(); + + // Verify ownership + require!(current_record.owner == user, ErrorCode::ConstraintOwner); + + let mut user_record = + LightAccount::<'_, UserRecord>::new_mut(&crate::ID, &account_meta, current_record) + .map_err(ProgramError::from)?; + + // Update fields if provided + if let Some(name) = new_name { + user_record.name = name; + } + if let Some(bio) = new_bio { + user_record.bio = bio; + } + if let Some(delta) = score_delta { + user_record.score = user_record.score.saturating_add_signed(delta); + } + user_record.updated_at = Clock::get()?.unix_timestamp; + user_record.last_written_slot = Clock::get()?.slot; + + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.user.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + let cpi_inputs = CpiInputs::new( + proof, + vec![user_record.to_account_info().map_err(ProgramError::from)?], + ); + + cpi_inputs + .invoke_light_system_program(light_cpi_accounts) + .map_err(ProgramError::from)?; + + emit!(UserRecordUpdated { + user, + new_score: user_record.score, + updated_at: user_record.updated_at, + }); + + Ok(()) + } + + /// Decompresses a user record to a regular PDA (for compatibility or migration) + pub fn decompress_user_record<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressUserRecord<'info>>, + proof: ValidityProof, + account_meta: CompressedAccountMeta, + compressed_record: UserRecord, + ) -> Result<()> { + let user = ctx.accounts.user.key(); + + // Verify ownership + require!(compressed_record.owner == user, ErrorCode::ConstraintOwner); + + let compressed_account = + LightAccount::<'_, UserRecord>::new_mut(&crate::ID, &account_meta, compressed_record) + .map_err(ProgramError::from)?; + + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.user.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Use the SDK helper for idempotent decompression + decompress_idempotent::( + &ctx.accounts.user_record_pda.to_account_info(), + compressed_account, + proof, + light_cpi_accounts, + &crate::ID, + &ctx.accounts.user.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + Clock::get()?.slot, + Rent::get()?.minimum_balance(std::mem::size_of::() + 8), + ) + .map_err(|_| error!(ErrorCode::CompressionError))?; + + emit!(UserRecordDecompressed { + user, + pda: ctx.accounts.user_record_pda.key(), + }); + + Ok(()) + } + + /// Compresses an existing PDA back to a compressed account + pub fn compress_user_record_pda<'info>( + ctx: Context<'_, '_, '_, 'info, CompressUserRecordPda<'info>>, + proof: ValidityProof, + compressed_account_meta: CompressedAccountMeta, + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Use the SDK helper to compress the PDA + compress_pda::( + &ctx.accounts.user_record_pda.to_account_info(), + &compressed_account_meta, + proof, + light_cpi_accounts, + &crate::ID, + &ctx.accounts.rent_recipient.to_account_info(), + Clock::get()?.slot, + ) + .map_err(|_| error!(ErrorCode::CompressionError))?; + + emit!(UserRecordCompressed { + user: ctx.accounts.user_record_pda.owner, + pda: ctx.accounts.user_record_pda.key(), + }); + + Ok(()) + } + + /// Compresses a PDA into a new compressed account with a specific address + pub fn compress_user_record_pda_new<'info>( + ctx: Context<'_, '_, '_, 'info, CompressUserRecordPdaNew<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Derive the address for the compressed account + let (address, address_seed) = derive_address( + &[b"user_record", ctx.accounts.user_record_pda.owner.as_ref()], + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, + &crate::ID, + ); + + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); + + // Use the SDK helper to compress the PDA into a new compressed account + compress_pda_new::( + &ctx.accounts.user_record_pda.to_account_info(), + address, + new_address_params, + output_state_tree_index, + proof, + light_cpi_accounts, + &crate::ID, + &ctx.accounts.rent_recipient.to_account_info(), + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, + Clock::get()?.slot, + ) + .map_err(|_| error!(ErrorCode::CompressionError))?; + + emit!(UserRecordCompressedNew { + user: ctx.accounts.user_record_pda.owner, + pda: ctx.accounts.user_record_pda.key(), + compressed_address: address, + }); + + Ok(()) + } +} + +/// Compressed user record that lives in the compressed state +#[event] +#[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] +pub struct UserRecord { + #[hash] + pub owner: Pubkey, + pub name: String, + pub bio: String, + pub score: i64, + pub created_at: i64, + pub updated_at: i64, + // PDA timing data for compression/decompression + pub last_written_slot: u64, + pub slots_until_compression: u64, +} + +// Implement the PdaTimingData trait for UserRecord +impl PdaTimingData for UserRecord { + fn last_written_slot(&self) -> u64 { + self.last_written_slot + } + + fn slots_until_compression(&self) -> u64 { + self.slots_until_compression + } + + fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } +} + +/// Regular on-chain PDA for decompressed records +#[account] +pub struct UserRecordPda { + pub owner: Pubkey, + pub name: String, + pub bio: String, + pub score: i64, + pub created_at: i64, + pub updated_at: i64, + // PDA timing data + pub last_written_slot: u64, + pub slots_until_compression: u64, +} + +#[derive(Accounts)] +pub struct CreateUserRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, +} + +#[derive(Accounts)] +pub struct UpdateUserRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, +} + +#[derive(Accounts)] +pub struct DecompressUserRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init_if_needed, + payer = user, + space = 8 + 32 + 4 + 64 + 4 + 256 + 8 + 8 + 8 + 8 + 8, // discriminator + owner + string lens + strings + timestamps + timing + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record_pda: Account<'info, UserRecordPda>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct CompressUserRecordPda<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user_record_pda.owner.as_ref()], + bump, + constraint = user_record_pda.owner == fee_payer.key(), + )] + pub user_record_pda: Account<'info, UserRecordPda>, + /// CHECK: Rent recipient can be any account + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressUserRecordPdaNew<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user_record_pda.owner.as_ref()], + bump, + constraint = user_record_pda.owner == fee_payer.key(), + )] + pub user_record_pda: Account<'info, UserRecordPda>, + /// CHECK: Rent recipient can be any account + #[account(mut)] + pub rent_recipient: AccountInfo<'info>, +} + +// Events +#[event] +pub struct UserRecordCreated { + pub user: Pubkey, + pub address: [u8; 32], + pub name: String, +} + +#[event] +pub struct UserRecordUpdated { + pub user: Pubkey, + pub new_score: i64, + pub updated_at: i64, +} + +#[event] +pub struct UserRecordDecompressed { + pub user: Pubkey, + pub pda: Pubkey, +} + +#[event] +pub struct UserRecordCompressed { + pub user: Pubkey, + pub pda: Pubkey, +} + +#[event] +pub struct UserRecordCompressedNew { + pub user: Pubkey, + pub pda: Pubkey, + pub compressed_address: [u8; 32], +} + +// Error codes +#[error_code] +pub enum ErrorCode { + #[msg("Compression operation failed")] + CompressionError, +} diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs new file mode 100644 index 0000000000..bc84f8b3c8 --- /dev/null +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs @@ -0,0 +1,291 @@ +#![cfg(feature = "test-sbf")] + +use anchor_compressible_user::{UserRecord, UserRecordCreated}; +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressed_account::{ + address::derive_address, compressed_account::CompressedAccountWithMerkleContext, + hashv_to_bn254_field_size_be, +}; +use light_program_test::{ + program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::instruction::{ + account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig, +}; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +#[tokio::test] +async fn test_anchor_compressible_user() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("anchor_compressible_user", anchor_compressible_user::ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Test 1: Create a user record + let user_name = "Alice".to_string(); + let user_bio = "I love compressed accounts!".to_string(); + + let address = create_user_record( + &mut rpc, + &payer, + user_name.clone(), + user_bio.clone(), + ) + .await + .unwrap(); + + // Verify the account was created + let compressed_account = rpc + .indexer() + .unwrap() + .get_compressed_account(address, None) + .await + .unwrap() + .value + .clone(); + + assert_eq!(compressed_account.address.unwrap(), address); + + // Test 2: Update the user record + let new_bio = "I REALLY love compressed accounts!".to_string(); + update_user_record( + &mut rpc, + &payer, + compressed_account.into(), + None, + Some(new_bio.clone()), + Some(100), + ) + .await + .unwrap(); + + // Test 3: Decompress the user record + let compressed_account = rpc + .indexer() + .unwrap() + .get_compressed_account(address, None) + .await + .unwrap() + .value + .clone(); + + decompress_user_record(&mut rpc, &payer, compressed_account.into()) + .await + .unwrap(); + + // Verify the PDA was created + let pda = Pubkey::find_program_address( + &[b"user_record", payer.pubkey().as_ref()], + &anchor_compressible_user::ID, + ) + .0; + + let pda_account = rpc.get_account(pda).await.unwrap(); + assert!(pda_account.is_some()); +} + +async fn create_user_record( + rpc: &mut LightProgramTest, + user: &Keypair, + name: String, + bio: String, +) -> Result<[u8; 32], RpcError> { + let address_tree_pubkey = rpc.get_address_merkle_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive the address based on user's pubkey + let address_seed = hashv_to_bn254_field_size_be(&[b"user_record", user.pubkey().as_ref()]); + let address = derive_address( + &address_seed, + &address_tree_pubkey.to_bytes(), + &anchor_compressible_user::ID.to_bytes(), + ); + + // Get validity proof + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address, + tree: address_tree_pubkey, + }], + None, + ) + .await? + .value; + + // Pack accounts + let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); + let mut accounts = PackedAccounts::default(); + accounts.add_pre_accounts_signer(user.pubkey()); + accounts.add_system_accounts(system_account_meta_config); + + let output_merkle_tree_index = accounts.insert_or_get(output_queue); + let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let (accounts, _, _) = accounts.to_account_metas(); + + // Create instruction data + let instruction_data = anchor_compressible_user::instruction::CreateUserRecord { + proof: rpc_result.proof, + address_tree_info: packed_address_tree_info, + output_tree_index: output_merkle_tree_index, + name, + bio, + }; + + let instruction = Instruction { + program_id: anchor_compressible_user::ID, + accounts, + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await?; + + Ok(address) +} + +async fn update_user_record( + rpc: &mut LightProgramTest, + user: &Keypair, + compressed_account: CompressedAccountWithMerkleContext, + new_name: Option, + new_bio: Option, + score_delta: Option, +) -> Result<(), RpcError> { + // Get validity proof + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash().unwrap()], vec![], None) + .await? + .value; + + // Pack accounts + let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); + let mut accounts = PackedAccounts::default(); + accounts.add_pre_accounts_signer(user.pubkey()); + accounts.add_system_accounts(system_account_meta_config); + + let packed_accounts = rpc_result + .pack_tree_infos(&mut accounts) + .state_trees + .unwrap(); + + let meta = CompressedAccountMeta { + tree_info: packed_accounts.packed_tree_infos[0], + address: compressed_account.compressed_account.address.unwrap(), + output_state_tree_index: packed_accounts.output_tree_index, + }; + + let (accounts, _, _) = accounts.to_account_metas(); + + // Deserialize current record + let current_record: UserRecord = UserRecord::deserialize( + &mut &compressed_account + .compressed_account + .data + .unwrap() + .data[..], + ) + .unwrap(); + + // Create instruction data + let instruction_data = anchor_compressible_user::instruction::UpdateUserRecord { + proof: rpc_result.proof, + account_meta: meta, + current_record, + new_name, + new_bio, + score_delta, + }; + + let instruction = Instruction { + program_id: anchor_compressible_user::ID, + accounts, + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await?; + + Ok(()) +} + +async fn decompress_user_record( + rpc: &mut LightProgramTest, + user: &Keypair, + compressed_account: CompressedAccountWithMerkleContext, +) -> Result<(), RpcError> { + // Get validity proof + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash().unwrap()], vec![], None) + .await? + .value; + + // Pack accounts + let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); + let mut accounts = PackedAccounts::default(); + accounts.add_pre_accounts_signer(user.pubkey()); + accounts.add_system_accounts(system_account_meta_config); + + let packed_accounts = rpc_result + .pack_tree_infos(&mut accounts) + .state_trees + .unwrap(); + + let meta = CompressedAccountMeta { + tree_info: packed_accounts.packed_tree_infos[0], + address: compressed_account.compressed_account.address.unwrap(), + output_state_tree_index: packed_accounts.output_tree_index, + }; + + // Deserialize current record + let compressed_record: UserRecord = UserRecord::deserialize( + &mut &compressed_account + .compressed_account + .data + .unwrap() + .data[..], + ) + .unwrap(); + + // Get the PDA account + let user_record_pda = Pubkey::find_program_address( + &[b"user_record", user.pubkey().as_ref()], + &anchor_compressible_user::ID, + ) + .0; + + // Create instruction accounts + let instruction_accounts = anchor_compressible_user::accounts::DecompressUserRecord { + user: user.pubkey(), + user_record_pda, + system_program: solana_sdk::system_program::ID, + }; + + let (mut accounts, _, _) = accounts.to_account_metas(); + accounts.extend_from_slice(&instruction_accounts.to_account_metas(Some(true))); + + // Create instruction data + let instruction_data = anchor_compressible_user::instruction::DecompressUserRecord { + proof: rpc_result.proof, + account_meta: meta, + compressed_record, + }; + + let instruction = Instruction { + program_id: anchor_compressible_user::ID, + accounts, + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/tsconfig.json b/program-tests/anchor-compressible-user/tsconfig.json new file mode 100644 index 0000000000..6f1d764179 --- /dev/null +++ b/program-tests/anchor-compressible-user/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} \ No newline at end of file diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 9afeb4af92..8d4deaa05e 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -28,6 +28,7 @@ solana-msg = { workspace = true } solana-cpi = { workspace = true } solana-program-error = { workspace = true } solana-instruction = { workspace = true } +solana-system-interface = { workspace = true } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } diff --git a/sdk-libs/sdk/src/compressible/compress_pda.rs b/sdk-libs/sdk/src/compressible/compress_pda.rs new file mode 100644 index 0000000000..d8501c0088 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_pda.rs @@ -0,0 +1,118 @@ +use crate::{ + account::LightAccount, + cpi::{CpiAccounts, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, + LightDiscriminator, +}; +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as BorshSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +/// Trait for PDA accounts that can be compressed +pub trait PdaTimingData { + fn last_written_slot(&self) -> u64; + fn slots_until_compression(&self) -> u64; + fn set_last_written_slot(&mut self, slot: u64); +} + +/// Helper function to compress a PDA and reclaim rent. +/// +/// 1. closes onchain PDA +/// 2. transfers PDA lamports to rent_recipient +/// 3. updates the empty compressed PDA with onchain PDA data +/// +/// This requires the compressed PDA that is tied to the onchain PDA to already +/// exist. +/// +/// # Arguments +/// * `pda_account` - The PDA account to compress (will be closed) +/// * `compressed_account_meta` - Metadata for the compressed account (must be +/// empty but have an address) +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed account +/// * `rent_recipient` - The account to receive the PDA's rent +/// * `current_slot` - The current slot for timing checks +// +// TODO: +// - check if any explicit checks required for compressed account? +// - consider multiple accounts per ix. +pub fn compress_pda( + pda_account: &AccountInfo, + compressed_account_meta: &CompressedAccountMeta, + proof: ValidityProof, + cpi_accounts: CpiAccounts, + owner_program: &Pubkey, + rent_recipient: &AccountInfo, + current_slot: u64, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData, +{ + // Check that the PDA account is owned by the caller program + if pda_account.owner != owner_program { + msg!( + "Invalid PDA owner. Expected: {}. Found: {}.", + owner_program, + pda_account.owner + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Deserialize the PDA data to check timing fields + let pda_data = pda_account.try_borrow_data()?; + let pda_account_data = A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; + drop(pda_data); + + let last_written_slot = pda_account_data.last_written_slot(); + let slots_until_compression = pda_account_data.slots_until_compression(); + + if current_slot < last_written_slot + slots_until_compression { + msg!( + "Cannot compress yet. {} slots remaining", + (last_written_slot + slots_until_compression).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Get the PDA lamports before we close it + let pda_lamports = pda_account.lamports(); + + let mut compressed_account = + LightAccount::<'_, A>::new_mut(owner_program, compressed_account_meta, A::default())?; + + compressed_account.account = pda_account_data; + + // Create CPI inputs + let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); + + // Invoke light system program to create the compressed account + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + // Close the PDA account + // 1. Transfer all lamports to the rent recipient + let dest_starting_lamports = rent_recipient.lamports(); + **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports + .checked_add(pda_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + // 2. Decrement source account lamports + **pda_account.try_borrow_mut_lamports()? = 0; + // 3. Clear all account data + pda_account.try_borrow_mut_data()?.fill(0); + // 4. Assign ownership back to the system program + pda_account.assign(&Pubkey::default()); + + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/compress_pda_new.rs b/sdk-libs/sdk/src/compressible/compress_pda_new.rs new file mode 100644 index 0000000000..9d53fee9aa --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_pda_new.rs @@ -0,0 +1,211 @@ +use crate::{ + account::LightAccount, + address::{v1::derive_address, PackedNewAddressParams}, + cpi::{CpiAccounts, CpiInputs}, + error::LightSdkError, + instruction::ValidityProof, + light_account_checks::AccountInfoTrait, + LightDiscriminator, +}; +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as BorshSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +use crate::compressible::compress_pda::PdaTimingData; + +/// Helper function to compress an onchain PDA into a new compressed account. +/// +/// This function handles the entire compression operation: creates a compressed account, +/// copies the PDA data, and closes the onchain PDA. +/// +/// # Arguments +/// * `pda_account` - The PDA account to compress (will be closed) +/// * `address` - The address for the compressed account +/// * `new_address_params` - Address parameters for the compressed account +/// * `output_state_tree_index` - Output state tree index for the compressed account +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed account +/// * `rent_recipient` - The account to receive the PDA's rent +/// * `expected_address_space` - Optional expected address space pubkey to validate against +/// * `current_slot` - The current slot for timing checks +/// +/// # Returns +/// * `Ok(())` if the PDA was compressed successfully +/// * `Err(LightSdkError)` if there was an error +pub fn compress_pda_new<'info, A>( + pda_account: &AccountInfo<'info>, + address: [u8; 32], + new_address_params: PackedNewAddressParams, + output_state_tree_index: u8, + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_recipient: &AccountInfo<'info>, + expected_address_space: &Pubkey, + current_slot: u64, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData + + Clone, +{ + compress_multiple_pdas_new::( + &[pda_account], + &[address], + vec![new_address_params], + &[output_state_tree_index], + proof, + cpi_accounts, + owner_program, + rent_recipient, + expected_address_space, + current_slot, + ) +} + +/// Helper function to compress multiple onchain PDAs into new compressed accounts. +/// +/// This function handles the entire compression operation for multiple PDAs. +/// +/// # Arguments +/// * `pda_accounts` - The PDA accounts to compress (will be closed) +/// * `addresses` - The addresses for the compressed accounts +/// * `new_address_params` - Address parameters for the compressed accounts +/// * `output_state_tree_indices` - Output state tree indices for the compressed accounts +/// * `proof` - Single validity proof for all accounts +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the compressed accounts +/// * `rent_recipient` - The account to receive the PDAs' rent +/// * `expected_address_space` - Optional expected address space pubkey to validate against +/// * `current_slot` - The current slot for timing checks +/// +/// # Returns +/// * `Ok(())` if all PDAs were compressed successfully +/// * `Err(LightSdkError)` if there was an error +pub fn compress_multiple_pdas_new<'info, A>( + pda_accounts: &[&AccountInfo<'info>], + addresses: &[[u8; 32]], + new_address_params: Vec, + output_state_tree_indices: &[u8], + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_recipient: &AccountInfo<'info>, + expected_address_space: &Pubkey, + current_slot: u64, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + PdaTimingData + + Clone, +{ + if pda_accounts.len() != addresses.len() + || pda_accounts.len() != new_address_params.len() + || pda_accounts.len() != output_state_tree_indices.len() + { + return Err(LightSdkError::ConstraintViolation); + } + + // CHECK: address space. + for params in &new_address_params { + let address_tree_account = cpi_accounts + .get_tree_account_info(params.address_merkle_tree_account_index as usize)?; + if address_tree_account.pubkey() != *expected_address_space { + msg!( + "Invalid address space. Expected: {}. Found: {}.", + expected_address_space, + address_tree_account.pubkey() + ); + return Err(LightSdkError::ConstraintViolation); + } + } + + let mut total_lamports = 0u64; + let mut compressed_account_infos = Vec::new(); + + for ((pda_account, &address), &output_state_tree_index) in pda_accounts + .iter() + .zip(addresses.iter()) + .zip(output_state_tree_indices.iter()) + { + // Check that the PDA account is owned by the caller program + if pda_account.owner != owner_program { + msg!( + "Invalid PDA owner for {}. Expected: {}. Found: {}.", + pda_account.key, + owner_program, + pda_account.owner + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Deserialize the PDA data to check timing fields + let pda_data = pda_account.try_borrow_data()?; + let pda_account_data = + A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; + drop(pda_data); + + let last_written_slot = pda_account_data.last_written_slot(); + let slots_until_compression = pda_account_data.slots_until_compression(); + + if current_slot < last_written_slot + slots_until_compression { + msg!( + "Cannot compress {} yet. {} slots remaining", + pda_account.key, + (last_written_slot + slots_until_compression).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation); + } + + // Create the compressed account with the PDA data + let mut compressed_account = + LightAccount::<'_, A>::new_init(owner_program, Some(address), output_state_tree_index); + compressed_account.account = pda_account_data; + + compressed_account_infos.push(compressed_account.to_account_info()?); + + // Accumulate lamports + total_lamports = total_lamports + .checked_add(pda_account.lamports()) + .ok_or(ProgramError::ArithmeticOverflow)?; + } + + // Create CPI inputs with all compressed accounts and new addresses + let cpi_inputs = + CpiInputs::new_with_address(proof, compressed_account_infos, new_address_params); + + // Invoke light system program to create all compressed accounts + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + // Close all PDA accounts + let dest_starting_lamports = rent_recipient.lamports(); + **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports + .checked_add(total_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + for pda_account in pda_accounts { + // Decrement source account lamports + **pda_account.try_borrow_mut_lamports()? = 0; + // Clear all account data + pda_account.try_borrow_mut_data()?.fill(0); + // Assign ownership back to the system program + pda_account.assign(&Pubkey::default()); + } + + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs new file mode 100644 index 0000000000..50070d90f9 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -0,0 +1,198 @@ +use crate::{ + account::LightAccount, + cpi::{CpiAccounts, CpiInputs}, + error::LightSdkError, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, + LightDiscriminator, +}; +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as BorshSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_system_interface::instruction as system_instruction; + +use crate::compressible::compress_pda::PdaTimingData; + +pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; + +/// Helper function to decompress a compressed account into a PDA idempotently. +/// +/// This function is idempotent, meaning it can be called multiple times with the same compressed account +/// and it will only decompress it once. If the PDA already exists and is initialized, it returns early. +/// +/// # Arguments +/// * `pda_account` - The PDA account to decompress into +/// * `compressed_account` - The compressed account to decompress +/// * `proof` - Validity proof +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the PDA +/// * `rent_payer` - The account to pay for PDA rent +/// * `system_program` - The system program +/// * `current_slot` - The current slot for timing +/// * `rent_minimum_balance` - The minimum balance required for rent exemption +/// +/// # Returns +/// * `Ok(())` if the compressed account was decompressed successfully or PDA already exists +/// * `Err(LightSdkError)` if there was an error +pub fn decompress_idempotent<'info, A>( + pda_account: &AccountInfo<'info>, + compressed_account: LightAccount<'_, A>, + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + current_slot: u64, + rent_minimum_balance: u64, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + Clone + + PdaTimingData, +{ + decompress_multiple_idempotent( + &[pda_account], + vec![compressed_account], + proof, + cpi_accounts, + owner_program, + rent_payer, + system_program, + current_slot, + rent_minimum_balance, + ) +} + +/// Helper function to decompress multiple compressed accounts into PDAs idempotently. +/// +/// This function is idempotent, meaning it can be called multiple times with the same compressed accounts +/// and it will only decompress them once. If a PDA already exists and is initialized, it skips that account. +/// +/// # Arguments +/// * `pda_accounts` - The PDA accounts to decompress into +/// * `compressed_accounts` - The compressed accounts to decompress +/// * `proof` - Single validity proof for all accounts +/// * `cpi_accounts` - Accounts needed for CPI +/// * `owner_program` - The program that will own the PDAs +/// * `rent_payer` - The account to pay for PDA rent +/// * `system_program` - The system program +/// +/// # Returns +/// * `Ok(())` if all compressed accounts were decompressed successfully or PDAs already exist +/// * `Err(LightSdkError)` if there was an error +pub fn decompress_multiple_idempotent<'info, A>( + pda_accounts: &[&AccountInfo<'info>], + compressed_accounts: Vec>, + proof: ValidityProof, + cpi_accounts: CpiAccounts<'_, 'info>, + owner_program: &Pubkey, + rent_payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + current_slot: u64, + rent_minimum_balance: u64, +) -> Result<(), LightSdkError> +where + A: DataHasher + + LightDiscriminator + + BorshSerialize + + BorshDeserialize + + Default + + Clone + + PdaTimingData, +{ + // Calculate space needed for PDA (same for all accounts of type A) + let space = std::mem::size_of::() + 8; // +8 for discriminator + + // Collect compressed accounts for CPI + let mut compressed_accounts_for_cpi = Vec::new(); + + for (pda_account, mut compressed_account) in + pda_accounts.iter().zip(compressed_accounts.into_iter()) + { + // Check if PDA is already initialized + if pda_account.data_len() > 0 { + msg!( + "PDA {} already initialized, skipping decompression", + pda_account.key + ); + continue; + } + + // Get the compressed account address + let compressed_address = compressed_account + .address() + .ok_or(LightSdkError::ConstraintViolation)?; + + // Derive onchain PDA using the compressed address as seed + let seeds: Vec<&[u8]> = vec![&compressed_address]; + + let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); + + // Verify PDA matches + if pda_pubkey != *pda_account.key { + msg!("Invalid PDA pubkey for account {}", pda_account.key); + return Err(LightSdkError::ConstraintViolation); + } + + // Create PDA account + let create_account_ix = system_instruction::create_account( + rent_payer.key, + pda_account.key, + rent_minimum_balance, + space as u64, + owner_program, + ); + + // Add bump to seeds for signing + let bump_seed = [pda_bump]; + let mut signer_seeds = seeds.clone(); + signer_seeds.push(&bump_seed); + let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect(); + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + (*pda_account).clone(), + system_program.clone(), + ], + &[&signer_seeds_refs], + )?; + + // Initialize PDA with decompressed data and update slot + let mut decompressed_pda = compressed_account.account.clone(); + decompressed_pda.set_last_written_slot(current_slot); + + // Write discriminator + let discriminator = A::LIGHT_DISCRIMINATOR; + pda_account.try_borrow_mut_data()?[..8].copy_from_slice(&discriminator); + + // Write data to PDA + decompressed_pda + .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) + .map_err(|_| LightSdkError::Borsh)?; + + // Zero the compressed account + compressed_account.account = A::default(); + + // Add to CPI batch + compressed_accounts_for_cpi.push(compressed_account.to_account_info()?); + } + + // Make single CPI call with all compressed accounts + if !compressed_accounts_for_cpi.is_empty() { + let cpi_inputs = CpiInputs::new(proof, compressed_accounts_for_cpi); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + } + + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/mod.rs b/sdk-libs/sdk/src/compressible/mod.rs new file mode 100644 index 0000000000..488a302543 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -0,0 +1,9 @@ +//! SDK helpers for compressing and decompressing PDAs. + +pub mod compress_pda; +pub mod compress_pda_new; +pub mod decompress_idempotent; + +pub use compress_pda::{compress_pda, PdaTimingData}; +pub use compress_pda_new::{compress_multiple_pdas_new, compress_pda_new}; +pub use decompress_idempotent::{decompress_idempotent, decompress_multiple_idempotent}; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index b8eef1be97..bb86dc3c3b 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -111,6 +111,8 @@ pub mod error; /// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; +/// SDK helpers for compressing and decompressing PDAs. +pub mod compressible; pub mod token; /// Transfer compressed sol between compressed accounts. pub mod transfer; From d2774f3b6c978e732013d3cbdd165e154cab104c Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 09:43:52 -0400 Subject: [PATCH 14/16] testprogram uses sdk --- Cargo.lock | 2 + .../anchor-compressible-user/README.md | 127 ++---- .../anchor-compressible-user/Cargo.toml | 10 +- .../anchor-compressible-user/src/lib.rs | 392 ++---------------- .../anchor-compressible-user/tests/test.rs | 321 +++----------- .../sdk-test/src/compress_dynamic_pda.rs | 8 +- .../sdk-test/src/create_dynamic_pda.rs | 3 +- .../sdk-test/src/decompress_dynamic_pda.rs | 135 +++--- program-tests/sdk-test/src/lib.rs | 2 - .../sdk-test/src/sdk/compress_pda.rs | 115 ----- .../sdk-test/src/sdk/compress_pda_new.rs | 277 ------------- .../sdk-test/src/sdk/decompress_idempotent.rs | 279 ------------- program-tests/sdk-test/src/sdk/mod.rs | 3 - sdk-libs/sdk/Cargo.toml | 3 + sdk-libs/sdk/src/compressible/compress_pda.rs | 6 +- .../sdk/src/compressible/compress_pda_new.rs | 8 +- .../src/compressible/decompress_idempotent.rs | 17 +- 17 files changed, 211 insertions(+), 1497 deletions(-) delete mode 100644 program-tests/sdk-test/src/sdk/compress_pda.rs delete mode 100644 program-tests/sdk-test/src/sdk/compress_pda_new.rs delete mode 100644 program-tests/sdk-test/src/sdk/decompress_idempotent.rs delete mode 100644 program-tests/sdk-test/src/sdk/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c8e981fdf6..8cb045bfac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3624,12 +3624,14 @@ dependencies = [ "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", + "solana-clock", "solana-cpi", "solana-instruction", "solana-msg", "solana-program-error", "solana-pubkey", "solana-system-interface", + "solana-sysvar", "thiserror 2.0.12", ] diff --git a/program-tests/anchor-compressible-user/README.md b/program-tests/anchor-compressible-user/README.md index c1f1617fb5..bedb8b3271 100644 --- a/program-tests/anchor-compressible-user/README.md +++ b/program-tests/anchor-compressible-user/README.md @@ -1,114 +1,59 @@ -# Anchor Compressible User Records +# Simple Anchor User Records Template -A comprehensive example demonstrating how to use Light Protocol's compressible SDK with Anchor framework, including the SDK helper functions for compressing and decompressing PDAs. +A basic Anchor program template demonstrating a simple user record system with create and update functionality. ## Overview -This program demonstrates: +This is a minimal template showing: -- Creating compressed user records based on the signer's public key -- Using Anchor's account constraints with compressed accounts -- Updating compressed records -- Decompressing records to regular PDAs using SDK helpers -- Compressing PDAs back to compressed accounts using SDK helpers -- Using the `PdaTimingData` trait for time-based compression controls +- Creating user records as PDAs (Program Derived Addresses) +- Updating existing user records +- Basic ownership validation -## Key Features - -### 1. **Deterministic Addressing** - -User records are created at deterministic addresses derived from: - -```rust -seeds = [b"user_record", user_pubkey] -``` - -This ensures each user can only have one record and it can be found without scanning. - -### 2. **Compressed Account Structure with Timing** +## Account Structure ```rust -#[event] -#[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] +#[account] pub struct UserRecord { - #[hash] - pub owner: Pubkey, // The user who owns this record - pub name: String, // User's display name - pub bio: String, // User's bio - pub score: i64, // Some score/reputation - pub created_at: i64, // Creation timestamp - pub updated_at: i64, // Last update timestamp - // PDA timing data for compression/decompression - pub last_written_slot: u64, - pub slots_until_compression: u64, + pub owner: Pubkey, // The user who owns this record + pub name: String, // User's name + pub score: u64, // User's score } ``` -### 3. **Five Main Instructions** - -#### Create User Record - -- Creates a new compressed account for the user -- Uses the user's pubkey as a seed for deterministic addressing -- Initializes with name, bio, timestamps, and timing data - -#### Update User Record - -- Updates an existing compressed user record -- Verifies ownership before allowing updates -- Can update name, bio, or increment/decrement score -- Updates the last_written_slot for timing controls - -#### Decompress User Record +## Instructions -- Uses `decompress_idempotent` SDK helper -- Converts a compressed account to a regular on-chain PDA -- Idempotent - can be called multiple times safely -- Preserves all data during decompression +### Create Record -#### Compress User Record PDA +- Creates a new user record PDA +- Seeds: `[b"user_record", user_pubkey]` +- Initializes with name and score of 0 -- Uses `compress_pda` SDK helper -- Compresses an existing PDA back to a compressed account -- Requires the compressed account to already exist -- Enforces timing constraints (slots_until_compression) +### Update Record -#### Compress User Record PDA New +- Updates an existing user record +- Validates ownership before allowing updates +- Can update both name and score -- Uses `compress_pda_new` SDK helper -- Compresses a PDA into a new compressed account with a specific address -- Creates the compressed account and closes the PDA in one operation -- Also enforces timing constraints +## Usage -## Integration with Light SDK Helpers - -The program uses Light SDK's PDA helper functions: - -1. **`decompress_idempotent`**: Safely decompresses accounts, handling the case where the PDA might already exist -2. **`compress_pda`**: Compresses an existing PDA into an existing compressed account -3. **`compress_pda_new`**: Compresses a PDA into a new compressed account with a derived address -4. **`PdaTimingData` trait**: Implements timing controls for when PDAs can be compressed - -## PDA Timing Controls - -The program implements the `PdaTimingData` trait to control when PDAs can be compressed: - -- `last_written_slot`: Tracks when the PDA was last modified -- `slots_until_compression`: Number of slots that must pass before compression is allowed -- This prevents immediate compression after decompression, allowing for transaction finality - -## Testing +```bash +# Build +anchor build -The test file demonstrates: +# Test +anchor test +``` -- Creating a user record -- Updating the record -- Decompressing to a regular PDA -- Compressing back to a compressed account +## PDA Derivation -Run tests with: +User records are stored at deterministic addresses: -```bash -cd program-tests/anchor-compressible-user -cargo test-sbf +```rust +let (user_record_pda, bump) = Pubkey::find_program_address( + &[b"user_record", user.key().as_ref()], + &program_id, +); ``` + +This ensures each user can only have one record. diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml index 65e49cdbc6..17711594d7 100644 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "anchor-compressible-user" version = "0.1.0" -description = "Simple Anchor program demonstrating compressible accounts with user records" +description = "Simple Anchor program template with user records" edition = "2021" [lib] @@ -18,13 +18,7 @@ default = [] [dependencies] anchor-lang = { workspace = true } light-sdk = { workspace = true } -light-sdk-types = { workspace = true } -light-hasher = { workspace = true } -solana-program = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } -borsh = { workspace = true } [dev-dependencies] -light-program-test = { workspace = true, features = ["devenv"] } -tokio = { workspace = true } +solana-program-test = { workspace = true } solana-sdk = { workspace = true } \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs index 625f53f904..90e1ffa4d2 100644 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs @@ -1,409 +1,65 @@ -#![allow(unexpected_cfgs)] - -use anchor_lang::{prelude::*, Discriminator}; -use light_sdk::{ - account::LightAccount, - address::v1::derive_address, - cpi::{CpiAccounts, CpiInputs, CpiSigner}, - derive_light_cpi_signer, - instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof}, - pda::{compress_pda, compress_pda_new, decompress_idempotent, PdaTimingData}, - LightDiscriminator, LightHasher, -}; +use anchor_lang::prelude::*; declare_id!("CompUser11111111111111111111111111111111111"); -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("CompUser11111111111111111111111111111111111"); - +// Simple anchor program retrofitted with compressible accounts. #[program] pub mod anchor_compressible_user { use super::*; - /// Creates a new compressed user record based on the signer's pubkey - pub fn create_user_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecord<'info>>, - proof: ValidityProof, - address_tree_info: PackedAddressTreeInfo, - output_tree_index: u8, - name: String, - bio: String, - ) -> Result<()> { - let user = ctx.accounts.user.key(); - - // Derive address using user's pubkey as seed - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); - - let (address, address_seed) = derive_address( - &[b"user_record", user.as_ref()], - &address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, - &crate::ID, - ); - - let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); - - // Create the compressed user record - let mut user_record = - LightAccount::<'_, UserRecord>::new_init(&crate::ID, Some(address), output_tree_index); + /// Creates a new user record + pub fn create_record(ctx: Context, name: String) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; - user_record.owner = user; + user_record.owner = ctx.accounts.user.key(); user_record.name = name; - user_record.bio = bio; user_record.score = 0; - user_record.created_at = Clock::get()?.unix_timestamp; - user_record.updated_at = Clock::get()?.unix_timestamp; - user_record.last_written_slot = Clock::get()?.slot; - user_record.slots_until_compression = 100; // Can be compressed after 100 slots - - let cpi_inputs = CpiInputs::new_with_address( - proof, - vec![user_record.to_account_info().map_err(ProgramError::from)?], - vec![new_address_params], - ); - - cpi_inputs - .invoke_light_system_program(light_cpi_accounts) - .map_err(ProgramError::from)?; - - emit!(UserRecordCreated { - user, - address, - name: user_record.name.clone(), - }); - - Ok(()) - } - - /// Updates an existing compressed user record - pub fn update_user_record<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateUserRecord<'info>>, - proof: ValidityProof, - account_meta: CompressedAccountMeta, - current_record: UserRecord, - new_name: Option, - new_bio: Option, - score_delta: Option, - ) -> Result<()> { - let user = ctx.accounts.user.key(); - - // Verify ownership - require!(current_record.owner == user, ErrorCode::ConstraintOwner); - - let mut user_record = - LightAccount::<'_, UserRecord>::new_mut(&crate::ID, &account_meta, current_record) - .map_err(ProgramError::from)?; - - // Update fields if provided - if let Some(name) = new_name { - user_record.name = name; - } - if let Some(bio) = new_bio { - user_record.bio = bio; - } - if let Some(delta) = score_delta { - user_record.score = user_record.score.saturating_add_signed(delta); - } - user_record.updated_at = Clock::get()?.unix_timestamp; - user_record.last_written_slot = Clock::get()?.slot; - - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); - - let cpi_inputs = CpiInputs::new( - proof, - vec![user_record.to_account_info().map_err(ProgramError::from)?], - ); - - cpi_inputs - .invoke_light_system_program(light_cpi_accounts) - .map_err(ProgramError::from)?; - - emit!(UserRecordUpdated { - user, - new_score: user_record.score, - updated_at: user_record.updated_at, - }); - - Ok(()) - } - - /// Decompresses a user record to a regular PDA (for compatibility or migration) - pub fn decompress_user_record<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressUserRecord<'info>>, - proof: ValidityProof, - account_meta: CompressedAccountMeta, - compressed_record: UserRecord, - ) -> Result<()> { - let user = ctx.accounts.user.key(); - - // Verify ownership - require!(compressed_record.owner == user, ErrorCode::ConstraintOwner); - - let compressed_account = - LightAccount::<'_, UserRecord>::new_mut(&crate::ID, &account_meta, compressed_record) - .map_err(ProgramError::from)?; - - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); - - // Use the SDK helper for idempotent decompression - decompress_idempotent::( - &ctx.accounts.user_record_pda.to_account_info(), - compressed_account, - proof, - light_cpi_accounts, - &crate::ID, - &ctx.accounts.user.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - Clock::get()?.slot, - Rent::get()?.minimum_balance(std::mem::size_of::() + 8), - ) - .map_err(|_| error!(ErrorCode::CompressionError))?; - - emit!(UserRecordDecompressed { - user, - pda: ctx.accounts.user_record_pda.key(), - }); - - Ok(()) - } - - /// Compresses an existing PDA back to a compressed account - pub fn compress_user_record_pda<'info>( - ctx: Context<'_, '_, '_, 'info, CompressUserRecordPda<'info>>, - proof: ValidityProof, - compressed_account_meta: CompressedAccountMeta, - ) -> Result<()> { - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); - - // Use the SDK helper to compress the PDA - compress_pda::( - &ctx.accounts.user_record_pda.to_account_info(), - &compressed_account_meta, - proof, - light_cpi_accounts, - &crate::ID, - &ctx.accounts.rent_recipient.to_account_info(), - Clock::get()?.slot, - ) - .map_err(|_| error!(ErrorCode::CompressionError))?; - - emit!(UserRecordCompressed { - user: ctx.accounts.user_record_pda.owner, - pda: ctx.accounts.user_record_pda.key(), - }); Ok(()) } - /// Compresses a PDA into a new compressed account with a specific address - pub fn compress_user_record_pda_new<'info>( - ctx: Context<'_, '_, '_, 'info, CompressUserRecordPdaNew<'info>>, - proof: ValidityProof, - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - let light_cpi_accounts = CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - ctx.remaining_accounts, - crate::LIGHT_CPI_SIGNER, - ); + /// Updates an existing user record + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; - // Derive the address for the compressed account - let (address, address_seed) = derive_address( - &[b"user_record", ctx.accounts.user_record_pda.owner.as_ref()], - &address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, - &crate::ID, - ); - - let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); - - // Use the SDK helper to compress the PDA into a new compressed account - compress_pda_new::( - &ctx.accounts.user_record_pda.to_account_info(), - address, - new_address_params, - output_state_tree_index, - proof, - light_cpi_accounts, - &crate::ID, - &ctx.accounts.rent_recipient.to_account_info(), - &address_tree_info - .get_tree_pubkey(&light_cpi_accounts) - .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, - Clock::get()?.slot, - ) - .map_err(|_| error!(ErrorCode::CompressionError))?; - - emit!(UserRecordCompressedNew { - user: ctx.accounts.user_record_pda.owner, - pda: ctx.accounts.user_record_pda.key(), - compressed_address: address, - }); + user_record.name = name; + user_record.score = score; Ok(()) } } -/// Compressed user record that lives in the compressed state -#[event] -#[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] -pub struct UserRecord { - #[hash] - pub owner: Pubkey, - pub name: String, - pub bio: String, - pub score: i64, - pub created_at: i64, - pub updated_at: i64, - // PDA timing data for compression/decompression - pub last_written_slot: u64, - pub slots_until_compression: u64, -} - -// Implement the PdaTimingData trait for UserRecord -impl PdaTimingData for UserRecord { - fn last_written_slot(&self) -> u64 { - self.last_written_slot - } - - fn slots_until_compression(&self) -> u64 { - self.slots_until_compression - } - - fn set_last_written_slot(&mut self, slot: u64) { - self.last_written_slot = slot; - } -} - -/// Regular on-chain PDA for decompressed records -#[account] -pub struct UserRecordPda { - pub owner: Pubkey, - pub name: String, - pub bio: String, - pub score: i64, - pub created_at: i64, - pub updated_at: i64, - // PDA timing data - pub last_written_slot: u64, - pub slots_until_compression: u64, -} - #[derive(Accounts)] -pub struct CreateUserRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, -} - -#[derive(Accounts)] -pub struct UpdateUserRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, -} - -#[derive(Accounts)] -pub struct DecompressUserRecord<'info> { +pub struct CreateRecord<'info> { #[account(mut)] pub user: Signer<'info>, #[account( - init_if_needed, + init, payer = user, - space = 8 + 32 + 4 + 64 + 4 + 256 + 8 + 8 + 8 + 8 + 8, // discriminator + owner + string lens + strings + timestamps + timing + space = 8 + 32 + 4 + 32 + 8, // discriminator + owner + string len + name + score seeds = [b"user_record", user.key().as_ref()], bump, )] - pub user_record_pda: Account<'info, UserRecordPda>, + pub user_record: Account<'info, UserRecord>, pub system_program: Program<'info, System>, } #[derive(Accounts)] -pub struct CompressUserRecordPda<'info> { +pub struct UpdateRecord<'info> { #[account(mut)] - pub fee_payer: Signer<'info>, - #[account( - mut, - seeds = [b"user_record", user_record_pda.owner.as_ref()], - bump, - constraint = user_record_pda.owner == fee_payer.key(), - )] - pub user_record_pda: Account<'info, UserRecordPda>, - /// CHECK: Rent recipient can be any account - #[account(mut)] - pub rent_recipient: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct CompressUserRecordPdaNew<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, + pub user: Signer<'info>, #[account( mut, - seeds = [b"user_record", user_record_pda.owner.as_ref()], + seeds = [b"user_record", user.key().as_ref()], bump, - constraint = user_record_pda.owner == fee_payer.key(), + constraint = user_record.owner == user.key() )] - pub user_record_pda: Account<'info, UserRecordPda>, - /// CHECK: Rent recipient can be any account - #[account(mut)] - pub rent_recipient: AccountInfo<'info>, + pub user_record: Account<'info, UserRecord>, } -// Events -#[event] -pub struct UserRecordCreated { - pub user: Pubkey, - pub address: [u8; 32], +#[account] +pub struct UserRecord { + pub owner: Pubkey, pub name: String, -} - -#[event] -pub struct UserRecordUpdated { - pub user: Pubkey, - pub new_score: i64, - pub updated_at: i64, -} - -#[event] -pub struct UserRecordDecompressed { - pub user: Pubkey, - pub pda: Pubkey, -} - -#[event] -pub struct UserRecordCompressed { - pub user: Pubkey, - pub pda: Pubkey, -} - -#[event] -pub struct UserRecordCompressedNew { - pub user: Pubkey, - pub pda: Pubkey, - pub compressed_address: [u8; 32], -} - -// Error codes -#[error_code] -pub enum ErrorCode { - #[msg("Compression operation failed")] - CompressionError, + pub score: u64, } diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs index bc84f8b3c8..41bd86eb0d 100644 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs @@ -1,291 +1,82 @@ #![cfg(feature = "test-sbf")] -use anchor_compressible_user::{UserRecord, UserRecordCreated}; -use anchor_lang::{InstructionData, ToAccountMetas}; -use light_compressed_account::{ - address::derive_address, compressed_account::CompressedAccountWithMerkleContext, - hashv_to_bn254_field_size_be, -}; -use light_program_test::{ - program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::instruction::{ - account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig, -}; +use anchor_lang::prelude::*; +use anchor_lang::InstructionData; +use anchor_lang::ToAccountMetas; +use solana_program_test::*; use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, signature::{Keypair, Signer}, + transaction::Transaction, }; #[tokio::test] -async fn test_anchor_compressible_user() { - let config = ProgramTestConfig::new_v2( - true, - Some(vec![("anchor_compressible_user", anchor_compressible_user::ID)]), +async fn test_user_record() { + let program_id = anchor_compressible_user::ID; + let mut program_test = ProgramTest::new( + "anchor_compressible_user", + program_id, + processor!(anchor_compressible_user::entry), ); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - // Test 1: Create a user record - let user_name = "Alice".to_string(); - let user_bio = "I love compressed accounts!".to_string(); - - let address = create_user_record( - &mut rpc, - &payer, - user_name.clone(), - user_bio.clone(), - ) - .await - .unwrap(); - - // Verify the account was created - let compressed_account = rpc - .indexer() - .unwrap() - .get_compressed_account(address, None) - .await - .unwrap() - .value - .clone(); - - assert_eq!(compressed_account.address.unwrap(), address); - - // Test 2: Update the user record - let new_bio = "I REALLY love compressed accounts!".to_string(); - update_user_record( - &mut rpc, - &payer, - compressed_account.into(), - None, - Some(new_bio.clone()), - Some(100), - ) - .await - .unwrap(); - - // Test 3: Decompress the user record - let compressed_account = rpc - .indexer() - .unwrap() - .get_compressed_account(address, None) - .await - .unwrap() - .value - .clone(); - - decompress_user_record(&mut rpc, &payer, compressed_account.into()) - .await - .unwrap(); - - // Verify the PDA was created - let pda = Pubkey::find_program_address( - &[b"user_record", payer.pubkey().as_ref()], - &anchor_compressible_user::ID, - ) - .0; - - let pda_account = rpc.get_account(pda).await.unwrap(); - assert!(pda_account.is_some()); -} -async fn create_user_record( - rpc: &mut LightProgramTest, - user: &Keypair, - name: String, - bio: String, -) -> Result<[u8; 32], RpcError> { - let address_tree_pubkey = rpc.get_address_merkle_tree_v2(); - let output_queue = rpc.get_random_state_tree_info().unwrap().queue; - - // Derive the address based on user's pubkey - let address_seed = hashv_to_bn254_field_size_be(&[b"user_record", user.pubkey().as_ref()]); - let address = derive_address( - &address_seed, - &address_tree_pubkey.to_bytes(), - &anchor_compressible_user::ID.to_bytes(), + let (mut banks_client, payer, recent_blockhash) = program_test.start().await; + + // Test create_record + let user = payer; + let (user_record_pda, _bump) = Pubkey::find_program_address( + &[b"user_record", user.pubkey().as_ref()], + &program_id, ); - - // Get validity proof - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address, - tree: address_tree_pubkey, - }], - None, - ) - .await? - .value; - - // Pack accounts - let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); - let mut accounts = PackedAccounts::default(); - accounts.add_pre_accounts_signer(user.pubkey()); - accounts.add_system_accounts(system_account_meta_config); - - let output_merkle_tree_index = accounts.insert_or_get(output_queue); - let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; - let (accounts, _, _) = accounts.to_account_metas(); - - // Create instruction data - let instruction_data = anchor_compressible_user::instruction::CreateUserRecord { - proof: rpc_result.proof, - address_tree_info: packed_address_tree_info, - output_tree_index: output_merkle_tree_index, - name, - bio, - }; - - let instruction = Instruction { - program_id: anchor_compressible_user::ID, - accounts, - data: instruction_data.data(), - }; - - rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) - .await?; - - Ok(address) -} -async fn update_user_record( - rpc: &mut LightProgramTest, - user: &Keypair, - compressed_account: CompressedAccountWithMerkleContext, - new_name: Option, - new_bio: Option, - score_delta: Option, -) -> Result<(), RpcError> { - // Get validity proof - let rpc_result = rpc - .get_validity_proof(vec![compressed_account.hash().unwrap()], vec![], None) - .await? - .value; - - // Pack accounts - let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); - let mut accounts = PackedAccounts::default(); - accounts.add_pre_accounts_signer(user.pubkey()); - accounts.add_system_accounts(system_account_meta_config); - - let packed_accounts = rpc_result - .pack_tree_infos(&mut accounts) - .state_trees - .unwrap(); - - let meta = CompressedAccountMeta { - tree_info: packed_accounts.packed_tree_infos[0], - address: compressed_account.compressed_account.address.unwrap(), - output_state_tree_index: packed_accounts.output_tree_index, + let accounts = anchor_compressible_user::accounts::CreateRecord { + user: user.pubkey(), + user_record: user_record_pda, + system_program: solana_sdk::system_program::ID, }; - - let (accounts, _, _) = accounts.to_account_metas(); - - // Deserialize current record - let current_record: UserRecord = UserRecord::deserialize( - &mut &compressed_account - .compressed_account - .data - .unwrap() - .data[..], - ) - .unwrap(); - - // Create instruction data - let instruction_data = anchor_compressible_user::instruction::UpdateUserRecord { - proof: rpc_result.proof, - account_meta: meta, - current_record, - new_name, - new_bio, - score_delta, + + let instruction_data = anchor_compressible_user::instruction::CreateRecord { + name: "Alice".to_string(), }; - + let instruction = Instruction { - program_id: anchor_compressible_user::ID, - accounts, + program_id, + accounts: accounts.to_account_metas(None), data: instruction_data.data(), }; - - rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) - .await?; - - Ok(()) -} -async fn decompress_user_record( - rpc: &mut LightProgramTest, - user: &Keypair, - compressed_account: CompressedAccountWithMerkleContext, -) -> Result<(), RpcError> { - // Get validity proof - let rpc_result = rpc - .get_validity_proof(vec![compressed_account.hash().unwrap()], vec![], None) - .await? - .value; - - // Pack accounts - let system_account_meta_config = SystemAccountMetaConfig::new(anchor_compressible_user::ID); - let mut accounts = PackedAccounts::default(); - accounts.add_pre_accounts_signer(user.pubkey()); - accounts.add_system_accounts(system_account_meta_config); - - let packed_accounts = rpc_result - .pack_tree_infos(&mut accounts) - .state_trees - .unwrap(); - - let meta = CompressedAccountMeta { - tree_info: packed_accounts.packed_tree_infos[0], - address: compressed_account.compressed_account.address.unwrap(), - output_state_tree_index: packed_accounts.output_tree_index, - }; - - // Deserialize current record - let compressed_record: UserRecord = UserRecord::deserialize( - &mut &compressed_account - .compressed_account - .data - .unwrap() - .data[..], - ) - .unwrap(); - - // Get the PDA account - let user_record_pda = Pubkey::find_program_address( - &[b"user_record", user.pubkey().as_ref()], - &anchor_compressible_user::ID, - ) - .0; - - // Create instruction accounts - let instruction_accounts = anchor_compressible_user::accounts::DecompressUserRecord { + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&user.pubkey()), + &[&user], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); + + // Test update_record + let accounts = anchor_compressible_user::accounts::UpdateRecord { user: user.pubkey(), - user_record_pda, - system_program: solana_sdk::system_program::ID, + user_record: user_record_pda, }; - - let (mut accounts, _, _) = accounts.to_account_metas(); - accounts.extend_from_slice(&instruction_accounts.to_account_metas(Some(true))); - - // Create instruction data - let instruction_data = anchor_compressible_user::instruction::DecompressUserRecord { - proof: rpc_result.proof, - account_meta: meta, - compressed_record, + + let instruction_data = anchor_compressible_user::instruction::UpdateRecord { + name: "Alice Updated".to_string(), + score: 100, }; - + let instruction = Instruction { - program_id: anchor_compressible_user::ID, - accounts, + program_id, + accounts: accounts.to_account_metas(None), data: instruction_data.data(), }; - - rpc.create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) - .await?; - - Ok(()) + + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&user.pubkey()), + &[&user], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); } \ No newline at end of file diff --git a/program-tests/sdk-test/src/compress_dynamic_pda.rs b/program-tests/sdk-test/src/compress_dynamic_pda.rs index 71453119b8..c6c8f151cf 100644 --- a/program-tests/sdk-test/src/compress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/compress_dynamic_pda.rs @@ -1,5 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ + compressible::compress_pda, cpi::CpiAccounts, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, @@ -7,10 +8,7 @@ use light_sdk::{ use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; -use crate::{ - create_dynamic_pda::RENT_RECIPIENT, decompress_dynamic_pda::MyPdaAccount, - sdk::compress_pda::compress_pda, -}; +use crate::decompress_dynamic_pda::MyPdaAccount; /// Compresses a PDA back into a compressed account /// Anyone can call this after the timeout period has elapsed @@ -27,7 +25,7 @@ pub fn compress_dynamic_pda( // CHECK: hardcoded rent recipient. let rent_recipient = &accounts[2]; - if rent_recipient.key != &RENT_RECIPIENT { + if rent_recipient.key != &crate::create_dynamic_pda::RENT_RECIPIENT { return Err(LightSdkError::ConstraintViolation); } diff --git a/program-tests/sdk-test/src/create_dynamic_pda.rs b/program-tests/sdk-test/src/create_dynamic_pda.rs index eb45796fb7..1e3b84708d 100644 --- a/program-tests/sdk-test/src/create_dynamic_pda.rs +++ b/program-tests/sdk-test/src/create_dynamic_pda.rs @@ -1,6 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_macros::pubkey; use light_sdk::{ + compressible::compress_pda_new, cpi::CpiAccounts, error::LightSdkError, instruction::{PackedAddressTreeInfo, ValidityProof}, @@ -9,7 +10,7 @@ use light_sdk_types::CpiAccountsConfig; use solana_program::account_info::AccountInfo; use solana_program::pubkey::Pubkey; -use crate::{decompress_dynamic_pda::MyPdaAccount, sdk::compress_pda_new::compress_pda_new}; +use crate::decompress_dynamic_pda::MyPdaAccount; pub const ADDRESS_SPACE: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); diff --git a/program-tests/sdk-test/src/decompress_dynamic_pda.rs b/program-tests/sdk-test/src/decompress_dynamic_pda.rs index 0e89bb4e44..df8bc5b02b 100644 --- a/program-tests/sdk-test/src/decompress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/decompress_dynamic_pda.rs @@ -1,6 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, + compressible::{decompress_idempotent, PdaTimingData, SLOTS_UNTIL_COMPRESSION}, cpi::{CpiAccounts, CpiAccountsConfig}, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, @@ -8,10 +9,6 @@ use light_sdk::{ }; use solana_program::account_info::AccountInfo; -use crate::sdk::decompress_idempotent::decompress_idempotent; - -pub const SLOTS_UNTIL_COMPRESSION: u64 = 10_000; - /// Decompresses a compressed account into a PDA idempotently. pub fn decompress_dynamic_pda( accounts: &[AccountInfo], @@ -24,24 +21,34 @@ pub fn decompress_dynamic_pda( // Get accounts let fee_payer = &accounts[0]; let pda_account = &accounts[1]; - let rent_payer = &accounts[2]; // Anyone can pay. + let rent_payer = &accounts[2]; let system_program = &accounts[3]; - // Cpi accounts + // Set up CPI accounts + let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + config.sol_pool_pda = false; + config.sol_compression_recipient = false; + let cpi_accounts = CpiAccounts::new_with_config( fee_payer, &accounts[instruction_data.system_accounts_offset as usize..], - CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + config, ); - // we zero out the compressed account. + + // Prepare account data + let account_data = MyPdaAccount { + last_written_slot: 0, + slots_until_compression: SLOTS_UNTIL_COMPRESSION, + data: instruction_data.data, + }; + let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( &crate::ID, - &instruction_data.compressed_account.meta, - instruction_data.compressed_account.data, + &instruction_data.compressed_account_meta, + account_data, )?; - // Call the SDK function to decompress idempotently - // this inits pda_account if not already initialized + // Call decompress_idempotent - this should work whether PDA exists or not decompress_idempotent::( pda_account, compressed_account, @@ -52,60 +59,15 @@ pub fn decompress_dynamic_pda( system_program, )?; - // do something with pda_account... - Ok(()) } -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct DecompressToPdaInstructionData { - pub proof: ValidityProof, - pub compressed_account: MyCompressedAccount, - pub system_accounts_offset: u8, -} - -// just a wrapper -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct MyCompressedAccount { - pub meta: CompressedAccountMeta, - pub data: MyPdaAccount, -} - -/// Account structure for the PDA -#[derive( - Clone, Debug, LightHasher, LightDiscriminator, Default, BorshDeserialize, BorshSerialize, -)] -pub struct MyPdaAccount { - /// Slot when this account was last written - pub last_written_slot: u64, - /// Number of slots after last_written_slot until this account can be - /// compressed again - pub slots_until_compression: u64, - /// The actual account data - pub data: [u8; 31], -} - -// We require this trait to be implemented for the custom PDA account. -impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount { - fn last_written_slot(&self) -> u64 { - self.last_written_slot - } - - fn slots_until_compression(&self) -> u64 { - self.slots_until_compression - } - - fn set_last_written_slot(&mut self, slot: u64) { - self.last_written_slot = slot; - } -} - -// TODO: do this properly. +/// Example: Decompresses multiple compressed accounts into PDAs in a single transaction. pub fn decompress_multiple_dynamic_pdas( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { - use crate::sdk::decompress_idempotent::decompress_multiple_idempotent; + use light_sdk::compressible::decompress_multiple_idempotent; #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] pub struct DecompressMultipleInstructionData { @@ -123,21 +85,20 @@ pub fn decompress_multiple_dynamic_pdas( let rent_payer = &accounts[1]; let system_program = &accounts[2]; - // Calculate where PDA accounts start + // Get PDA accounts (after fixed accounts, before system accounts) let pda_accounts_start = 3; - let num_accounts = instruction_data.compressed_accounts.len(); + let pda_accounts_end = instruction_data.system_accounts_offset as usize; + let pda_accounts = &accounts[pda_accounts_start..pda_accounts_end]; - // Get PDA accounts - let pda_accounts = &accounts[pda_accounts_start..pda_accounts_start + num_accounts]; + // Set up CPI accounts + let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + config.sol_pool_pda = false; + config.sol_compression_recipient = false; - // Cpi accounts - // TODO: currently all cPDAs would have to have the same CPI_ACCOUNTS in the same order. - // - must support flexible CPI_ACCOUNTS eg for token accounts - // - must support flexible trees. let cpi_accounts = CpiAccounts::new_with_config( fee_payer, &accounts[instruction_data.system_accounts_offset as usize..], - CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + config, ); // Build inputs for batch decompression @@ -169,3 +130,41 @@ pub fn decompress_multiple_dynamic_pdas( Ok(()) } + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct DecompressToPdaInstructionData { + pub proof: ValidityProof, + pub compressed_account_meta: CompressedAccountMeta, + pub data: [u8; 31], + pub system_accounts_offset: u8, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct MyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: MyPdaAccount, +} + +#[derive( + Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize, +)] +pub struct MyPdaAccount { + pub last_written_slot: u64, + pub slots_until_compression: u64, + pub data: [u8; 31], +} + +// Implement the PdaTimingData trait +impl PdaTimingData for MyPdaAccount { + fn last_written_slot(&self) -> u64 { + self.last_written_slot + } + + fn slots_until_compression(&self) -> u64 { + self.slots_until_compression + } + + fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } +} diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index 98bdc1cf4a..e2a4ab77ac 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -8,8 +8,6 @@ pub mod compress_dynamic_pda; pub mod create_dynamic_pda; pub mod create_pda; pub mod decompress_dynamic_pda; -pub mod sdk; - pub mod update_pda; pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); diff --git a/program-tests/sdk-test/src/sdk/compress_pda.rs b/program-tests/sdk-test/src/sdk/compress_pda.rs deleted file mode 100644 index 71f82085c3..0000000000 --- a/program-tests/sdk-test/src/sdk/compress_pda.rs +++ /dev/null @@ -1,115 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::DataHasher; -use light_sdk::{ - account::LightAccount, - cpi::{CpiAccounts, CpiInputs}, - error::LightSdkError, - instruction::{account_meta::CompressedAccountMeta, ValidityProof}, - LightDiscriminator, -}; -use solana_program::sysvar::Sysvar; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, -}; - -/// Trait for PDA accounts that can be compressed -pub trait PdaTimingData { - fn last_written_slot(&self) -> u64; - fn slots_until_compression(&self) -> u64; - fn set_last_written_slot(&mut self, slot: u64); -} - -/// Helper function to compress a PDA and reclaim rent. -/// -/// 1. closes onchain PDA -/// 2. transfers PDA lamports to rent_recipient -/// 3. updates the empty compressed PDA with onchain PDA data -/// -/// This requires the compressed PDA that is tied to the onchain PDA to already -/// exist. -/// -/// # Arguments -/// * `pda_account` - The PDA account to compress (will be closed) -/// * `compressed_account_meta` - Metadata for the compressed account (must be -/// empty but have an address) -/// * `proof` - Validity proof -/// * `cpi_accounts` - Accounts needed for CPI -/// * `owner_program` - The program that will own the compressed account -/// * `rent_recipient` - The account to receive the PDA's rent -// -// TODO: -// - check if any explicit checks required for compressed account? -// - consider multiple accounts per ix. -pub fn compress_pda( - pda_account: &AccountInfo, - compressed_account_meta: &CompressedAccountMeta, - proof: ValidityProof, - cpi_accounts: CpiAccounts, - owner_program: &Pubkey, - rent_recipient: &AccountInfo, -) -> Result<(), LightSdkError> -where - A: DataHasher - + LightDiscriminator - + BorshSerialize - + BorshDeserialize - + Default - + PdaTimingData, -{ - // Check that the PDA account is owned by the caller program - if pda_account.owner != owner_program { - msg!( - "Invalid PDA owner. Expected: {}. Found: {}.", - owner_program, - pda_account.owner - ); - return Err(LightSdkError::ConstraintViolation); - } - - let current_slot = Clock::get()?.slot; - - // Deserialize the PDA data to check timing fields - let pda_data = pda_account.try_borrow_data()?; - let pda_account_data = A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; - drop(pda_data); - - let last_written_slot = pda_account_data.last_written_slot(); - let slots_until_compression = pda_account_data.slots_until_compression(); - - if current_slot < last_written_slot + slots_until_compression { - msg!( - "Cannot compress yet. {} slots remaining", - (last_written_slot + slots_until_compression).saturating_sub(current_slot) - ); - return Err(LightSdkError::ConstraintViolation); - } - - // Get the PDA lamports before we close it - let pda_lamports = pda_account.lamports(); - - let mut compressed_account = - LightAccount::<'_, A>::new_mut(owner_program, compressed_account_meta, A::default())?; - - compressed_account.account = pda_account_data; - - // Create CPI inputs - let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]); - - // Invoke light system program to create the compressed account - cpi_inputs.invoke_light_system_program(cpi_accounts)?; - - // Close the PDA account - // 1. Transfer all lamports to the rent recipient - let dest_starting_lamports = rent_recipient.lamports(); - **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports - .checked_add(pda_lamports) - .ok_or(ProgramError::ArithmeticOverflow)?; - // 2. Decrement source account lamports - **pda_account.try_borrow_mut_lamports()? = 0; - // 3. Clear all account data - pda_account.try_borrow_mut_data()?.fill(0); - // 4. Assign ownership back to the system program - pda_account.assign(&solana_program::system_program::ID); - - Ok(()) -} diff --git a/program-tests/sdk-test/src/sdk/compress_pda_new.rs b/program-tests/sdk-test/src/sdk/compress_pda_new.rs deleted file mode 100644 index fe5cc77172..0000000000 --- a/program-tests/sdk-test/src/sdk/compress_pda_new.rs +++ /dev/null @@ -1,277 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::DataHasher; -use light_sdk::{ - account::LightAccount, - address::{v1::derive_address, PackedNewAddressParams}, - cpi::{CpiAccounts, CpiInputs}, - error::LightSdkError, - instruction::ValidityProof, - light_account_checks::AccountInfoTrait, - LightDiscriminator, -}; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey, - sysvar::Sysvar, -}; - -use crate::sdk::compress_pda::PdaTimingData; - -/// Helper function to compress an onchain PDA into a new compressed account. -/// -/// This function handles the entire compression operation: creates a compressed account, -/// copies the PDA data, and closes the onchain PDA. -/// -/// # Arguments -/// * `pda_account` - The PDA account to compress (will be closed) -/// * `address` - The address for the compressed account -/// * `new_address_params` - Address parameters for the compressed account -/// * `output_state_tree_index` - Output state tree index for the compressed account -/// * `proof` - Validity proof -/// * `cpi_accounts` - Accounts needed for CPI -/// * `owner_program` - The program that will own the compressed account -/// * `rent_recipient` - The account to receive the PDA's rent -/// * `expected_address_space` - Optional expected address space pubkey to validate against -/// -/// # Returns -/// * `Ok(())` if the PDA was compressed successfully -/// * `Err(LightSdkError)` if there was an error -pub fn compress_pda_new<'info, A>( - pda_account: &AccountInfo<'info>, - address: [u8; 32], - new_address_params: PackedNewAddressParams, - output_state_tree_index: u8, - proof: ValidityProof, - cpi_accounts: CpiAccounts<'_, 'info>, - owner_program: &Pubkey, - rent_recipient: &AccountInfo<'info>, - expected_address_space: &Pubkey, -) -> Result<(), LightSdkError> -where - A: DataHasher - + LightDiscriminator - + BorshSerialize - + BorshDeserialize - + Default - + PdaTimingData - + Clone, -{ - compress_multiple_pdas_new::( - &[pda_account], - &[address], - vec![new_address_params], - &[output_state_tree_index], - proof, - cpi_accounts, - owner_program, - rent_recipient, - expected_address_space, - ) -} - -/// Helper function to compress multiple onchain PDAs into new compressed accounts. -/// -/// This function handles the entire compression operation for multiple PDAs. -/// -/// # Arguments -/// * `pda_accounts` - The PDA accounts to compress (will be closed) -/// * `addresses` - The addresses for the compressed accounts -/// * `new_address_params` - Address parameters for the compressed accounts -/// * `output_state_tree_indices` - Output state tree indices for the compressed accounts -/// * `proof` - Single validity proof for all accounts -/// * `cpi_accounts` - Accounts needed for CPI -/// * `owner_program` - The program that will own the compressed accounts -/// * `rent_recipient` - The account to receive the PDAs' rent -/// * `expected_address_space` - Optional expected address space pubkey to validate against -/// -/// # Returns -/// * `Ok(())` if all PDAs were compressed successfully -/// * `Err(LightSdkError)` if there was an error -pub fn compress_multiple_pdas_new<'info, A>( - pda_accounts: &[&AccountInfo<'info>], - addresses: &[[u8; 32]], - new_address_params: Vec, - output_state_tree_indices: &[u8], - proof: ValidityProof, - cpi_accounts: CpiAccounts<'_, 'info>, - owner_program: &Pubkey, - rent_recipient: &AccountInfo<'info>, - expected_address_space: &Pubkey, -) -> Result<(), LightSdkError> -where - A: DataHasher - + LightDiscriminator - + BorshSerialize - + BorshDeserialize - + Default - + PdaTimingData - + Clone, -{ - if pda_accounts.len() != addresses.len() - || pda_accounts.len() != new_address_params.len() - || pda_accounts.len() != output_state_tree_indices.len() - { - return Err(LightSdkError::ConstraintViolation); - } - - // CHECK: address space. - for params in &new_address_params { - let address_tree_account = cpi_accounts - .get_tree_account_info(params.address_merkle_tree_account_index as usize)?; - if address_tree_account.pubkey() != *expected_address_space { - msg!( - "Invalid address space. Expected: {}. Found: {}.", - expected_address_space, - address_tree_account.pubkey() - ); - return Err(LightSdkError::ConstraintViolation); - } - } - - let current_slot = Clock::get()?.slot; - let mut total_lamports = 0u64; - let mut compressed_account_infos = Vec::new(); - - for ((pda_account, &address), &output_state_tree_index) in pda_accounts - .iter() - .zip(addresses.iter()) - .zip(output_state_tree_indices.iter()) - { - // Check that the PDA account is owned by the caller program - if pda_account.owner != owner_program { - msg!( - "Invalid PDA owner for {}. Expected: {}. Found: {}.", - pda_account.key, - owner_program, - pda_account.owner - ); - return Err(LightSdkError::ConstraintViolation); - } - - // Deserialize the PDA data to check timing fields - let pda_data = pda_account.try_borrow_data()?; - let pda_account_data = - A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; - drop(pda_data); - - let last_written_slot = pda_account_data.last_written_slot(); - let slots_until_compression = pda_account_data.slots_until_compression(); - - if current_slot < last_written_slot + slots_until_compression { - msg!( - "Cannot compress {} yet. {} slots remaining", - pda_account.key, - (last_written_slot + slots_until_compression).saturating_sub(current_slot) - ); - return Err(LightSdkError::ConstraintViolation); - } - - // Create the compressed account with the PDA data - let mut compressed_account = - LightAccount::<'_, A>::new_init(owner_program, Some(address), output_state_tree_index); - compressed_account.account = pda_account_data; - - compressed_account_infos.push(compressed_account.to_account_info()?); - - // Accumulate lamports - total_lamports = total_lamports - .checked_add(pda_account.lamports()) - .ok_or(ProgramError::ArithmeticOverflow)?; - } - - // Create CPI inputs with all compressed accounts and new addresses - let cpi_inputs = - CpiInputs::new_with_address(proof, compressed_account_infos, new_address_params); - - // Invoke light system program to create all compressed accounts - cpi_inputs.invoke_light_system_program(cpi_accounts)?; - - // Close all PDA accounts - let dest_starting_lamports = rent_recipient.lamports(); - **rent_recipient.try_borrow_mut_lamports()? = dest_starting_lamports - .checked_add(total_lamports) - .ok_or(ProgramError::ArithmeticOverflow)?; - - for pda_account in pda_accounts { - // Decrement source account lamports - **pda_account.try_borrow_mut_lamports()? = 0; - // Clear all account data - pda_account.try_borrow_mut_data()?.fill(0); - // Assign ownership back to the system program - pda_account.assign(&solana_program::system_program::ID); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::decompress_dynamic_pda::MyPdaAccount; - use light_sdk::cpi::CpiAccountsConfig; - use light_sdk::instruction::PackedAddressTreeInfo; - - /// Test instruction that demonstrates compressing an onchain PDA into a new compressed account - pub fn test_compress_pda_new( - accounts: &[AccountInfo], - instruction_data: &[u8], - ) -> Result<(), LightSdkError> { - msg!("Testing compress PDA into new compressed account"); - - #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] - struct TestInstructionData { - pub proof: ValidityProof, - pub address_tree_info: PackedAddressTreeInfo, - pub output_state_tree_index: u8, - pub system_accounts_offset: u8, - } - - let mut instruction_data = instruction_data; - let instruction_data = TestInstructionData::deserialize(&mut instruction_data) - .map_err(|_| LightSdkError::Borsh)?; - - // Get accounts - let fee_payer = &accounts[0]; - let pda_account = &accounts[1]; - let rent_recipient = &accounts[2]; - - // Set up CPI accounts - let cpi_accounts = CpiAccounts::new_with_config( - fee_payer, - &accounts[instruction_data.system_accounts_offset as usize..], - CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), - ); - - // Get the address tree pubkey - let address_tree_pubkey = instruction_data - .address_tree_info - .get_tree_pubkey(&cpi_accounts)?; - - // This can happen offchain too! - let (address, address_seed) = derive_address( - &[pda_account.key.as_ref()], - &address_tree_pubkey, - &crate::ID, - ); - - // Create new address params - let new_address_params = instruction_data - .address_tree_info - .into_new_address_params_packed(address_seed); - - // Compress the PDA - this handles everything internally - compress_pda_new::( - pda_account, - address, - new_address_params, - instruction_data.output_state_tree_index, - instruction_data.proof, - cpi_accounts, - &crate::ID, - rent_recipient, - &crate::create_dynamic_pda::ADDRESS_SPACE, - )?; - - msg!("PDA compressed successfully into new compressed account"); - Ok(()) - } -} diff --git a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs b/program-tests/sdk-test/src/sdk/decompress_idempotent.rs deleted file mode 100644 index a2add4a480..0000000000 --- a/program-tests/sdk-test/src/sdk/decompress_idempotent.rs +++ /dev/null @@ -1,279 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::DataHasher; -use light_sdk::{ - account::LightAccount, - cpi::{CpiAccounts, CpiInputs}, - error::LightSdkError, - instruction::{account_meta::CompressedAccountMeta, ValidityProof}, - LightDiscriminator, -}; -use solana_program::{ - account_info::AccountInfo, clock::Clock, msg, program::invoke_signed, pubkey::Pubkey, - rent::Rent, system_instruction, sysvar::Sysvar, -}; - -use crate::sdk::compress_pda::PdaTimingData; - -pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; - -/// Helper function to decompress a compressed account into a PDA idempotently. -/// -/// This function is idempotent, meaning it can be called multiple times with the same compressed account -/// and it will only decompress it once. If the PDA already exists and is initialized, it returns early. -/// -/// # Arguments -/// * `pda_account` - The PDA account to decompress into -/// * `compressed_account` - The compressed account to decompress -/// * `proof` - Validity proof -/// * `cpi_accounts` - Accounts needed for CPI -/// * `owner_program` - The program that will own the PDA -/// * `rent_payer` - The account to pay for PDA rent -/// * `system_program` - The system program -/// -/// # Returns -/// * `Ok(())` if the compressed account was decompressed successfully or PDA already exists -/// * `Err(LightSdkError)` if there was an error -pub fn decompress_idempotent<'info, A>( - pda_account: &AccountInfo<'info>, - compressed_account: LightAccount<'_, A>, - proof: ValidityProof, - cpi_accounts: CpiAccounts<'_, 'info>, - owner_program: &Pubkey, - rent_payer: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, -) -> Result<(), LightSdkError> -where - A: DataHasher - + LightDiscriminator - + BorshSerialize - + BorshDeserialize - + Default - + Clone - + PdaTimingData, -{ - decompress_multiple_idempotent( - &[pda_account], - vec![compressed_account], - proof, - cpi_accounts, - owner_program, - rent_payer, - system_program, - ) -} - -/// Helper function to decompress multiple compressed accounts into PDAs idempotently. -/// -/// This function is idempotent, meaning it can be called multiple times with the same compressed accounts -/// and it will only decompress them once. If a PDA already exists and is initialized, it skips that account. -/// -/// # Arguments -/// * `pda_accounts` - The PDA accounts to decompress into -/// * `compressed_accounts` - The compressed accounts to decompress -/// * `proof` - Single validity proof for all accounts -/// * `cpi_accounts` - Accounts needed for CPI -/// * `owner_program` - The program that will own the PDAs -/// * `rent_payer` - The account to pay for PDA rent -/// * `system_program` - The system program -/// -/// # Returns -/// * `Ok(())` if all compressed accounts were decompressed successfully or PDAs already exist -/// * `Err(LightSdkError)` if there was an error -pub fn decompress_multiple_idempotent<'info, A>( - pda_accounts: &[&AccountInfo<'info>], - compressed_accounts: Vec>, - proof: ValidityProof, - cpi_accounts: CpiAccounts<'_, 'info>, - owner_program: &Pubkey, - rent_payer: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, -) -> Result<(), LightSdkError> -where - A: DataHasher - + LightDiscriminator - + BorshSerialize - + BorshDeserialize - + Default - + Clone - + PdaTimingData, -{ - // Get current slot and rent once for all accounts - let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; - let current_slot = clock.slot; - let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; - - // Calculate space needed for PDA (same for all accounts of type A) - let space = std::mem::size_of::() + 8; // +8 for discriminator - let minimum_balance = rent.minimum_balance(space); - - // Collect compressed accounts for CPI - let mut compressed_accounts_for_cpi = Vec::new(); - - for (pda_account, mut compressed_account) in - pda_accounts.iter().zip(compressed_accounts.into_iter()) - { - // Check if PDA is already initialized - if pda_account.data_len() > 0 { - msg!( - "PDA {} already initialized, skipping decompression", - pda_account.key - ); - continue; - } - - // Get the compressed account address - let compressed_address = compressed_account - .address() - .ok_or(LightSdkError::ConstraintViolation)?; - - // Derive onchain PDA using the compressed address as seed - let seeds: Vec<&[u8]> = vec![&compressed_address]; - - let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); - - // Verify PDA matches - if pda_pubkey != *pda_account.key { - msg!("Invalid PDA pubkey for account {}", pda_account.key); - return Err(LightSdkError::ConstraintViolation); - } - - // Create PDA account - let create_account_ix = system_instruction::create_account( - rent_payer.key, - pda_account.key, - minimum_balance, - space as u64, - owner_program, - ); - - // Add bump to seeds for signing - let bump_seed = [pda_bump]; - let mut signer_seeds = seeds.clone(); - signer_seeds.push(&bump_seed); - let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect(); - - invoke_signed( - &create_account_ix, - &[ - rent_payer.clone(), - (*pda_account).clone(), - system_program.clone(), - ], - &[&signer_seeds_refs], - )?; - - // Initialize PDA with decompressed data and update slot - let mut decompressed_pda = compressed_account.account.clone(); - decompressed_pda.set_last_written_slot(current_slot); - - // Write discriminator - let discriminator = A::LIGHT_DISCRIMINATOR; - pda_account.try_borrow_mut_data()?[..8].copy_from_slice(&discriminator); - - // Write data to PDA - decompressed_pda - .serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..]) - .map_err(|_| LightSdkError::Borsh)?; - - // Zero the compressed account - compressed_account.account = A::default(); - - // Add to CPI batch - compressed_accounts_for_cpi.push(compressed_account.to_account_info()?); - } - - // Make single CPI call with all compressed accounts - if !compressed_accounts_for_cpi.is_empty() { - let cpi_inputs = CpiInputs::new(proof, compressed_accounts_for_cpi); - cpi_inputs.invoke_light_system_program(cpi_accounts)?; - } - - Ok(()) -} - -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct DecompressToPdaInstructionData { - pub proof: ValidityProof, - pub compressed_account: DecompressMyCompressedAccount, - pub additional_seed: [u8; 32], // Additional seed for PDA derivation - pub system_accounts_offset: u8, -} - -#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] -pub struct DecompressMyCompressedAccount { - pub meta: CompressedAccountMeta, - pub data: [u8; 31], -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::decompress_dynamic_pda::MyPdaAccount; - use light_sdk::cpi::CpiAccountsConfig; - - /// Test instruction that demonstrates idempotent decompression - /// This can be called multiple times with the same compressed account - pub fn test_decompress_idempotent( - accounts: &[AccountInfo], - instruction_data: &[u8], - ) -> Result<(), LightSdkError> { - msg!("Testing idempotent decompression"); - - #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] - struct TestInstructionData { - pub proof: ValidityProof, - pub compressed_account_meta: Option, - pub data: [u8; 31], - pub additional_seed: [u8; 32], - pub system_accounts_offset: u8, - } - - let mut instruction_data = instruction_data; - let instruction_data = TestInstructionData::deserialize(&mut instruction_data) - .map_err(|_| LightSdkError::Borsh)?; - - // Get accounts - let fee_payer = &accounts[0]; - let pda_account = &accounts[1]; - let rent_payer = &accounts[2]; - let system_program = &accounts[3]; - - // Set up CPI accounts - let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - config.sol_pool_pda = false; - config.sol_compression_recipient = false; - - let cpi_accounts = CpiAccounts::new_with_config( - fee_payer, - &accounts[instruction_data.system_accounts_offset as usize..], - config, - ); - - // Prepare account data - let account_data = MyPdaAccount { - last_written_slot: 0, - slots_until_compression: SLOTS_UNTIL_COMPRESSION, - data: instruction_data.data, - }; - - let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( - &crate::ID, - &instruction_data.compressed_account_meta.unwrap(), - account_data, - )?; - - // Call decompress_idempotent - this should work whether PDA exists or not - decompress_idempotent::( - pda_account, - compressed_account, - instruction_data.proof, - cpi_accounts, - &crate::ID, - rent_payer, - system_program, - )?; - - msg!("Idempotent decompression completed successfully"); - Ok(()) - } -} diff --git a/program-tests/sdk-test/src/sdk/mod.rs b/program-tests/sdk-test/src/sdk/mod.rs deleted file mode 100644 index 4c94591dd8..0000000000 --- a/program-tests/sdk-test/src/sdk/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod compress_pda; -pub mod compress_pda_new; -pub mod decompress_idempotent; diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 8d4deaa05e..b8fc645f02 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -29,6 +29,9 @@ solana-cpi = { workspace = true } solana-program-error = { workspace = true } solana-instruction = { workspace = true } solana-system-interface = { workspace = true } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } +solana-rent = { workspace = true } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } diff --git a/sdk-libs/sdk/src/compressible/compress_pda.rs b/sdk-libs/sdk/src/compressible/compress_pda.rs index d8501c0088..330d5e2320 100644 --- a/sdk-libs/sdk/src/compressible/compress_pda.rs +++ b/sdk-libs/sdk/src/compressible/compress_pda.rs @@ -11,9 +11,11 @@ use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as Bors use borsh::{BorshDeserialize, BorshSerialize}; use light_hasher::DataHasher; use solana_account_info::AccountInfo; +use solana_clock::Clock; use solana_msg::msg; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; +use solana_sysvar::Sysvar; /// Trait for PDA accounts that can be compressed pub trait PdaTimingData { @@ -39,7 +41,6 @@ pub trait PdaTimingData { /// * `cpi_accounts` - Accounts needed for CPI /// * `owner_program` - The program that will own the compressed account /// * `rent_recipient` - The account to receive the PDA's rent -/// * `current_slot` - The current slot for timing checks // // TODO: // - check if any explicit checks required for compressed account? @@ -51,7 +52,6 @@ pub fn compress_pda( cpi_accounts: CpiAccounts, owner_program: &Pubkey, rent_recipient: &AccountInfo, - current_slot: u64, ) -> Result<(), LightSdkError> where A: DataHasher @@ -71,6 +71,8 @@ where return Err(LightSdkError::ConstraintViolation); } + let current_slot = Clock::get()?.slot; + // Deserialize the PDA data to check timing fields let pda_data = pda_account.try_borrow_data()?; let pda_account_data = A::try_from_slice(&pda_data[8..]).map_err(|_| LightSdkError::Borsh)?; diff --git a/sdk-libs/sdk/src/compressible/compress_pda_new.rs b/sdk-libs/sdk/src/compressible/compress_pda_new.rs index 9d53fee9aa..6263786161 100644 --- a/sdk-libs/sdk/src/compressible/compress_pda_new.rs +++ b/sdk-libs/sdk/src/compressible/compress_pda_new.rs @@ -13,9 +13,11 @@ use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as Bors use borsh::{BorshDeserialize, BorshSerialize}; use light_hasher::DataHasher; use solana_account_info::AccountInfo; +use solana_clock::Clock; use solana_msg::msg; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; +use solana_sysvar::Sysvar; use crate::compressible::compress_pda::PdaTimingData; @@ -34,7 +36,6 @@ use crate::compressible::compress_pda::PdaTimingData; /// * `owner_program` - The program that will own the compressed account /// * `rent_recipient` - The account to receive the PDA's rent /// * `expected_address_space` - Optional expected address space pubkey to validate against -/// * `current_slot` - The current slot for timing checks /// /// # Returns /// * `Ok(())` if the PDA was compressed successfully @@ -49,7 +50,6 @@ pub fn compress_pda_new<'info, A>( owner_program: &Pubkey, rent_recipient: &AccountInfo<'info>, expected_address_space: &Pubkey, - current_slot: u64, ) -> Result<(), LightSdkError> where A: DataHasher @@ -70,7 +70,6 @@ where owner_program, rent_recipient, expected_address_space, - current_slot, ) } @@ -88,7 +87,6 @@ where /// * `owner_program` - The program that will own the compressed accounts /// * `rent_recipient` - The account to receive the PDAs' rent /// * `expected_address_space` - Optional expected address space pubkey to validate against -/// * `current_slot` - The current slot for timing checks /// /// # Returns /// * `Ok(())` if all PDAs were compressed successfully @@ -103,7 +101,6 @@ pub fn compress_multiple_pdas_new<'info, A>( owner_program: &Pubkey, rent_recipient: &AccountInfo<'info>, expected_address_space: &Pubkey, - current_slot: u64, ) -> Result<(), LightSdkError> where A: DataHasher @@ -163,6 +160,7 @@ where let last_written_slot = pda_account_data.last_written_slot(); let slots_until_compression = pda_account_data.slots_until_compression(); + let current_slot = Clock::get()?.slot; if current_slot < last_written_slot + slots_until_compression { msg!( "Cannot compress {} yet. {} slots remaining", diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs index 50070d90f9..c9ce8db4eb 100644 --- a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -11,10 +11,13 @@ use anchor_lang::{AnchorDeserialize as BorshDeserialize, AnchorSerialize as Bors use borsh::{BorshDeserialize, BorshSerialize}; use light_hasher::DataHasher; use solana_account_info::AccountInfo; +use solana_clock::Clock; use solana_cpi::invoke_signed; use solana_msg::msg; use solana_pubkey::Pubkey; +use solana_rent::Rent; use solana_system_interface::instruction as system_instruction; +use solana_sysvar::Sysvar; use crate::compressible::compress_pda::PdaTimingData; @@ -33,8 +36,6 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; /// * `owner_program` - The program that will own the PDA /// * `rent_payer` - The account to pay for PDA rent /// * `system_program` - The system program -/// * `current_slot` - The current slot for timing -/// * `rent_minimum_balance` - The minimum balance required for rent exemption /// /// # Returns /// * `Ok(())` if the compressed account was decompressed successfully or PDA already exists @@ -47,8 +48,6 @@ pub fn decompress_idempotent<'info, A>( owner_program: &Pubkey, rent_payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, - current_slot: u64, - rent_minimum_balance: u64, ) -> Result<(), LightSdkError> where A: DataHasher @@ -67,8 +66,6 @@ where owner_program, rent_payer, system_program, - current_slot, - rent_minimum_balance, ) } @@ -97,8 +94,6 @@ pub fn decompress_multiple_idempotent<'info, A>( owner_program: &Pubkey, rent_payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, - current_slot: u64, - rent_minimum_balance: u64, ) -> Result<(), LightSdkError> where A: DataHasher @@ -109,8 +104,14 @@ where + Clone + PdaTimingData, { + // Get current slot and rent once for all accounts + let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?; + let current_slot = clock.slot; + let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?; + // Calculate space needed for PDA (same for all accounts of type A) let space = std::mem::size_of::() + 8; // +8 for discriminator + let rent_minimum_balance = rent.minimum_balance(space); // Collect compressed accounts for CPI let mut compressed_accounts_for_cpi = Vec::new(); From 97c98223a96149a622ebac3f0ab897c59eeb60d9 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 10:05:29 -0400 Subject: [PATCH 15/16] fix compilation --- Cargo.lock | 1 + Cargo.toml | 1 + pnpm-lock.yaml | 186 +++++++++++++++++- .../anchor-compressible-user/src/lib.rs | 13 +- .../sdk-test/src/create_dynamic_pda.rs | 2 - .../sdk-test/src/decompress_dynamic_pda.rs | 23 +-- 6 files changed, 198 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cb045bfac..3191a8cbca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3630,6 +3630,7 @@ dependencies = [ "solana-msg", "solana-program-error", "solana-pubkey", + "solana-rent", "solana-system-interface", "solana-sysvar", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index 176115adb1..e36663b1c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ solana-transaction = { version = "2.2" } solana-transaction-error = { version = "2.2" } solana-hash = { version = "2.2" } solana-clock = { version = "2.2" } +solana-rent = { version = "2.2" } solana-signature = { version = "2.2" } solana-commitment-config = { version = "2.2" } solana-account = { version = "2.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c9e247861..0e2ed104a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,6 +522,28 @@ importers: program-tests: {} + program-tests/anchor-compressible-user: + dependencies: + '@coral-xyz/anchor': + specifier: ^0.29.0 + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + devDependencies: + chai: + specifier: ^4.3.4 + version: 4.5.0 + mocha: + specifier: ^9.0.3 + version: 9.2.2 + prettier: + specifier: ^2.6.2 + version: 2.8.8 + ts-mocha: + specifier: ^10.0.0 + version: 10.1.0(mocha@9.2.2) + typescript: + specifier: ^4.3.5 + version: 4.9.5 + program-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': @@ -3329,6 +3351,9 @@ packages: resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/promise-all-settled@1.1.2': + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3532,6 +3557,10 @@ packages: anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -4046,6 +4075,10 @@ packages: check-types@11.2.3: resolution: {integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==} + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4317,6 +4350,15 @@ packages: supports-color: optional: true + debug@4.3.3: + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -4452,6 +4494,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -5379,6 +5425,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Glob versions prior to v9 are no longer supported + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -5445,6 +5495,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + growl@1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -6428,6 +6482,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@4.2.1: + resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} + engines: {node: '>=10'} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -6486,6 +6544,11 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + mocha@9.2.2: + resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} + engines: {node: '>= 12.0.0'} + hasBin: true + mock-stdin@1.0.0: resolution: {integrity: sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==} @@ -6505,13 +6568,13 @@ packages: nanoassert@2.0.0: resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.1: + resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -7081,6 +7144,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -7511,6 +7579,9 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} + serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -8124,6 +8195,11 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.5.3: resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} @@ -8410,6 +8486,9 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerpool@6.2.0: + resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} + workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} @@ -8497,6 +8576,10 @@ packages: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} + yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -10007,11 +10090,11 @@ snapshots: '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.98.0) '@noble/hashes': 1.5.0 '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - bn.js: 5.2.1 + bn.js: 5.2.2 bs58: 4.0.1 buffer-layout: 1.2.2 camelcase: 6.3.0 - cross-fetch: 3.1.8 + cross-fetch: 3.2.0 crypto-hash: 1.3.0 eventemitter3: 4.0.7 pako: 2.1.0 @@ -12423,6 +12506,8 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 + '@ungap/promise-all-settled@1.1.2': {} + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-darwin-arm64@1.7.2': @@ -12597,6 +12682,8 @@ snapshots: anser@1.4.10: {} + ansi-colors@4.1.1: {} + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -13256,6 +13343,18 @@ snapshots: check-types@11.2.3: {} + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -13560,6 +13659,12 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.3(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 @@ -13660,6 +13765,8 @@ snapshots: diff@4.0.2: {} + diff@5.0.0: {} + diff@5.2.0: {} diff@7.0.0: {} @@ -14998,6 +15105,15 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 2.0.0 + glob@7.2.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15095,6 +15211,8 @@ snapshots: graphemer@1.4.0: {} + growl@1.10.5: {} + has-bigints@1.0.2: {} has-bigints@1.1.0: {} @@ -16269,6 +16387,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@4.2.1: + dependencies: + brace-expansion: 1.1.11 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -16354,6 +16476,33 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 + mocha@9.2.2: + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.3(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 4.2.1 + ms: 2.1.3 + nanoid: 3.3.1 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + which: 2.0.2 + workerpool: 6.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + mock-stdin@1.0.0: {} ms@2.0.0: {} @@ -16366,9 +16515,9 @@ snapshots: nanoassert@2.0.0: {} - nanoid@3.3.11: {} + nanoid@3.3.1: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} napi-postinstall@0.2.3: {} @@ -16939,7 +17088,7 @@ snapshots: postcss@8.5.1: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -16947,6 +17096,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@2.8.8: {} + prettier@3.3.3: {} prettier@3.4.2: {} @@ -17480,6 +17631,10 @@ snapshots: serialize-error@2.1.0: {} + serialize-javascript@6.0.0: + dependencies: + randombytes: 2.1.0 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -18021,6 +18176,13 @@ snapshots: optionalDependencies: tsconfig-paths: 3.15.0 + ts-mocha@10.1.0(mocha@9.2.2): + dependencies: + mocha: 9.2.2 + ts-node: 7.0.1 + optionalDependencies: + tsconfig-paths: 3.15.0 + ts-mocha@11.1.0(mocha@11.5.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))(tsconfig-paths@4.2.0): dependencies: mocha: 11.5.0 @@ -18238,6 +18400,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript@4.9.5: {} + typescript@5.5.3: {} typescript@5.6.2: {} @@ -18554,6 +18718,8 @@ snapshots: wordwrap@1.0.0: {} + workerpool@6.2.0: {} + workerpool@6.5.1: {} wrap-ansi@6.2.0: @@ -18618,6 +18784,8 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 + yargs-parser@20.2.4: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs index 90e1ffa4d2..5852473f29 100644 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs +++ b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs @@ -1,6 +1,8 @@ use anchor_lang::prelude::*; declare_id!("CompUser11111111111111111111111111111111111"); +pub const ADDRESS_SPACE: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); // Simple anchor program retrofitted with compressible accounts. #[program] @@ -8,7 +10,14 @@ pub mod anchor_compressible_user { use super::*; /// Creates a new user record - pub fn create_record(ctx: Context, name: String) -> Result<()> { + pub fn create_record( + ctx: Context, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_tree_index: u8, + ) -> Result<()> { let user_record = &mut ctx.accounts.user_record; user_record.owner = ctx.accounts.user.key(); @@ -42,6 +51,8 @@ pub struct CreateRecord<'info> { )] pub user_record: Account<'info, UserRecord>, pub system_program: Program<'info, System>, + // UNCHECKED. + pub rent_recipient: AccountInfo<'info>, } #[derive(Accounts)] diff --git a/program-tests/sdk-test/src/create_dynamic_pda.rs b/program-tests/sdk-test/src/create_dynamic_pda.rs index 1e3b84708d..6a5d68b952 100644 --- a/program-tests/sdk-test/src/create_dynamic_pda.rs +++ b/program-tests/sdk-test/src/create_dynamic_pda.rs @@ -25,10 +25,8 @@ pub fn create_dynamic_pda( .map_err(|_| LightSdkError::Borsh)?; let fee_payer = &accounts[0]; - // UNCHECKED: ...caller program checks this. let pda_account = &accounts[1]; - // CHECK: hardcoded rent recipient. let rent_recipient = &accounts[2]; if rent_recipient.key != &RENT_RECIPIENT { diff --git a/program-tests/sdk-test/src/decompress_dynamic_pda.rs b/program-tests/sdk-test/src/decompress_dynamic_pda.rs index df8bc5b02b..842713c830 100644 --- a/program-tests/sdk-test/src/decompress_dynamic_pda.rs +++ b/program-tests/sdk-test/src/decompress_dynamic_pda.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, - compressible::{decompress_idempotent, PdaTimingData, SLOTS_UNTIL_COMPRESSION}, + compressible::{decompress_idempotent, PdaTimingData}, cpi::{CpiAccounts, CpiAccountsConfig}, error::LightSdkError, instruction::{account_meta::CompressedAccountMeta, ValidityProof}, @@ -9,6 +9,8 @@ use light_sdk::{ }; use solana_program::account_info::AccountInfo; +pub const SLOTS_UNTIL_COMPRESSION: u64 = 100; + /// Decompresses a compressed account into a PDA idempotently. pub fn decompress_dynamic_pda( accounts: &[AccountInfo], @@ -25,27 +27,17 @@ pub fn decompress_dynamic_pda( let system_program = &accounts[3]; // Set up CPI accounts - let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - config.sol_pool_pda = false; - config.sol_compression_recipient = false; - + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); let cpi_accounts = CpiAccounts::new_with_config( fee_payer, &accounts[instruction_data.system_accounts_offset as usize..], config, ); - // Prepare account data - let account_data = MyPdaAccount { - last_written_slot: 0, - slots_until_compression: SLOTS_UNTIL_COMPRESSION, - data: instruction_data.data, - }; - let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut( &crate::ID, - &instruction_data.compressed_account_meta, - account_data, + &instruction_data.compressed_account.meta, + instruction_data.compressed_account.data, )?; // Call decompress_idempotent - this should work whether PDA exists or not @@ -134,8 +126,7 @@ pub fn decompress_multiple_dynamic_pdas( #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] pub struct DecompressToPdaInstructionData { pub proof: ValidityProof, - pub compressed_account_meta: CompressedAccountMeta, - pub data: [u8; 31], + pub compressed_account: MyCompressedAccount, pub system_accounts_offset: u8, } From bc645a6ceffdd84142cf77a39b608ec3a0acd9f6 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 6 Jul 2025 10:22:22 -0400 Subject: [PATCH 16/16] wip --- Cargo.lock | 17 ++ Cargo.toml | 1 + pnpm-lock.yaml | 175 ------------------ .../anchor-compressible-user/Anchor.toml | 19 -- .../anchor-compressible-user/Cargo.toml | 39 ++++ .../anchor-compressible-user/README.md | 59 ------ .../anchor-compressible-user => }/Xargo.toml | 0 .../anchor-compressible-user/package.json | 19 -- .../anchor-compressible-user/Cargo.toml | 24 --- .../anchor-compressible-user => }/src/lib.rs | 24 ++- .../tests/test.rs | 0 .../anchor-compressible-user/tsconfig.json | 10 - 12 files changed, 79 insertions(+), 308 deletions(-) delete mode 100644 program-tests/anchor-compressible-user/Anchor.toml create mode 100644 program-tests/anchor-compressible-user/Cargo.toml delete mode 100644 program-tests/anchor-compressible-user/README.md rename program-tests/anchor-compressible-user/{programs/anchor-compressible-user => }/Xargo.toml (100%) delete mode 100644 program-tests/anchor-compressible-user/package.json delete mode 100644 program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml rename program-tests/anchor-compressible-user/{programs/anchor-compressible-user => }/src/lib.rs (71%) rename program-tests/anchor-compressible-user/{programs/anchor-compressible-user => }/tests/test.rs (100%) delete mode 100644 program-tests/anchor-compressible-user/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 3191a8cbca..ab2e8d8107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "anchor-compressible-user" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-types", + "solana-program", + "solana-sdk", + "tokio", +] + [[package]] name = "anchor-derive-accounts" version = "0.31.1" diff --git a/Cargo.toml b/Cargo.toml index e36663b1c3..da3bde8684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "forester-utils", "forester", "sparse-merkle-tree", + "program-tests/anchor-compressible-user", ] resolver = "2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2ed104a1..6f725ba3f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -522,28 +522,6 @@ importers: program-tests: {} - program-tests/anchor-compressible-user: - dependencies: - '@coral-xyz/anchor': - specifier: ^0.29.0 - version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - devDependencies: - chai: - specifier: ^4.3.4 - version: 4.5.0 - mocha: - specifier: ^9.0.3 - version: 9.2.2 - prettier: - specifier: ^2.6.2 - version: 2.8.8 - ts-mocha: - specifier: ^10.0.0 - version: 10.1.0(mocha@9.2.2) - typescript: - specifier: ^4.3.5 - version: 4.9.5 - program-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': @@ -3351,9 +3329,6 @@ packages: resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/promise-all-settled@1.1.2': - resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3557,10 +3532,6 @@ packages: anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} - ansi-colors@4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} - engines: {node: '>=6'} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -4075,10 +4046,6 @@ packages: check-types@11.2.3: resolution: {integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==} - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4350,15 +4317,6 @@ packages: supports-color: optional: true - debug@4.3.3: - resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -4494,10 +4452,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - diff@5.0.0: - resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} - engines: {node: '>=0.3.1'} - diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -5425,10 +5379,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - glob@7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - deprecated: Glob versions prior to v9 are no longer supported - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -5495,10 +5445,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - growl@1.10.5: - resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} - engines: {node: '>=4.x'} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -6482,10 +6428,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@4.2.1: - resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} - engines: {node: '>=10'} - minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -6544,11 +6486,6 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true - mocha@9.2.2: - resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} - engines: {node: '>= 12.0.0'} - hasBin: true - mock-stdin@1.0.0: resolution: {integrity: sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==} @@ -6568,11 +6505,6 @@ packages: nanoassert@2.0.0: resolution: {integrity: sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==} - nanoid@3.3.1: - resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7144,11 +7076,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -7579,9 +7506,6 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} - serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -8195,11 +8119,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - typescript@5.5.3: resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} engines: {node: '>=14.17'} @@ -8486,9 +8405,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - workerpool@6.2.0: - resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} - workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} @@ -8576,10 +8492,6 @@ packages: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} - yargs-parser@20.2.4: - resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} - engines: {node: '>=10'} - yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -12506,8 +12418,6 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 - '@ungap/promise-all-settled@1.1.2': {} - '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-darwin-arm64@1.7.2': @@ -12682,8 +12592,6 @@ snapshots: anser@1.4.10: {} - ansi-colors@4.1.1: {} - ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -13343,18 +13251,6 @@ snapshots: check-types@11.2.3: {} - chokidar@3.5.3: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -13659,12 +13555,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.3(supports-color@8.1.1): - dependencies: - ms: 2.1.2 - optionalDependencies: - supports-color: 8.1.1 - debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 @@ -13765,8 +13655,6 @@ snapshots: diff@4.0.2: {} - diff@5.0.0: {} - diff@5.2.0: {} diff@7.0.0: {} @@ -15105,15 +14993,6 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 2.0.0 - glob@7.2.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -15211,8 +15090,6 @@ snapshots: graphemer@1.4.0: {} - growl@1.10.5: {} - has-bigints@1.0.2: {} has-bigints@1.1.0: {} @@ -16387,10 +16264,6 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimatch@4.2.1: - dependencies: - brace-expansion: 1.1.11 - minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -16476,33 +16349,6 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 - mocha@9.2.2: - dependencies: - '@ungap/promise-all-settled': 1.1.2 - ansi-colors: 4.1.1 - browser-stdout: 1.3.1 - chokidar: 3.5.3 - debug: 4.3.3(supports-color@8.1.1) - diff: 5.0.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 7.2.0 - growl: 1.10.5 - he: 1.2.0 - js-yaml: 4.1.0 - log-symbols: 4.1.0 - minimatch: 4.2.1 - ms: 2.1.3 - nanoid: 3.3.1 - serialize-javascript: 6.0.0 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - which: 2.0.2 - workerpool: 6.2.0 - yargs: 16.2.0 - yargs-parser: 20.2.4 - yargs-unparser: 2.0.0 - mock-stdin@1.0.0: {} ms@2.0.0: {} @@ -16515,8 +16361,6 @@ snapshots: nanoassert@2.0.0: {} - nanoid@3.3.1: {} - nanoid@3.3.11: {} napi-postinstall@0.2.3: {} @@ -17096,8 +16940,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier@2.8.8: {} - prettier@3.3.3: {} prettier@3.4.2: {} @@ -17631,10 +17473,6 @@ snapshots: serialize-error@2.1.0: {} - serialize-javascript@6.0.0: - dependencies: - randombytes: 2.1.0 - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -18176,13 +18014,6 @@ snapshots: optionalDependencies: tsconfig-paths: 3.15.0 - ts-mocha@10.1.0(mocha@9.2.2): - dependencies: - mocha: 9.2.2 - ts-node: 7.0.1 - optionalDependencies: - tsconfig-paths: 3.15.0 - ts-mocha@11.1.0(mocha@11.5.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))(tsconfig-paths@4.2.0): dependencies: mocha: 11.5.0 @@ -18400,8 +18231,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@4.9.5: {} - typescript@5.5.3: {} typescript@5.6.2: {} @@ -18718,8 +18547,6 @@ snapshots: wordwrap@1.0.0: {} - workerpool@6.2.0: {} - workerpool@6.5.1: {} wrap-ansi@6.2.0: @@ -18784,8 +18611,6 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 - yargs-parser@20.2.4: {} - yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/program-tests/anchor-compressible-user/Anchor.toml b/program-tests/anchor-compressible-user/Anchor.toml deleted file mode 100644 index cd3f9ab2ed..0000000000 --- a/program-tests/anchor-compressible-user/Anchor.toml +++ /dev/null @@ -1,19 +0,0 @@ -[features] -resolution = true -skip-lint = false - -[programs.localnet] -anchor_compressible_user = "CompUser11111111111111111111111111111111111" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "~/.config/solana/id.json" - -[scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" - -[test] -startup_wait = 5000 \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/Cargo.toml b/program-tests/anchor-compressible-user/Cargo.toml new file mode 100644 index 0000000000..564f2d6d26 --- /dev/null +++ b/program-tests/anchor-compressible-user/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "anchor-compressible-user" +version = "0.1.0" +description = "Simple Anchor program template with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "anchor_compressible_user" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + + +[dependencies] +light-sdk = { workspace = true } +light-sdk-types = { workspace = true } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"] } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +anchor-lang = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +tokio = { workspace = true } +solana-sdk = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/program-tests/anchor-compressible-user/README.md b/program-tests/anchor-compressible-user/README.md deleted file mode 100644 index bedb8b3271..0000000000 --- a/program-tests/anchor-compressible-user/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Simple Anchor User Records Template - -A basic Anchor program template demonstrating a simple user record system with create and update functionality. - -## Overview - -This is a minimal template showing: - -- Creating user records as PDAs (Program Derived Addresses) -- Updating existing user records -- Basic ownership validation - -## Account Structure - -```rust -#[account] -pub struct UserRecord { - pub owner: Pubkey, // The user who owns this record - pub name: String, // User's name - pub score: u64, // User's score -} -``` - -## Instructions - -### Create Record - -- Creates a new user record PDA -- Seeds: `[b"user_record", user_pubkey]` -- Initializes with name and score of 0 - -### Update Record - -- Updates an existing user record -- Validates ownership before allowing updates -- Can update both name and score - -## Usage - -```bash -# Build -anchor build - -# Test -anchor test -``` - -## PDA Derivation - -User records are stored at deterministic addresses: - -```rust -let (user_record_pda, bump) = Pubkey::find_program_address( - &[b"user_record", user.key().as_ref()], - &program_id, -); -``` - -This ensures each user can only have one record. diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml b/program-tests/anchor-compressible-user/Xargo.toml similarity index 100% rename from program-tests/anchor-compressible-user/programs/anchor-compressible-user/Xargo.toml rename to program-tests/anchor-compressible-user/Xargo.toml diff --git a/program-tests/anchor-compressible-user/package.json b/program-tests/anchor-compressible-user/package.json deleted file mode 100644 index ca054f09d4..0000000000 --- a/program-tests/anchor-compressible-user/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "anchor-compressible-user", - "version": "1.0.0", - "description": "Anchor program demonstrating compressible accounts", - "scripts": { - "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", - "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" - }, - "dependencies": { - "@coral-xyz/anchor": "^0.29.0" - }, - "devDependencies": { - "chai": "^4.3.4", - "mocha": "^9.0.3", - "prettier": "^2.6.2", - "ts-mocha": "^10.0.0", - "typescript": "^4.3.5" - } -} \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml b/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml deleted file mode 100644 index 17711594d7..0000000000 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "anchor-compressible-user" -version = "0.1.0" -description = "Simple Anchor program template with user records" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "anchor_compressible_user" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] - -[dependencies] -anchor-lang = { workspace = true } -light-sdk = { workspace = true } - -[dev-dependencies] -solana-program-test = { workspace = true } -solana-sdk = { workspace = true } \ No newline at end of file diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs b/program-tests/anchor-compressible-user/src/lib.rs similarity index 71% rename from program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs rename to program-tests/anchor-compressible-user/src/lib.rs index 5852473f29..5374a94e4e 100644 --- a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/src/lib.rs +++ b/program-tests/anchor-compressible-user/src/lib.rs @@ -16,7 +16,7 @@ pub mod anchor_compressible_user { proof: ValidityProof, compressed_address: [u8; 32], address_tree_info: PackedAddressTreeInfo, - output_tree_index: u8, + output_state_tree_index: u8, ) -> Result<()> { let user_record = &mut ctx.accounts.user_record; @@ -24,9 +24,29 @@ pub mod anchor_compressible_user { user_record.name = name; user_record.score = 0; + let cpi_accounts = CpiAccounts::new_with_config( + &ctx.accounts.user, // fee_payer + &ctx.remaining_accounts[..], + CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER), + ); + let new_address_params = + address_tree_info.into_new_address_params_packed(user_record.key.to_bytes()); + + compress_pda_new::( + &user_record, + compressed_address, + new_address_params, + output_state_tree_index, + proof, + cpi_accounts, + &crate::ID, + &ctx.accounts.rent_recipient, + &ADDRESS_SPACE, + )?; Ok(()) } + /// Can be the same because the PDA will be decompressed in a separate instruction. /// Updates an existing user record pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { let user_record = &mut ctx.accounts.user_record; @@ -51,7 +71,7 @@ pub struct CreateRecord<'info> { )] pub user_record: Account<'info, UserRecord>, pub system_program: Program<'info, System>, - // UNCHECKED. + #[account(address = RENT_RECIPIENT)] pub rent_recipient: AccountInfo<'info>, } diff --git a/program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs b/program-tests/anchor-compressible-user/tests/test.rs similarity index 100% rename from program-tests/anchor-compressible-user/programs/anchor-compressible-user/tests/test.rs rename to program-tests/anchor-compressible-user/tests/test.rs diff --git a/program-tests/anchor-compressible-user/tsconfig.json b/program-tests/anchor-compressible-user/tsconfig.json deleted file mode 100644 index 6f1d764179..0000000000 --- a/program-tests/anchor-compressible-user/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "types": ["mocha", "chai"], - "typeRoots": ["./node_modules/@types"], - "lib": ["es2015"], - "module": "commonjs", - "target": "es6", - "esModuleInterop": true - } -} \ No newline at end of file