From b9fd1394bca30740cff8089fbee98f0ffaa961e1 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 19 Feb 2026 18:38:29 +0000 Subject: [PATCH 1/4] test: add basic unit tests for light pda compression and decompression Entire-Checkpoint: 90f73237f4da --- Cargo.lock | 1 + sdk-libs/sdk-types/Cargo.toml | 3 + .../program/compression/processor.rs | 68 +++- .../program/decompression/processor.rs | 76 +++- sdk-libs/sdk-types/tests/common/mod.rs | 44 +++ .../sdk-types/tests/compress_processor.rs | 349 ++++++++++++++++++ .../sdk-types/tests/decompress_processor.rs | 307 +++++++++++++++ 7 files changed, 808 insertions(+), 40 deletions(-) create mode 100644 sdk-libs/sdk-types/tests/common/mod.rs create mode 100644 sdk-libs/sdk-types/tests/compress_processor.rs create mode 100644 sdk-libs/sdk-types/tests/decompress_processor.rs diff --git a/Cargo.lock b/Cargo.lock index af5725380a..cb4f0a6303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4365,6 +4365,7 @@ dependencies = [ "light-hasher", "light-macros", "light-token-interface", + "solana-account-info", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml index 32a4c0da33..5a2825892e 100644 --- a/sdk-libs/sdk-types/Cargo.toml +++ b/sdk-libs/sdk-types/Cargo.toml @@ -39,6 +39,9 @@ thiserror = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true } +solana-account-info = { workspace = true } +light-account-checks = { workspace = true, features = ["solana", "test-only"] } +light-compressed-account = { workspace = true, features = ["keccak"] } [lints.rust.unexpected_cfgs] level = "allow" diff --git a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs index 20018c1de3..0a51d42e5e 100644 --- a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs @@ -58,22 +58,22 @@ pub type CompressDispatchFn = fn( ctx: &mut CompressCtx<'_, AI>, ) -> Result<(), LightSdkTypesError>; -/// Process compress-and-close for PDA accounts (idempotent). -/// -/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`, -/// then invokes the Light system program CPI to commit compressed state, -/// and closes the PDA accounts (transferring lamports to rent_sponsor). -/// -/// Idempotent: if any account is not yet compressible (rent function check fails), -/// the entire batch is silently skipped. -#[inline(never)] -pub fn process_compress_pda_accounts_idempotent( - remaining_accounts: &[AI], +/// Result of building CPI data for compress-and-close. +pub struct CompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo, + pub cpi_accounts: CpiAccounts<'a, AI>, + pub pda_indices_to_close: Vec, +} + +/// Validates accounts and builds CPI data for compress-and-close. +/// Returns None when any account is non-compressible (idempotent skip, no CPI needed). +pub fn build_compress_pda_cpi_data<'a, AI: AccountInfoTrait + Clone>( + remaining_accounts: &'a [AI], params: &CompressAndCloseParams, dispatch_fn: CompressDispatchFn, cpi_signer: CpiSigner, program_id: &[u8; 32], -) -> Result<(), LightSdkTypesError> { +) -> Result>, LightSdkTypesError> { let system_accounts_offset = params.system_accounts_offset as usize; let num_pdas = params.compressed_accounts.len(); @@ -81,6 +81,10 @@ pub fn process_compress_pda_accounts_idempotent( return Err(LightSdkTypesError::InvalidInstructionData); } + if system_accounts_offset > remaining_accounts.len() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + // 2. Load and validate config let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?; @@ -120,7 +124,7 @@ pub fn process_compress_pda_accounts_idempotent( // 6. Idempotent: if any account is not yet compressible, skip entire batch if has_non_compressible { - return Ok(()); + return Ok(None); } // 7. Build CPI instruction data @@ -138,14 +142,38 @@ pub fn process_compress_pda_accounts_idempotent( cpi_signer, ); - // 9. Invoke Light system program CPI - cpi_ix_data.invoke::(cpi_accounts)?; + Ok(Some(CompressPdaBuilt { + cpi_ix_data, + cpi_accounts, + pda_indices_to_close, + })) +} - // 10. Close PDA accounts, transferring lamports to rent_sponsor - for pda_index in &pda_indices_to_close { - light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor) - .map_err(LightSdkTypesError::AccountError)?; +/// Process compress-and-close for PDA accounts (idempotent). +/// +/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`, +/// then invokes the Light system program CPI to commit compressed state, +/// and closes the PDA accounts (transferring lamports to rent_sponsor). +/// +/// Idempotent: if any account is not yet compressible (rent function check fails), +/// the entire batch is silently skipped. +#[inline(never)] +pub fn process_compress_pda_accounts_idempotent( + remaining_accounts: &[AI], + params: &CompressAndCloseParams, + dispatch_fn: CompressDispatchFn, + cpi_signer: CpiSigner, + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + if let Some(built) = + build_compress_pda_cpi_data(remaining_accounts, params, dispatch_fn, cpi_signer, program_id)? + { + let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; + built.cpi_ix_data.invoke::(built.cpi_accounts)?; + for pda_index in &built.pda_indices_to_close { + light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor) + .map_err(LightSdkTypesError::AccountError)?; + } } - Ok(()) } diff --git a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs index 66cc8eb141..f7e39f3f5a 100644 --- a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs @@ -133,27 +133,21 @@ pub struct DecompressCtx<'a, AI: AccountInfoTrait + Clone> { // PDA-only Processor // ============================================================================ -/// Process decompression for PDA accounts (idempotent, PDA-only). -/// -/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`, -/// then invokes the Light system program CPI to commit compressed state. -/// -/// Idempotent: if a PDA is already initialized, it is silently skipped. -/// -/// # Account layout in remaining_accounts: -/// - `[0]`: fee_payer (Signer, mut) -/// - `[1]`: config (LightConfig PDA) -/// - `[2]`: rent_sponsor (mut) -/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts -/// - `[hot_accounts_start..]`: PDA accounts to decompress into -#[inline(never)] -pub fn process_decompress_pda_accounts_idempotent( - remaining_accounts: &[AI], +/// Result of building CPI data for PDA decompression. +pub struct DecompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo, + pub cpi_accounts: CpiAccounts<'a, AI>, +} + +/// Validates accounts and builds CPI data for PDA decompression. +/// Returns None when all accounts already initialized (idempotent skip, no CPI needed). +pub fn build_decompress_pda_cpi_data<'a, AI, V>( + remaining_accounts: &'a [AI], params: &DecompressIdempotentParams, cpi_signer: CpiSigner, program_id: &[u8; 32], current_slot: u64, -) -> Result<(), LightSdkTypesError> +) -> Result>, LightSdkTypesError> where AI: AccountInfoTrait + Clone, V: DecompressVariant, @@ -234,18 +228,57 @@ where // 6. If no compressed accounts were produced (all already initialized), skip CPI if compressed_account_infos.is_empty() { - return Ok(()); + return Ok(None); } - // 7. Build and invoke Light system program CPI + // 7. Build CPI instruction data let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new( program_id.into(), cpi_signer.bump, params.proof.into(), ); cpi_ix_data.account_infos = compressed_account_infos; - cpi_ix_data.invoke::(cpi_accounts)?; + Ok(Some(DecompressPdaBuilt { + cpi_ix_data, + cpi_accounts, + })) +} + +/// Process decompression for PDA accounts (idempotent, PDA-only). +/// +/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`, +/// then invokes the Light system program CPI to commit compressed state. +/// +/// Idempotent: if a PDA is already initialized, it is silently skipped. +/// +/// # Account layout in remaining_accounts: +/// - `[0]`: fee_payer (Signer, mut) +/// - `[1]`: config (LightConfig PDA) +/// - `[2]`: rent_sponsor (mut) +/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts +/// - `[hot_accounts_start..]`: PDA accounts to decompress into +#[inline(never)] +pub fn process_decompress_pda_accounts_idempotent( + remaining_accounts: &[AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + if let Some(built) = build_decompress_pda_cpi_data( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + )? { + built.cpi_ix_data.invoke::(built.cpi_accounts)?; + } Ok(()) } @@ -283,6 +316,8 @@ where AI: AccountInfoTrait + Clone, V: DecompressVariant, { + // TODO: extract into testable setup function and add a randomized unit test + // - start context setup let system_accounts_offset = params.system_accounts_offset as usize; if system_accounts_offset > remaining_accounts.len() { return Err(LightSdkTypesError::InvalidInstructionData); @@ -387,6 +422,7 @@ where decompress_ctx.token_seeds, ) }; + // - end context setup // 7. PDA CPI (Light system program) if has_pda_accounts { diff --git a/sdk-libs/sdk-types/tests/common/mod.rs b/sdk-libs/sdk-types/tests/common/mod.rs new file mode 100644 index 0000000000..dd7809c86c --- /dev/null +++ b/sdk-libs/sdk-types/tests/common/mod.rs @@ -0,0 +1,44 @@ +use borsh::BorshSerialize; +use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use light_account_checks::discriminator::Discriminator; +use light_compressible::rent::RentConfig; +use light_sdk_types::interface::program::config::{LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED}; +use solana_pubkey::Pubkey; + +/// Creates a fully valid LightConfig TestAccount for the given program_id. +/// Returns (config_account, rent_sponsor_key). +pub fn make_config_account(program_id: [u8; 32]) -> (TestAccount, [u8; 32]) { + let prog = Pubkey::from(program_id); + + let config_bump_u16 = 0u16; + let (config_pda, bump) = + Pubkey::find_program_address(&[LIGHT_CONFIG_SEED, &config_bump_u16.to_le_bytes()], &prog); + let (rent_sponsor_pda, rent_sponsor_bump) = + Pubkey::find_program_address(&[RENT_SPONSOR_SEED], &prog); + + let config = LightConfig { + version: 1, + write_top_up: 1000, + update_authority: [1u8; 32], + rent_sponsor: rent_sponsor_pda.to_bytes(), + compression_authority: [2u8; 32], + rent_config: RentConfig::default(), + config_bump: 0, + bump, + rent_sponsor_bump, + address_space: vec![[3u8; 32]], + }; + + let mut data = LightConfig::LIGHT_DISCRIMINATOR.to_vec(); + config.serialize(&mut data).unwrap(); + + let mut account = TestAccount::new(config_pda, Pubkey::from(program_id), data.len()); + account.data = data; + + (account, rent_sponsor_pda.to_bytes()) +} + +/// Creates a dummy writable TestAccount with the given key, owner, and data size. +pub fn make_dummy_account(key: [u8; 32], owner: [u8; 32], size: usize) -> TestAccount { + TestAccount::new(Pubkey::from(key), Pubkey::from(owner), size) +} diff --git a/sdk-libs/sdk-types/tests/compress_processor.rs b/sdk-libs/sdk-types/tests/compress_processor.rs new file mode 100644 index 0000000000..508ebd0e2f --- /dev/null +++ b/sdk-libs/sdk-types/tests/compress_processor.rs @@ -0,0 +1,349 @@ +mod common; + +use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + interface::program::compression::processor::{ + build_compress_pda_cpi_data, CompressAndCloseParams, CompressCtx, + }, + CpiSigner, +}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +use common::{make_config_account, make_dummy_account}; + +// ============================================================================ +// Mock dispatch functions +// ============================================================================ + +fn mock_dispatch_compressible<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + pda_index: usize, + ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(CompressedAccountInfo { + address: None, + input: None, + output: None, + }); + ctx.pda_indices_to_close.push(pda_index); + Ok(()) +} + +fn mock_dispatch_non_compressible<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + _pda_index: usize, + ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + ctx.has_non_compressible = true; + Ok(()) +} + +// ============================================================================ +// Helper: build the standard 5-account layout for valid tests +// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account +// ============================================================================ + +fn make_valid_accounts( + program_id: [u8; 32], +) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + (fee_payer, config_account, rent_sponsor, system_account, pda_account) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // system_accounts_offset = 100 > remaining_accounts.len() = 3 -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 100, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_empty_compressed_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_not_enough_remaining_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // Only 3 accounts, but 10 compressed_accounts requested -> checked_sub underflows + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default(); 10], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config account owner to a wrong value + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config data with zeros (wrong discriminator) + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override rent_sponsor key to a value that doesn't match config.rent_sponsor + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); +} + +#[test] +fn test_idempotent_returns_none_when_non_compressible() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_non_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_build_compress_produces_expected_instruction_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda + // num_pdas=1, pda_start = 5 - 1 = 4 -> pda_index = 4 + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + let expected_cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: 255, + invoking_program_id: program_id.into(), + account_infos: vec![CompressedAccountInfo { + address: None, + input: None, + output: None, + }], + proof: None, + ..Default::default() + }; + + let built = result.unwrap().unwrap(); + assert_eq!(built.cpi_ix_data, expected_cpi_ix_data); + // pda_start = 5 - 1 = 4 + assert_eq!(built.pda_indices_to_close, vec![4usize]); +} diff --git a/sdk-libs/sdk-types/tests/decompress_processor.rs b/sdk-libs/sdk-types/tests/decompress_processor.rs new file mode 100644 index 0000000000..fc99562cef --- /dev/null +++ b/sdk-libs/sdk-types/tests/decompress_processor.rs @@ -0,0 +1,307 @@ +mod common; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::compression_info::CompressedAccountData, + program::decompression::processor::{ + build_decompress_pda_cpi_data, DecompressCtx, DecompressIdempotentParams, + DecompressVariant, + }, + }, + CpiSigner, +}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +use common::{make_config_account, make_dummy_account}; + +// ============================================================================ +// Mock DecompressVariant implementations +// ============================================================================ + +/// Simulates an already-initialized PDA: pushes nothing to compressed_account_infos. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct SkipVariant; + +impl<'info> DecompressVariant> for SkipVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + // Don't push anything -> idempotent skip (all already initialized) + Ok(()) + } +} + +/// Pushes one known CompressedAccountInfo to simulate decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct DecompressVariantMock; + +impl<'info> DecompressVariant> for DecompressVariantMock { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(CompressedAccountInfo { + address: None, + input: None, + output: None, + }); + Ok(()) + } +} + +// ============================================================================ +// Helper: build the standard account list for valid tests +// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account +// ============================================================================ + +fn make_valid_accounts( + program_id: [u8; 32], +) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + (fee_payer, config_account, rent_sponsor, system_account, pda_account) +} + +fn one_pda_params( + data: V, + system_accounts_offset: u8, +) -> DecompressIdempotentParams { + DecompressIdempotentParams { + system_accounts_offset, + token_accounts_offset: 1, // 1 PDA account, 0 token accounts + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data, + }], + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // system_accounts_offset = 100 > remaining_accounts.len() = 3 -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 100, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_empty_pda_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // token_accounts_offset = 0 -> pda_accounts = accounts[0..0] = [] -> empty -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_not_enough_hot_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // Only 3 remaining_accounts, 5 accounts in params -> checked_sub(5) on len=3 fails + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 5, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + ], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config account owner to a wrong value + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override rent_sponsor key to a value that doesn't match config.rent_sponsor + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); +} + +#[test] +fn test_idempotent_returns_none_when_all_initialized() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + // SkipVariant pushes nothing -> compressed_account_infos.is_empty() -> Ok(None) + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_build_decompress_produces_expected_instruction_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + // DecompressVariantMock pushes one CompressedAccountInfo -> CPI data built + let params = one_pda_params(DecompressVariantMock, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + let expected_cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: 255, + invoking_program_id: program_id.into(), + account_infos: vec![CompressedAccountInfo { + address: None, + input: None, + output: None, + }], + proof: None, + ..Default::default() + }; + + let built = result.unwrap().unwrap(); + assert_eq!(built.cpi_ix_data, expected_cpi_ix_data); +} From 982b50cc6c7550f508875d55eec0c655400a83b1 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 19 Feb 2026 19:22:05 +0000 Subject: [PATCH 2/4] test: randomized decompress Entire-Checkpoint: 34ad2a47e6b5 --- Cargo.lock | 1 + sdk-libs/sdk-types/Cargo.toml | 1 + .../program/decompression/processor.rs | 103 ++- .../tests/decompress_accounts_processor.rs | 674 ++++++++++++++++++ 4 files changed, 756 insertions(+), 23 deletions(-) create mode 100644 sdk-libs/sdk-types/tests/decompress_accounts_processor.rs diff --git a/Cargo.lock b/Cargo.lock index cb4f0a6303..4412efe2ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4365,6 +4365,7 @@ dependencies = [ "light-hasher", "light-macros", "light-token-interface", + "rand 0.8.5", "solana-account-info", "solana-msg 2.2.1", "solana-program-error 2.2.2", diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml index 5a2825892e..bf3ee4da20 100644 --- a/sdk-libs/sdk-types/Cargo.toml +++ b/sdk-libs/sdk-types/Cargo.toml @@ -42,6 +42,7 @@ solana-pubkey = { workspace = true } solana-account-info = { workspace = true } light-account-checks = { workspace = true, features = ["solana", "test-only"] } light-compressed-account = { workspace = true, features = ["keccak"] } +rand = { workspace = true } [lints.rust.unexpected_cfgs] level = "allow" diff --git a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs index f7e39f3f5a..2fc7cfc811 100644 --- a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs @@ -286,38 +286,36 @@ where // Full Processor (PDA + Token) // ============================================================================ -/// Process decompression for both PDA and token accounts (idempotent). -/// -/// Handles the combined PDA + token decompression flow: -/// - PDA accounts are decompressed first -/// - If both PDAs and tokens exist, PDA data is written to CPI context first -/// - Token accounts are decompressed via Transfer2 CPI to the light token program +/// Result of building CPI data for the combined PDA + token decompression. +#[cfg(feature = "token")] +pub struct DecompressAccountsBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_accounts: CpiAccounts<'a, AI>, + pub compressed_account_infos: Vec, + pub has_pda_accounts: bool, + pub has_token_accounts: bool, + pub cpi_context: bool, + pub in_token_data: Vec, + pub in_tlv: Option>>, + pub token_seeds: Vec>, +} + +/// Validates accounts, dispatches all variants, and collects CPI inputs for +/// the combined PDA + token decompression. /// -/// # Account layout in remaining_accounts: -/// - `[0]`: fee_payer (Signer, mut) -/// - `[1]`: config (LightConfig PDA) -/// - `[2]`: rent_sponsor (mut) -/// - `[3]`: ctoken_rent_sponsor (mut) -/// - `[4]`: light_token_program -/// - `[5]`: cpi_authority -/// - `[6]`: ctoken_compressible_config -/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts -/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens) +/// Returns the assembled [`DecompressAccountsBuilt`] on success. +/// The caller is responsible for executing the actual CPIs. #[cfg(feature = "token")] -#[inline(never)] -pub fn process_decompress_accounts_idempotent( - remaining_accounts: &[AI], +pub fn build_decompress_accounts_cpi_data<'a, AI, V>( + remaining_accounts: &'a [AI], params: &DecompressIdempotentParams, cpi_signer: CpiSigner, program_id: &[u8; 32], current_slot: u64, -) -> Result<(), LightSdkTypesError> +) -> Result, LightSdkTypesError> where AI: AccountInfoTrait + Clone, V: DecompressVariant, { - // TODO: extract into testable setup function and add a randomized unit test - // - start context setup let system_accounts_offset = params.system_accounts_offset as usize; if system_accounts_offset > remaining_accounts.len() { return Err(LightSdkTypesError::InvalidInstructionData); @@ -422,7 +420,66 @@ where decompress_ctx.token_seeds, ) }; - // - end context setup + + Ok(DecompressAccountsBuilt { + cpi_accounts, + compressed_account_infos, + has_pda_accounts, + has_token_accounts, + cpi_context, + in_token_data, + in_tlv, + token_seeds, + }) +} + +/// Process decompression for both PDA and token accounts (idempotent). +/// +/// Handles the combined PDA + token decompression flow: +/// - PDA accounts are decompressed first +/// - If both PDAs and tokens exist, PDA data is written to CPI context first +/// - Token accounts are decompressed via Transfer2 CPI to the light token program +/// +/// # Account layout in remaining_accounts: +/// - `[0]`: fee_payer (Signer, mut) +/// - `[1]`: config (LightConfig PDA) +/// - `[2]`: rent_sponsor (mut) +/// - `[3]`: ctoken_rent_sponsor (mut) +/// - `[4]`: light_token_program +/// - `[5]`: cpi_authority +/// - `[6]`: ctoken_compressible_config +/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts +/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens) +#[cfg(feature = "token")] +#[inline(never)] +pub fn process_decompress_accounts_idempotent( + remaining_accounts: &[AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + let DecompressAccountsBuilt { + cpi_accounts, + compressed_account_infos, + has_pda_accounts, + has_token_accounts, + cpi_context, + in_token_data, + in_tlv, + token_seeds, + } = build_decompress_accounts_cpi_data( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + )?; + let system_accounts_offset = params.system_accounts_offset as usize; // 7. PDA CPI (Light system program) if has_pda_accounts { diff --git a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs new file mode 100644 index 0000000000..2ec617050a --- /dev/null +++ b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs @@ -0,0 +1,674 @@ +#![cfg(feature = "token")] + +mod common; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use light_compressed_account::{ + compressed_account::PackedMerkleContext, + instruction_data::{compressed_proof::ValidityProof, with_account_info::CompressedAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::compression_info::CompressedAccountData, + program::decompression::processor::{ + build_decompress_accounts_cpi_data, DecompressCtx, DecompressIdempotentParams, + DecompressVariant, + }, + }, + CpiSigner, +}; +use light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext; +use rand::Rng; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +use common::{make_config_account, make_dummy_account}; + +// ============================================================================ +// Mock DecompressVariant implementations +// ============================================================================ + +/// Pushes nothing — simulates an already-initialized PDA. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct SkipVariant; + +impl<'info> DecompressVariant> for SkipVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + Ok(()) + } +} + +/// Carries and pushes a specific CompressedAccountInfo — simulates PDA decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct PdaMockVariant(CompressedAccountInfo); + +impl<'info> DecompressVariant> for PdaMockVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(self.0.clone()); + Ok(()) + } +} + +/// Carries and pushes a specific MultiInputTokenDataWithContext — simulates token decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct TokenMockVariant(MultiInputTokenDataWithContext); + +impl<'info> DecompressVariant> for TokenMockVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.in_token_data.push(self.0); + Ok(()) + } +} + +/// Unified enum for tests that mix PDA and token accounts in one params.accounts Vec. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +enum MockVariant { + Pda(PdaMockVariant), + Token(TokenMockVariant), +} + +impl<'info> DecompressVariant> for MockVariant { + fn decompress( + &self, + meta: &PackedStateTreeInfo, + pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + match self { + MockVariant::Pda(v) => v.decompress(meta, pda_account, ctx), + MockVariant::Token(v) => v.decompress(meta, pda_account, ctx), + } + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Standard 5-account layout for PDA-only error/happy path tests. +/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account +fn make_valid_accounts( + program_id: [u8; 32], +) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + (fee_payer, config_account, rent_sponsor, system_account, pda_account) +} + +/// 9-account layout for PDA+token tests. +/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, +/// [3]=ctoken_rent_sponsor, [4..6]=dummies, [6]=ctoken_compressible_config, +/// (system_accounts_offset=7, no system accounts between 7 and hot_accounts_start) +/// [7]=pda_account, [8]=token_account +fn make_valid_accounts_with_tokens( + program_id: [u8; 32], +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + let token_account = make_dummy_account([20u8; 32], program_id, 100); + ( + fee_payer, + config_account, + rent_sponsor, + ctoken_rent_sponsor, + dummy4, + dummy5, + ctoken_config, + pda_account, + token_account, + ) +} + +fn one_pda_params( + data: V, + system_accounts_offset: u8, +) -> DecompressIdempotentParams { + DecompressIdempotentParams { + system_accounts_offset, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data, + }], + } +} + +// ============================================================================ +// Error path tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 100, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_bad_token_accounts_offset_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // token_accounts_offset=99 > accounts.len()=1 -> split_at_checked returns None + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 99, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); +} + +#[test] +fn test_not_enough_hot_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // accounts.len()=5, remaining_accounts.len()=3 -> checked_sub(5) underflows + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 5, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }; + 5 + ], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); +} + +// ============================================================================ +// Happy path tests +// ============================================================================ + +#[test] +fn test_pda_only_builds_correct_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + let mut pda_account = make_dummy_account([10u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=pda (hot) + // system_accounts_offset=3, token_accounts_offset=1 (=accounts.len() -> no tokens) + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, pda_ai]; + let params = one_pda_params( + PdaMockVariant(CompressedAccountInfo { address: None, input: None, output: None }), + 3, + ); + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(built.has_pda_accounts); + assert!(!built.has_token_accounts); + assert!(!built.cpi_context); + assert_eq!( + built.compressed_account_infos, + vec![CompressedAccountInfo { address: None, input: None, output: None }] + ); + assert_eq!(built.in_token_data, Vec::::new()); +} + +#[test] +fn test_token_only_builds_correct_data() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + _, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let token_ai = token_account.get_account_info(); + + // [0..6] fixed, [7]=token (hot); system_accounts_offset=7, no system accounts + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + token_ai, + ]; + // token_accounts_offset=0: all accounts are tokens, no PDAs + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: TokenMockVariant(MultiInputTokenDataWithContext::default()), + }], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(!built.has_pda_accounts); + assert!(built.has_token_accounts); + assert!(!built.cpi_context); + assert_eq!(built.compressed_account_infos, Vec::::new()); + assert_eq!(built.in_token_data, vec![MultiInputTokenDataWithContext::default()]); +} + +#[test] +fn test_pda_and_token_sets_cpi_context() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + // [0..6] fixed, [7]=pda, [8]=token (hot); system_accounts_offset=7 + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + pda_ai, + token_ai, + ]; + // token_accounts_offset=1: accounts[0]=PDA, accounts[1]=token + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + })), + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant( + MultiInputTokenDataWithContext::default(), + )), + }, + ], + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(built.has_pda_accounts); + assert!(built.has_token_accounts); + assert!(built.cpi_context); + assert_eq!( + built.compressed_account_infos, + vec![CompressedAccountInfo { address: None, input: None, output: None }] + ); + assert_eq!(built.in_token_data, vec![MultiInputTokenDataWithContext::default()]); +} + +#[test] +fn test_randomized_pda_and_token_decompression() { + let program_id = [42u8; 32]; + let mut rng = rand::thread_rng(); + + // Pick random counts in 0..=5, ensuring at least one account total. + let mut n_pdas: usize = rng.gen_range(0..=5); + let mut n_tokens: usize = rng.gen_range(0..=5); + if n_pdas + n_tokens == 0 { + // Clamp: give at least one of each type. + n_pdas = 1; + n_tokens = 1; + } + + // Build expected PDA infos with random addresses. + let expected_pda_infos: Vec = (0..n_pdas) + .map(|_| CompressedAccountInfo { + address: Some(rng.gen::<[u8; 32]>()), + input: None, + output: None, + }) + .collect(); + + // Build expected token data with random fields. + let expected_token_data: Vec = (0..n_tokens) + .map(|_| MultiInputTokenDataWithContext { + owner: rng.gen::(), + amount: rng.gen(), + has_delegate: rng.gen(), + delegate: rng.gen::(), + mint: rng.gen::(), + version: rng.gen::(), + root_index: rng.gen(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: rng.gen(), + queue_pubkey_index: rng.gen(), + leaf_index: rng.gen(), + prove_by_index: rng.gen(), + }, + }) + .collect(); + + // Build accounts: 7-account header + n_pdas PDAs + n_tokens token accounts. + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + let mut pda_accounts: Vec = (0..n_pdas) + .map(|i| make_dummy_account([(10 + i) as u8; 32], program_id, 100)) + .collect(); + let mut token_accounts: Vec = (0..n_tokens) + .map(|i| make_dummy_account([(20 + i) as u8; 32], program_id, 100)) + .collect(); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let mut pda_ais: Vec> = + pda_accounts.iter_mut().map(|a| a.get_account_info()).collect(); + let mut token_ais: Vec> = + token_accounts.iter_mut().map(|a| a.get_account_info()).collect(); + + let mut remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + ]; + remaining_accounts.append(&mut pda_ais); + remaining_accounts.append(&mut token_ais); + + // Build params.accounts: PDAs first, then tokens. + let mut accounts: Vec> = Vec::new(); + for info in &expected_pda_infos { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(info.clone())), + }); + } + for token in &expected_token_data { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant(*token)), + }); + } + + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: n_pdas as u8, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts, + }; + let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert_eq!( + built.compressed_account_infos, expected_pda_infos, + "n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.in_token_data, expected_token_data, + "n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_pda_accounts, + n_pdas > 0, + "n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_token_accounts, + n_tokens > 0, + "n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.cpi_context, + n_pdas > 0 && n_tokens > 0, + "n_pdas={n_pdas} n_tokens={n_tokens}" + ); +} From d32af50d7276922c065bda0a599da8c1ba2bf0a9 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 19 Feb 2026 19:37:46 +0000 Subject: [PATCH 3/4] format --- .../src/v3/actions/create-mint-interface.ts | 4 +- .../src/v3/get-mint-interface.ts | 4 +- .../program/compression/processor.rs | 10 +- sdk-libs/sdk-types/tests/common/mod.rs | 9 +- .../sdk-types/tests/compress_processor.rs | 97 +++++++++--- .../tests/decompress_accounts_processor.rs | 148 ++++++++++++++---- .../sdk-types/tests/decompress_processor.rs | 111 ++++++++++--- 7 files changed, 303 insertions(+), 80 deletions(-) diff --git a/js/compressed-token/src/v3/actions/create-mint-interface.ts b/js/compressed-token/src/v3/actions/create-mint-interface.ts index 757c38a6b7..f57f7bbd40 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -81,9 +81,7 @@ export async function createMintInterface( // Default: light-token mint creation if (!('secretKey' in mintAuthority)) { - throw new Error( - 'mintAuthority must be a Signer for light-token mints', - ); + throw new Error('mintAuthority must be a Signer for light-token mints'); } if ( addressTreeInfo && diff --git a/js/compressed-token/src/v3/get-mint-interface.ts b/js/compressed-token/src/v3/get-mint-interface.ts index aeffe71611..3d5bf10033 100644 --- a/js/compressed-token/src/v3/get-mint-interface.ts +++ b/js/compressed-token/src/v3/get-mint-interface.ts @@ -103,9 +103,7 @@ export async function getMintInterface( ); if (!compressedAccount?.data?.data) { - throw new Error( - `Light mint not found for ${address.toString()}`, - ); + throw new Error(`Light mint not found for ${address.toString()}`); } const compressedData = Buffer.from(compressedAccount.data.data); diff --git a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs index 0a51d42e5e..0597b2e01f 100644 --- a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs @@ -165,9 +165,13 @@ pub fn process_compress_pda_accounts_idempotent( cpi_signer: CpiSigner, program_id: &[u8; 32], ) -> Result<(), LightSdkTypesError> { - if let Some(built) = - build_compress_pda_cpi_data(remaining_accounts, params, dispatch_fn, cpi_signer, program_id)? - { + if let Some(built) = build_compress_pda_cpi_data( + remaining_accounts, + params, + dispatch_fn, + cpi_signer, + program_id, + )? { let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; built.cpi_ix_data.invoke::(built.cpi_accounts)?; for pda_index in &built.pda_indices_to_close { diff --git a/sdk-libs/sdk-types/tests/common/mod.rs b/sdk-libs/sdk-types/tests/common/mod.rs index dd7809c86c..616bd8d4e7 100644 --- a/sdk-libs/sdk-types/tests/common/mod.rs +++ b/sdk-libs/sdk-types/tests/common/mod.rs @@ -1,8 +1,11 @@ use borsh::BorshSerialize; -use light_account_checks::account_info::test_account_info::solana_program::TestAccount; -use light_account_checks::discriminator::Discriminator; +use light_account_checks::{ + account_info::test_account_info::solana_program::TestAccount, discriminator::Discriminator, +}; use light_compressible::rent::RentConfig; -use light_sdk_types::interface::program::config::{LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED}; +use light_sdk_types::interface::program::config::{ + LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED, +}; use solana_pubkey::Pubkey; /// Creates a fully valid LightConfig TestAccount for the given program_id. diff --git a/sdk-libs/sdk-types/tests/compress_processor.rs b/sdk-libs/sdk-types/tests/compress_processor.rs index 508ebd0e2f..7c91bd2ff8 100644 --- a/sdk-libs/sdk-types/tests/compress_processor.rs +++ b/sdk-libs/sdk-types/tests/compress_processor.rs @@ -1,5 +1,6 @@ mod common; +use common::{make_config_account, make_dummy_account}; use light_account_checks::account_info::test_account_info::solana_program::TestAccount; use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, @@ -16,8 +17,6 @@ use light_sdk_types::{ use solana_account_info::AccountInfo; use solana_pubkey::Pubkey; -use common::{make_config_account, make_dummy_account}; - // ============================================================================ // Mock dispatch functions // ============================================================================ @@ -54,13 +53,25 @@ fn mock_dispatch_non_compressible<'a>( fn make_valid_accounts( program_id: [u8; 32], -) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { let (config_account, rent_sponsor_key) = make_config_account(program_id); let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); let pda_account = make_dummy_account([10u8; 32], program_id, 100); - (fee_payer, config_account, rent_sponsor, system_account, pda_account) + ( + fee_payer, + config_account, + rent_sponsor, + system_account, + pda_account, + ) } // ============================================================================ @@ -85,7 +96,11 @@ fn test_system_offset_exceeds_accounts_returns_error() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 100, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -95,7 +110,10 @@ fn test_system_offset_exceeds_accounts_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -117,7 +135,11 @@ fn test_empty_compressed_accounts_returns_error() { compressed_accounts: vec![], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -127,7 +149,10 @@ fn test_empty_compressed_accounts_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -148,7 +173,11 @@ fn test_not_enough_remaining_accounts_returns_error() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default(); 10], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -158,7 +187,10 @@ fn test_not_enough_remaining_accounts_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); } #[test] @@ -183,7 +215,11 @@ fn test_config_wrong_owner_returns_error() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -193,7 +229,10 @@ fn test_config_wrong_owner_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); } #[test] @@ -218,7 +257,11 @@ fn test_config_wrong_discriminator_returns_error() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -228,7 +271,10 @@ fn test_config_wrong_discriminator_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); } #[test] @@ -253,7 +299,11 @@ fn test_wrong_rent_sponsor_key_returns_error() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -263,7 +313,10 @@ fn test_wrong_rent_sponsor_key_returns_error() { &program_id, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); } #[test] @@ -285,7 +338,11 @@ fn test_idempotent_returns_none_when_non_compressible() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, @@ -319,7 +376,11 @@ fn test_build_compress_produces_expected_instruction_data() { compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], system_accounts_offset: 3, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_compress_pda_cpi_data( &remaining_accounts, diff --git a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs index 2ec617050a..d6f62cfb6e 100644 --- a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs +++ b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs @@ -3,6 +3,7 @@ mod common; use borsh::{BorshDeserialize, BorshSerialize}; +use common::{make_config_account, make_dummy_account}; use light_account_checks::account_info::test_account_info::solana_program::TestAccount; use light_compressed_account::{ compressed_account::PackedMerkleContext, @@ -25,8 +26,6 @@ use rand::Rng; use solana_account_info::AccountInfo; use solana_pubkey::Pubkey; -use common::{make_config_account, make_dummy_account}; - // ============================================================================ // Mock DecompressVariant implementations // ============================================================================ @@ -107,13 +106,25 @@ impl<'info> DecompressVariant> for MockVariant { /// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account fn make_valid_accounts( program_id: [u8; 32], -) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { let (config_account, rent_sponsor_key) = make_config_account(program_id); let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); let pda_account = make_dummy_account([10u8; 32], program_id, 100); - (fee_payer, config_account, rent_sponsor, system_account, pda_account) + ( + fee_payer, + config_account, + rent_sponsor, + system_account, + pda_account, + ) } /// 9-account layout for PDA+token tests. @@ -198,7 +209,11 @@ fn test_system_offset_exceeds_accounts_returns_error() { data: SkipVariant, }], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -208,7 +223,10 @@ fn test_system_offset_exceeds_accounts_returns_error() { 0, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -235,7 +253,11 @@ fn test_bad_token_accounts_offset_returns_error() { data: SkipVariant, }], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -245,7 +267,10 @@ fn test_bad_token_accounts_offset_returns_error() { 0, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -273,7 +298,11 @@ fn test_not_enough_hot_accounts_returns_error() { 5 ], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -283,7 +312,10 @@ fn test_not_enough_hot_accounts_returns_error() { 0, ); - assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); } #[test] @@ -302,7 +334,11 @@ fn test_config_wrong_owner_returns_error() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; let params = one_pda_params(SkipVariant, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -312,7 +348,10 @@ fn test_config_wrong_owner_returns_error() { 0, ); - assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); } #[test] @@ -331,7 +370,11 @@ fn test_wrong_rent_sponsor_key_returns_error() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; let params = one_pda_params(SkipVariant, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -341,7 +384,10 @@ fn test_wrong_rent_sponsor_key_returns_error() { 0, ); - assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); } // ============================================================================ @@ -364,10 +410,18 @@ fn test_pda_only_builds_correct_data() { // system_accounts_offset=3, token_accounts_offset=1 (=accounts.len() -> no tokens) let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, pda_ai]; let params = one_pda_params( - PdaMockVariant(CompressedAccountInfo { address: None, input: None, output: None }), + PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + }), 3, ); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -383,9 +437,16 @@ fn test_pda_only_builds_correct_data() { assert!(!built.cpi_context); assert_eq!( built.compressed_account_infos, - vec![CompressedAccountInfo { address: None, input: None, output: None }] + vec![CompressedAccountInfo { + address: None, + input: None, + output: None + }] + ); + assert_eq!( + built.in_token_data, + Vec::::new() ); - assert_eq!(built.in_token_data, Vec::::new()); } #[test] @@ -434,7 +495,11 @@ fn test_token_only_builds_correct_data() { data: TokenMockVariant(MultiInputTokenDataWithContext::default()), }], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -448,8 +513,14 @@ fn test_token_only_builds_correct_data() { assert!(!built.has_pda_accounts); assert!(built.has_token_accounts); assert!(!built.cpi_context); - assert_eq!(built.compressed_account_infos, Vec::::new()); - assert_eq!(built.in_token_data, vec![MultiInputTokenDataWithContext::default()]); + assert_eq!( + built.compressed_account_infos, + Vec::::new() + ); + assert_eq!( + built.in_token_data, + vec![MultiInputTokenDataWithContext::default()] + ); } #[test] @@ -512,7 +583,11 @@ fn test_pda_and_token_sets_cpi_context() { }, ], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, @@ -528,9 +603,16 @@ fn test_pda_and_token_sets_cpi_context() { assert!(built.cpi_context); assert_eq!( built.compressed_account_infos, - vec![CompressedAccountInfo { address: None, input: None, output: None }] + vec![CompressedAccountInfo { + address: None, + input: None, + output: None + }] + ); + assert_eq!( + built.in_token_data, + vec![MultiInputTokenDataWithContext::default()] ); - assert_eq!(built.in_token_data, vec![MultiInputTokenDataWithContext::default()]); } #[test] @@ -598,10 +680,14 @@ fn test_randomized_pda_and_token_decompression() { let dummy4_ai = dummy4.get_account_info(); let dummy5_ai = dummy5.get_account_info(); let ctoken_config_ai = ctoken_config.get_account_info(); - let mut pda_ais: Vec> = - pda_accounts.iter_mut().map(|a| a.get_account_info()).collect(); - let mut token_ais: Vec> = - token_accounts.iter_mut().map(|a| a.get_account_info()).collect(); + let mut pda_ais: Vec> = pda_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); + let mut token_ais: Vec> = token_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); let mut remaining_accounts = vec![ fee_payer_ai, @@ -637,7 +723,11 @@ fn test_randomized_pda_and_token_decompression() { proof: ValidityProof::default(), accounts, }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_accounts_cpi_data( &remaining_accounts, diff --git a/sdk-libs/sdk-types/tests/decompress_processor.rs b/sdk-libs/sdk-types/tests/decompress_processor.rs index fc99562cef..7cdd84e482 100644 --- a/sdk-libs/sdk-types/tests/decompress_processor.rs +++ b/sdk-libs/sdk-types/tests/decompress_processor.rs @@ -1,6 +1,7 @@ mod common; use borsh::{BorshDeserialize, BorshSerialize}; +use common::{make_config_account, make_dummy_account}; use light_account_checks::account_info::test_account_info::solana_program::TestAccount; use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, @@ -21,8 +22,6 @@ use light_sdk_types::{ use solana_account_info::AccountInfo; use solana_pubkey::Pubkey; -use common::{make_config_account, make_dummy_account}; - // ============================================================================ // Mock DecompressVariant implementations // ============================================================================ @@ -70,13 +69,25 @@ impl<'info> DecompressVariant> for DecompressVariantMock { fn make_valid_accounts( program_id: [u8; 32], -) -> (TestAccount, TestAccount, TestAccount, TestAccount, TestAccount) { +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { let (config_account, rent_sponsor_key) = make_config_account(program_id); let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); let pda_account = make_dummy_account([10u8; 32], program_id, 100); - (fee_payer, config_account, rent_sponsor, system_account, pda_account) + ( + fee_payer, + config_account, + rent_sponsor, + system_account, + pda_account, + ) } fn one_pda_params( @@ -122,12 +133,19 @@ fn test_system_offset_exceeds_accounts_returns_error() { data: SkipVariant, }], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -153,12 +171,19 @@ fn test_empty_pda_accounts_returns_error() { data: SkipVariant, }], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); - assert!(matches!(result, Err(LightSdkTypesError::InvalidInstructionData))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); } #[test] @@ -180,19 +205,41 @@ fn test_not_enough_hot_accounts_returns_error() { output_queue_index: 0, proof: ValidityProof::default(), accounts: vec![ - CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, - CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, - CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, - CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, - CompressedAccountData { tree_info: PackedStateTreeInfo::default(), data: SkipVariant }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, ], }; - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); - assert!(matches!(result, Err(LightSdkTypesError::NotEnoughAccountKeys))); + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); } #[test] @@ -212,12 +259,19 @@ fn test_config_wrong_owner_returns_error() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; let params = one_pda_params(SkipVariant, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); - assert!(matches!(result, Err(LightSdkTypesError::ConstraintViolation))); + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); } #[test] @@ -237,12 +291,19 @@ fn test_wrong_rent_sponsor_key_returns_error() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; let params = one_pda_params(SkipVariant, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); - assert!(matches!(result, Err(LightSdkTypesError::InvalidRentSponsor))); + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); } #[test] @@ -260,7 +321,11 @@ fn test_idempotent_returns_none_when_all_initialized() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; // SkipVariant pushes nothing -> compressed_account_infos.is_empty() -> Ok(None) let params = one_pda_params(SkipVariant, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); @@ -284,7 +349,11 @@ fn test_build_decompress_produces_expected_instruction_data() { let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; // DecompressVariantMock pushes one CompressedAccountInfo -> CPI data built let params = one_pda_params(DecompressVariantMock, 3); - let cpi_signer = CpiSigner { program_id, cpi_signer: [0u8; 32], bump: 255 }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; let result = build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); From 7647b9300a81f688ff93737b67afbba8093e57b2 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 20 Feb 2026 00:46:07 +0000 Subject: [PATCH 4/4] address feedback Entire-Checkpoint: 7c9c24f71fe9 --- sdk-libs/sdk-types/tests/common/mod.rs | 53 +- .../sdk-types/tests/compress_processor.rs | 172 +++++- .../tests/decompress_accounts_processor.rs | 549 +++++++++++++----- .../sdk-types/tests/decompress_processor.rs | 121 ++-- 4 files changed, 672 insertions(+), 223 deletions(-) diff --git a/sdk-libs/sdk-types/tests/common/mod.rs b/sdk-libs/sdk-types/tests/common/mod.rs index 616bd8d4e7..6a41ea4a1d 100644 --- a/sdk-libs/sdk-types/tests/common/mod.rs +++ b/sdk-libs/sdk-types/tests/common/mod.rs @@ -1,11 +1,17 @@ -use borsh::BorshSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; use light_account_checks::{ account_info::test_account_info::solana_program::TestAccount, discriminator::Discriminator, }; use light_compressible::rent::RentConfig; -use light_sdk_types::interface::program::config::{ - LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED, +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::program::{ + config::{LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED}, + decompression::processor::{DecompressCtx, DecompressVariant}, + }, }; +use solana_account_info::AccountInfo; use solana_pubkey::Pubkey; /// Creates a fully valid LightConfig TestAccount for the given program_id. @@ -45,3 +51,44 @@ pub fn make_config_account(program_id: [u8; 32]) -> (TestAccount, [u8; 32]) { pub fn make_dummy_account(key: [u8; 32], owner: [u8; 32], size: usize) -> TestAccount { TestAccount::new(Pubkey::from(key), Pubkey::from(owner), size) } + +/// Standard 5-account layout used by compress and decompress tests. +/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account +pub fn make_valid_accounts( + program_id: [u8; 32], +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + ( + fee_payer, + config_account, + rent_sponsor, + system_account, + pda_account, + ) +} + +/// Simulates an already-initialized PDA: pushes nothing to compressed_account_infos. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +#[allow(dead_code)] +pub struct SkipVariant; + +impl<'info> DecompressVariant> for SkipVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + Ok(()) + } +} diff --git a/sdk-libs/sdk-types/tests/compress_processor.rs b/sdk-libs/sdk-types/tests/compress_processor.rs index 7c91bd2ff8..b0a749ba84 100644 --- a/sdk-libs/sdk-types/tests/compress_processor.rs +++ b/sdk-libs/sdk-types/tests/compress_processor.rs @@ -1,7 +1,6 @@ mod common; -use common::{make_config_account, make_dummy_account}; -use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use common::{make_dummy_account, make_valid_accounts}; use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, @@ -46,34 +45,6 @@ fn mock_dispatch_non_compressible<'a>( Ok(()) } -// ============================================================================ -// Helper: build the standard 5-account layout for valid tests -// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account -// ============================================================================ - -fn make_valid_accounts( - program_id: [u8; 32], -) -> ( - TestAccount, - TestAccount, - TestAccount, - TestAccount, - TestAccount, -) { - let (config_account, rent_sponsor_key) = make_config_account(program_id); - let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); - let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); - let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); - let pda_account = make_dummy_account([10u8; 32], program_id, 100); - ( - fee_payer, - config_account, - rent_sponsor, - system_account, - pda_account, - ) -} - // ============================================================================ // Tests // ============================================================================ @@ -408,3 +379,144 @@ fn test_build_compress_produces_expected_instruction_data() { // pda_start = 5 - 1 = 4 assert_eq!(built.pda_indices_to_close, vec![4usize]); } + +fn mock_dispatch_error<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + _pda_index: usize, + _ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + Err(LightSdkTypesError::ConstraintViolation) +} + +#[test] +fn test_dispatch_fn_error_propagates() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_error, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_multiple_pdas_non_compressible_skips_all() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda1) = + make_valid_accounts(program_id); + let mut pda2 = make_dummy_account([11u8; 32], program_id, 100); + let mut pda3 = make_dummy_account([12u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda1_ai = pda1.get_account_info(); + let pda2_ai = pda2.get_account_info(); + let pda3_ai = pda3.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda1, [5]=pda2, [6]=pda3 + // pda_start = 7 - 3 = 4; accounts[4],[5],[6] are the three PDAs + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + system_ai, + pda1_ai, + pda2_ai, + pda3_ai, + ]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![ + CompressedAccountMetaNoLamportsNoAddress::default(), + CompressedAccountMetaNoLamportsNoAddress::default(), + CompressedAccountMetaNoLamportsNoAddress::default(), + ], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_non_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_empty_system_accounts_slice() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + let mut pda_account = make_dummy_account([10u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=pda + // system_accounts_offset=3, pda_start = 4 - 1 = 3 = system_accounts_offset + // remaining_accounts[3..3] = empty system slice -> no panic + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(Some(_)))); +} diff --git a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs index d6f62cfb6e..3414f4c922 100644 --- a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs +++ b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs @@ -3,7 +3,7 @@ mod common; use borsh::{BorshDeserialize, BorshSerialize}; -use common::{make_config_account, make_dummy_account}; +use common::{make_config_account, make_dummy_account, make_valid_accounts, SkipVariant}; use light_account_checks::account_info::test_account_info::solana_program::TestAccount; use light_compressed_account::{ compressed_account::PackedMerkleContext, @@ -22,7 +22,7 @@ use light_sdk_types::{ CpiSigner, }; use light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext; -use rand::Rng; +use rand::{Rng, SeedableRng}; use solana_account_info::AccountInfo; use solana_pubkey::Pubkey; @@ -30,21 +30,6 @@ use solana_pubkey::Pubkey; // Mock DecompressVariant implementations // ============================================================================ -/// Pushes nothing — simulates an already-initialized PDA. -#[derive(BorshSerialize, BorshDeserialize, Clone)] -struct SkipVariant; - -impl<'info> DecompressVariant> for SkipVariant { - fn decompress( - &self, - _meta: &PackedStateTreeInfo, - _pda_account: &AccountInfo<'info>, - _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, - ) -> Result<(), LightSdkTypesError> { - Ok(()) - } -} - /// Carries and pushes a specific CompressedAccountInfo — simulates PDA decompression. #[derive(BorshSerialize, BorshDeserialize, Clone)] struct PdaMockVariant(CompressedAccountInfo); @@ -82,6 +67,7 @@ impl<'info> DecompressVariant> for TokenMockVariant { enum MockVariant { Pda(PdaMockVariant), Token(TokenMockVariant), + Error, } impl<'info> DecompressVariant> for MockVariant { @@ -94,6 +80,7 @@ impl<'info> DecompressVariant> for MockVariant { match self { MockVariant::Pda(v) => v.decompress(meta, pda_account, ctx), MockVariant::Token(v) => v.decompress(meta, pda_account, ctx), + MockVariant::Error => Err(LightSdkTypesError::ConstraintViolation), } } } @@ -102,31 +89,6 @@ impl<'info> DecompressVariant> for MockVariant { // Helpers // ============================================================================ -/// Standard 5-account layout for PDA-only error/happy path tests. -/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account -fn make_valid_accounts( - program_id: [u8; 32], -) -> ( - TestAccount, - TestAccount, - TestAccount, - TestAccount, - TestAccount, -) { - let (config_account, rent_sponsor_key) = make_config_account(program_id); - let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); - let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); - let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); - let pda_account = make_dummy_account([10u8; 32], program_id, 100); - ( - fee_payer, - config_account, - rent_sponsor, - system_account, - pda_account, - ) -} - /// 9-account layout for PDA+token tests. /// [0]=fee_payer, [1]=config, [2]=rent_sponsor, /// [3]=ctoken_rent_sponsor, [4..6]=dummies, [6]=ctoken_compressible_config, @@ -618,60 +580,259 @@ fn test_pda_and_token_sets_cpi_context() { #[test] fn test_randomized_pda_and_token_decompression() { let program_id = [42u8; 32]; - let mut rng = rand::thread_rng(); - - // Pick random counts in 0..=5, ensuring at least one account total. - let mut n_pdas: usize = rng.gen_range(0..=5); - let mut n_tokens: usize = rng.gen_range(0..=5); - if n_pdas + n_tokens == 0 { - // Clamp: give at least one of each type. - n_pdas = 1; - n_tokens = 1; + + // Explicit edge case: zero PDAs, zero tokens. + { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + + let remaining_accounts = vec![ + fee_payer.get_account_info(), + config_account.get_account_info(), + rent_sponsor.get_account_info(), + ctoken_rent_sponsor.get_account_info(), + dummy4.get_account_info(), + dummy5.get_account_info(), + ctoken_config.get_account_info(), + ]; + + let params: DecompressIdempotentParams = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let built = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ) + .unwrap(); + + assert_eq!( + built.compressed_account_infos, + Vec::::new(), + "zero-zero case: expected empty pda infos" + ); + assert_eq!( + built.in_token_data, + Vec::::new(), + "zero-zero case: expected empty token data" + ); + assert!(!built.has_pda_accounts, "zero-zero: has_pda_accounts"); + assert!(!built.has_token_accounts, "zero-zero: has_token_accounts"); + assert!(!built.cpi_context, "zero-zero: cpi_context"); } - // Build expected PDA infos with random addresses. - let expected_pda_infos: Vec = (0..n_pdas) - .map(|_| CompressedAccountInfo { - address: Some(rng.gen::<[u8; 32]>()), - input: None, - output: None, - }) - .collect(); - - // Build expected token data with random fields. - let expected_token_data: Vec = (0..n_tokens) - .map(|_| MultiInputTokenDataWithContext { - owner: rng.gen::(), - amount: rng.gen(), - has_delegate: rng.gen(), - delegate: rng.gen::(), - mint: rng.gen::(), - version: rng.gen::(), - root_index: rng.gen(), - merkle_context: PackedMerkleContext { - merkle_tree_pubkey_index: rng.gen(), - queue_pubkey_index: rng.gen(), - leaf_index: rng.gen(), - prove_by_index: rng.gen(), - }, - }) - .collect(); + for _ in 0..50 { + let seed: [u8; 32] = rand::thread_rng().gen(); + println!("seed: {seed:?}"); + let mut rng = rand::rngs::StdRng::from_seed(seed); + + // Pick random counts in 0..=5 (0,0 is valid; covered by explicit case above). + let n_pdas: usize = rng.gen_range(0..=5); + let n_tokens: usize = rng.gen_range(0..=5); + + // Build expected PDA infos with random addresses. + let expected_pda_infos: Vec = (0..n_pdas) + .map(|_| CompressedAccountInfo { + address: Some(rng.gen::<[u8; 32]>()), + input: None, + output: None, + }) + .collect(); + + // Build expected token data with random fields. + let expected_token_data: Vec = (0..n_tokens) + .map(|_| MultiInputTokenDataWithContext { + owner: rng.gen::(), + amount: rng.gen(), + has_delegate: rng.gen(), + delegate: rng.gen::(), + mint: rng.gen::(), + version: rng.gen::(), + root_index: rng.gen(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: rng.gen(), + queue_pubkey_index: rng.gen(), + leaf_index: rng.gen(), + prove_by_index: rng.gen(), + }, + }) + .collect(); + + // Build accounts: 7-account header + n_pdas PDAs + n_tokens token accounts. + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + let mut pda_accounts: Vec = (0..n_pdas) + .map(|i| make_dummy_account([(10 + i) as u8; 32], program_id, 100)) + .collect(); + let mut token_accounts: Vec = (0..n_tokens) + .map(|i| make_dummy_account([(20 + i) as u8; 32], program_id, 100)) + .collect(); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let mut pda_ais: Vec> = pda_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); + let mut token_ais: Vec> = token_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); + + let mut remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + ]; + remaining_accounts.append(&mut pda_ais); + remaining_accounts.append(&mut token_ais); + + // Build params.accounts: PDAs first, then tokens. + let mut accounts: Vec> = Vec::new(); + for info in &expected_pda_infos { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(info.clone())), + }); + } + for token in &expected_token_data { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant(*token)), + }); + } - // Build accounts: 7-account header + n_pdas PDAs + n_tokens token accounts. - let (config_account, rent_sponsor_key) = make_config_account(program_id); - let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); - let mut config_account = config_account; - let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); - let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); - let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); - let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); - let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); - let mut pda_accounts: Vec = (0..n_pdas) - .map(|i| make_dummy_account([(10 + i) as u8; 32], program_id, 100)) - .collect(); - let mut token_accounts: Vec = (0..n_tokens) - .map(|i| make_dummy_account([(20 + i) as u8; 32], program_id, 100)) - .collect(); + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: n_pdas as u8, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert_eq!( + built.compressed_account_infos, expected_pda_infos, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.in_token_data, expected_token_data, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_pda_accounts, + n_pdas > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_token_accounts, + n_tokens > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.cpi_context, + n_pdas > 0 && n_tokens > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + } +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_decompress_variant_error_during_pda_phase() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); let fee_payer_ai = fee_payer.get_account_info(); let config_ai = config_account.get_account_info(); @@ -680,16 +841,10 @@ fn test_randomized_pda_and_token_decompression() { let dummy4_ai = dummy4.get_account_info(); let dummy5_ai = dummy5.get_account_info(); let ctoken_config_ai = ctoken_config.get_account_info(); - let mut pda_ais: Vec> = pda_accounts - .iter_mut() - .map(|a| a.get_account_info()) - .collect(); - let mut token_ais: Vec> = token_accounts - .iter_mut() - .map(|a| a.get_account_info()) - .collect(); - - let mut remaining_accounts = vec![ + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + let remaining_accounts = vec![ fee_payer_ai, config_ai, rent_sponsor_ai, @@ -697,31 +852,28 @@ fn test_randomized_pda_and_token_decompression() { dummy4_ai, dummy5_ai, ctoken_config_ai, + pda_ai, + token_ai, ]; - remaining_accounts.append(&mut pda_ais); - remaining_accounts.append(&mut token_ais); - - // Build params.accounts: PDAs first, then tokens. - let mut accounts: Vec> = Vec::new(); - for info in &expected_pda_infos { - accounts.push(CompressedAccountData { - tree_info: PackedStateTreeInfo::default(), - data: MockVariant::Pda(PdaMockVariant(info.clone())), - }); - } - for token in &expected_token_data { - accounts.push(CompressedAccountData { - tree_info: PackedStateTreeInfo::default(), - data: MockVariant::Token(TokenMockVariant(*token)), - }); - } + // First entry is Error (PDA phase) → error propagated; token phase never reached. let params = DecompressIdempotentParams { system_accounts_offset: 7, - token_accounts_offset: n_pdas as u8, + token_accounts_offset: 1, output_queue_index: 0, proof: ValidityProof::default(), - accounts, + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Error, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant( + MultiInputTokenDataWithContext::default(), + )), + }, + ], }; let cpi_signer = CpiSigner { program_id, @@ -737,28 +889,137 @@ fn test_randomized_pda_and_token_decompression() { 0, ); - let built = result.unwrap(); - assert_eq!( - built.compressed_account_infos, expected_pda_infos, - "n_pdas={n_pdas} n_tokens={n_tokens}" - ); - assert_eq!( - built.in_token_data, expected_token_data, - "n_pdas={n_pdas} n_tokens={n_tokens}" - ); - assert_eq!( - built.has_pda_accounts, - n_pdas > 0, - "n_pdas={n_pdas} n_tokens={n_tokens}" - ); - assert_eq!( - built.has_token_accounts, - n_tokens > 0, - "n_pdas={n_pdas} n_tokens={n_tokens}" + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_decompress_variant_error_during_token_phase() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + pda_ai, + token_ai, + ]; + + // First entry is Pda (succeeds), second is Error (token phase) → error propagated. + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + })), + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Error, + }, + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, ); - assert_eq!( - built.cpi_context, - n_pdas > 0 && n_tokens > 0, - "n_pdas={n_pdas} n_tokens={n_tokens}" + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_token_decompression_missing_ctoken_config() { + let program_id = [42u8; 32]; + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut hot_token = make_dummy_account([20u8; 32], program_id, 100); + + // Only 5 accounts: [0]=fee_payer, [1]=config, [2]=rent_sponsor, + // [3]=ctoken_rent_sponsor, [4]=hot_token. + // has_token_accounts = true triggers get(6) which returns None → NotEnoughAccountKeys. + let remaining_accounts = vec![ + fee_payer.get_account_info(), + config_account.get_account_info(), + rent_sponsor.get_account_info(), + ctoken_rent_sponsor.get_account_info(), + hot_token.get_account_info(), + ]; + + let params: DecompressIdempotentParams = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant(MultiInputTokenDataWithContext::default())), + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, ); + + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); } diff --git a/sdk-libs/sdk-types/tests/decompress_processor.rs b/sdk-libs/sdk-types/tests/decompress_processor.rs index 7cdd84e482..51dd6ac67d 100644 --- a/sdk-libs/sdk-types/tests/decompress_processor.rs +++ b/sdk-libs/sdk-types/tests/decompress_processor.rs @@ -1,8 +1,7 @@ mod common; use borsh::{BorshDeserialize, BorshSerialize}; -use common::{make_config_account, make_dummy_account}; -use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use common::{make_dummy_account, make_valid_accounts, SkipVariant}; use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, @@ -26,22 +25,6 @@ use solana_pubkey::Pubkey; // Mock DecompressVariant implementations // ============================================================================ -/// Simulates an already-initialized PDA: pushes nothing to compressed_account_infos. -#[derive(BorshSerialize, BorshDeserialize, Clone)] -struct SkipVariant; - -impl<'info> DecompressVariant> for SkipVariant { - fn decompress( - &self, - _meta: &PackedStateTreeInfo, - _pda_account: &AccountInfo<'info>, - _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, - ) -> Result<(), LightSdkTypesError> { - // Don't push anything -> idempotent skip (all already initialized) - Ok(()) - } -} - /// Pushes one known CompressedAccountInfo to simulate decompression. #[derive(BorshSerialize, BorshDeserialize, Clone)] struct DecompressVariantMock; @@ -62,34 +45,6 @@ impl<'info> DecompressVariant> for DecompressVariantMock { } } -// ============================================================================ -// Helper: build the standard account list for valid tests -// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account -// ============================================================================ - -fn make_valid_accounts( - program_id: [u8; 32], -) -> ( - TestAccount, - TestAccount, - TestAccount, - TestAccount, - TestAccount, -) { - let (config_account, rent_sponsor_key) = make_config_account(program_id); - let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); - let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); - let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); - let pda_account = make_dummy_account([10u8; 32], program_id, 100); - ( - fee_payer, - config_account, - rent_sponsor, - system_account, - pda_account, - ) -} - fn one_pda_params( data: V, system_accounts_offset: u8, @@ -374,3 +329,77 @@ fn test_build_decompress_produces_expected_instruction_data() { let built = result.unwrap().unwrap(); assert_eq!(built.cpi_ix_data, expected_cpi_ix_data); } + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct ErrorVariant; + +impl<'info> DecompressVariant> for ErrorVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + Err(LightSdkTypesError::ConstraintViolation) + } +} + +#[test] +fn test_decompress_variant_error_propagates() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(ErrorVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +}