Skip to content

Conversation

@ananas-block
Copy link
Contributor

@ananas-block ananas-block commented Jan 24, 2026

Summary by CodeRabbit

  • New Features

    • Added new public APIs for PDA derivation, verification, and zero-copy account handling.
    • Introduced enhanced seed preparation and account decompression workflows with new helper functions.
  • Refactor

    • Refactored decompression process to use a two-pass approach with improved memory efficiency.
    • Updated account information structures with enhanced mutability handling.
    • Restructured public API surface with revised method signatures for token and PDA collection.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

This pull request refactors core decompression infrastructure from a single-pass to a two-pass zero-allocation approach. It adds #[repr(C)] and ZeroCopyMut derive to four account-info structs, removes the ZOutAccountInfoMut type, and introduces new public APIs for seed handling, PDA verification, and composite decompression workflows, all gated behind v2 and cpi-context features.

Changes

Cohort / File(s) Summary
Zero-Copy Account-Info Struct Updates
program-libs/compressed-account/src/instruction_data/with_account_info.rs
Added #[repr(C)] and ZeroCopyMut derive to InAccountInfo, OutAccountInfo, CompressedAccountInfo, and InstructionDataInvokeCpiWithAccountInfo; removed ZOutAccountInfoMut<'a> type and its Deref/DerefMut implementations.
Macro Codegen Two-Pass Refactor
sdk-libs/macros/src/light_pdas/account/decompress_context.rs
Introduced two-pass zero-allocation codegen: Pass 1 collects layout and counts PDAs; Pass 2 unpacks data, derives seeds, creates PDAs, and writes to zero-copy buffer. Updated method signatures for collect_layout_and_tokens and introduced new create_and_write_pda method; swapped Vec<CompressedData> with slices and added pda_indices plumbing.
Decompression Idempotent Helpers
sdk-libs/sdk/src/interface/decompress_idempotent.rs
Added MAX_SEEDS constant and new public functions: prepare_account_for_decompression_with_vec_seeds, verify_pda_match, and derive_verify_create_and_write_pda for seed handling and composite decompression workflows.
Decompression Runtime Two-Pass Flow
sdk-libs/sdk/src/interface/decompress_runtime.rs
Introduced two-pass DecompressContext API with new methods collect_layout_and_tokens and create_and_write_pda; added constants MAX_DECOMPRESS_ACCOUNTS and PDA_OUTPUT_DATA_LEN; added CPI helpers (build_decompression_cpi_config, allocate_decompress_cpi_bytes, execute_cpi_invoke_sdk, execute_cpi_write_to_context) for orchestrating the new flow.
Feature-Gated Public Exports
sdk-libs/sdk/src/interface/mod.rs
Added re-exports for new decompression helpers under cfg(feature = "v2") and cfg(all(feature = "v2", feature = "cpi-context")); exports include MAX_SEEDS, prepare_account_for_decompression_with_vec_seeds, verify_pda_match, derive_verify_create_and_write_pda, and zero-copy buffer types.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

🔄 Two passes now, not one—efficiency won!
Zero copies flow, stack usage is done.
Account-infos dressed in repr(C), so neat,
PDAs verified, the decompression's complete. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: zero copy decompress runtime' directly and specifically describes the main change: a refactoring of the decompression runtime to use a zero-copy, two-pass approach with improved memory efficiency.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jorrit/refactor-zero-copy-decompress-runtim

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ananas-block ananas-block force-pushed the jorrit/refactor-zero-copy-decompress-runtim branch from 52c97ef to ad7c229 Compare January 24, 2026 23:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@sdk-libs/macros/src/light_pdas/account/decompress_context.rs`:
- Around line 23-40: The match arm that writes into pda_indices in
collect_layout_and_tokens currently assumes pda_count < MAX_DECOMPRESS_ACCOUNTS
and can panic; add a bounds check before assigning pda_indices[pda_count] and
incrementing pda_count: if pda_count >= MAX_DECOMPRESS_ACCOUNTS return a
deterministic error (e.g. a new or existing LightSdkError variant indicating too
many PDAs) instead of writing out-of-bounds. Update the arm for
LightAccountVariant::`#packed_variant_name` { .. } to perform the check, and
reference pda_indices, pda_count and MAX_DECOMPRESS_ACCOUNTS when implementing
the guard so the function never panics on overflow.

In `@sdk-libs/sdk/src/interface/decompress_idempotent.rs`:
- Around line 99-143: The function
prepare_account_for_decompression_with_vec_seeds currently truncates seeds if
seeds_vec.len() > MAX_SEEDS which can break PDA derivation; instead, check the
seed count up front and return an error when seeds_vec.len() > MAX_SEEDS (do not
silently drop seeds), e.g., return a LightSdkError variant indicating too many
seeds (or add one) before building seed_refs, then proceed to populate seed_refs
and call prepare_account_for_decompression_idempotent as before; reference:
prepare_account_for_decompression_with_vec_seeds, MAX_SEEDS,
prepare_account_for_decompression_idempotent.

Comment on lines +23 to +40
// Generate match arms for collect_layout_and_tokens - count PDAs that need decompression
let collect_layout_pda_arms: Vec<_> = pda_ctx_seeds
.iter()
.map(|info| {
let variant_name = &info.variant_name;
let packed_variant_name = make_packed_variant_name(variant_name);
quote! {
LightAccountVariant::#packed_variant_name { .. } => {
// PDA variant: only count if not already initialized (idempotent check)
if solana_accounts[i].data_is_empty() {
pda_indices[pda_count] = i;
pda_count += 1;
}
}
LightAccountVariant::#variant_name { .. } => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add bounds check before writing to pda_indices

If more than MAX_DECOMPRESS_ACCOUNTS PDAs are encountered, the current code will panic on out‑of‑bounds write. Guard the write and return a deterministic error instead.

Proposed fix
-                    if solana_accounts[i].data_is_empty() {
-                        pda_indices[pda_count] = i;
-                        pda_count += 1;
-                    }
+                    if solana_accounts[i].data_is_empty() {
+                        if pda_count >= light_sdk::interface::MAX_DECOMPRESS_ACCOUNTS {
+                            return std::result::Result::Err(
+                                light_sdk::error::LightSdkError::ConstraintViolation.into()
+                            );
+                        }
+                        pda_indices[pda_count] = i;
+                        pda_count += 1;
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Generate match arms for collect_layout_and_tokens - count PDAs that need decompression
let collect_layout_pda_arms: Vec<_> = pda_ctx_seeds
.iter()
.map(|info| {
let variant_name = &info.variant_name;
let packed_variant_name = make_packed_variant_name(variant_name);
quote! {
LightAccountVariant::#packed_variant_name { .. } => {
// PDA variant: only count if not already initialized (idempotent check)
if solana_accounts[i].data_is_empty() {
pda_indices[pda_count] = i;
pda_count += 1;
}
}
LightAccountVariant::#variant_name { .. } => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
}
// Generate match arms for collect_layout_and_tokens - count PDAs that need decompression
let collect_layout_pda_arms: Vec<_> = pda_ctx_seeds
.iter()
.map(|info| {
let variant_name = &info.variant_name;
let packed_variant_name = make_packed_variant_name(variant_name);
quote! {
LightAccountVariant::`#packed_variant_name` { .. } => {
// PDA variant: only count if not already initialized (idempotent check)
if solana_accounts[i].data_is_empty() {
if pda_count >= light_sdk::interface::MAX_DECOMPRESS_ACCOUNTS {
return std::result::Result::Err(
light_sdk::error::LightSdkError::ConstraintViolation.into()
);
}
pda_indices[pda_count] = i;
pda_count += 1;
}
}
LightAccountVariant::`#variant_name` { .. } => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
}
🤖 Prompt for AI Agents
In `@sdk-libs/macros/src/light_pdas/account/decompress_context.rs` around lines 23
- 40, The match arm that writes into pda_indices in collect_layout_and_tokens
currently assumes pda_count < MAX_DECOMPRESS_ACCOUNTS and can panic; add a
bounds check before assigning pda_indices[pda_count] and incrementing pda_count:
if pda_count >= MAX_DECOMPRESS_ACCOUNTS return a deterministic error (e.g. a new
or existing LightSdkError variant indicating too many PDAs) instead of writing
out-of-bounds. Update the arm for LightAccountVariant::`#packed_variant_name` { ..
} to perform the check, and reference pda_indices, pda_count and
MAX_DECOMPRESS_ACCOUNTS when implementing the guard so the function never panics
on overflow.

Comment on lines +99 to +143
/// Maximum number of seeds for PDA derivation.
pub const MAX_SEEDS: usize = 16;

/// Convert Vec seeds to fixed array and call PDA creation.
/// Isolated in separate function to reduce stack usage (seed_refs array is on its own frame).
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_with_vec_seeds<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
seeds_vec: &[Vec<u8>],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ HasCompressionInfo
+ 'info,
{
// Convert Vec seeds to fixed array on this stack frame
let mut seed_refs: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
let len = seeds_vec.len().min(MAX_SEEDS);
for j in 0..len {
seed_refs[j] = seeds_vec[j].as_slice();
}

prepare_account_for_decompression_idempotent(
program_id,
data,
compressed_meta,
solana_account,
rent_sponsor,
cpi_accounts,
&seed_refs[..len],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid silently truncating PDA seeds

Truncating extra seeds can desynchronize signer seeds from the derived PDA and cause CPI signature failures. Return an error when the seed count exceeds MAX_SEEDS instead of dropping data.

Proposed fix
-    let len = seeds_vec.len().min(MAX_SEEDS);
+    let len = seeds_vec.len();
+    if len > MAX_SEEDS {
+        return Err(LightSdkError::ConstraintViolation);
+    }
     for j in 0..len {
         seed_refs[j] = seeds_vec[j].as_slice();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Maximum number of seeds for PDA derivation.
pub const MAX_SEEDS: usize = 16;
/// Convert Vec seeds to fixed array and call PDA creation.
/// Isolated in separate function to reduce stack usage (seed_refs array is on its own frame).
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_with_vec_seeds<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
seeds_vec: &[Vec<u8>],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ HasCompressionInfo
+ 'info,
{
// Convert Vec seeds to fixed array on this stack frame
let mut seed_refs: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
let len = seeds_vec.len().min(MAX_SEEDS);
for j in 0..len {
seed_refs[j] = seeds_vec[j].as_slice();
}
prepare_account_for_decompression_idempotent(
program_id,
data,
compressed_meta,
solana_account,
rent_sponsor,
cpi_accounts,
&seed_refs[..len],
)
/// Maximum number of seeds for PDA derivation.
pub const MAX_SEEDS: usize = 16;
/// Convert Vec seeds to fixed array and call PDA creation.
/// Isolated in separate function to reduce stack usage (seed_refs array is on its own frame).
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_with_vec_seeds<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
seeds_vec: &[Vec<u8>],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
crate::account::Size
LightDiscriminator
Default
AnchorSerialize
AnchorDeserialize
HasCompressionInfo
'info,
{
// Convert Vec seeds to fixed array on this stack frame
let mut seed_refs: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
let len = seeds_vec.len();
if len > MAX_SEEDS {
return Err(LightSdkError::ConstraintViolation);
}
for j in 0..len {
seed_refs[j] = seeds_vec[j].as_slice();
}
prepare_account_for_decompression_idempotent(
program_id,
data,
compressed_meta,
solana_account,
rent_sponsor,
cpi_accounts,
&seed_refs[..len],
)
🤖 Prompt for AI Agents
In `@sdk-libs/sdk/src/interface/decompress_idempotent.rs` around lines 99 - 143,
The function prepare_account_for_decompression_with_vec_seeds currently
truncates seeds if seeds_vec.len() > MAX_SEEDS which can break PDA derivation;
instead, check the seed count up front and return an error when seeds_vec.len()
> MAX_SEEDS (do not silently drop seeds), e.g., return a LightSdkError variant
indicating too many seeds (or add one) before building seed_refs, then proceed
to populate seed_refs and call prepare_account_for_decompression_idempotent as
before; reference: prepare_account_for_decompression_with_vec_seeds, MAX_SEEDS,
prepare_account_for_decompression_idempotent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@sdk-libs/macros/src/light_pdas/account/decompress_context.rs`:
- Around line 23-43: The generated match arms write into pda_indices[pda_count]
without checking bounds, which can panic if more than MAX_DECOMPRESS_ACCOUNTS
PDAs are encountered; update the code in the match arms generated for
collect_layout_and_tokens (the block using pda_indices, pda_count) to first
check if pda_count >= MAX_DECOMPRESS_ACCOUNTS and return an appropriate error
(e.g., LightSdkError::TooManyDecompressAccounts or map to an existing error)
before assigning to pda_indices and incrementing pda_count; apply the same guard
to the other similar generated block around lines 187-214 so both sites perform
the capacity check and return on overflow instead of writing out of bounds.
- Around line 216-246: The code panics when slicing all_infos with
&all_infos[post_system_offset..]; change that to check bounds via
all_infos.get(post_system_offset..) and handle the None case by returning an
appropriate ProgramError instead of panicking. In the create_and_write_pda
function, replace let post_system_accounts = &all_infos[post_system_offset..];
with a match or if-let on all_infos.get(post_system_offset..) binding
post_system_accounts on Some and returning Err(...) on None (for example
Err(light_sdk::error::LightSdkError::UnexpectedAccountListLength.into()) or
another suitable ProgramError).

In `@sdk-libs/sdk/src/interface/decompress_idempotent.rs`:
- Around line 97-142: The function
prepare_account_for_decompression_with_vec_seeds currently silently truncates
seeds via seeds_vec.len().min(MAX_SEEDS), which will break PDA signer seeds;
instead, check if seeds_vec.len() > MAX_SEEDS and return an Err(LightSdkError)
(with a clear message like "too many PDA seeds") before attempting conversion;
then proceed to build seed_refs and call
prepare_account_for_decompression_idempotent as before. Reference MAX_SEEDS and
prepare_account_for_decompression_with_vec_seeds when locating the change.
- Around line 3-6: The CompressedAccountInfo import is over-gated; change the
cfg so CompressedAccountInfo and OutAccountInfo are gated together under the
same feature used by the functions that return CompressedAccountInfo (i.e., use
#[cfg(feature = "v2")] for both imports rather than all(feature = "v2", feature
= "cpi-context")). Locate the use of CompressedAccountInfo and OutAccountInfo in
the import list and replace the current cfg that only allows
CompressedAccountInfo under both features with a single #[cfg(feature = "v2")]
(or apply the same cfg to both types) so the return types compile when feature
"v2" is enabled.

Comment on lines +23 to +43
// Generate match arms for collect_layout_and_tokens - count PDAs that need decompression
let collect_layout_pda_arms: Vec<_> = pda_ctx_seeds
.iter()
.map(|info| {
let variant_name = &info.variant_name;
let packed_variant_name = make_packed_variant_name(variant_name);
quote! {
LightAccountVariant::#packed_variant_name { .. } => {
// PDA variant: only count if not already initialized (idempotent check)
if solana_accounts[i].data_is_empty() {
pda_indices[pda_count] = i;
pda_count += 1;
}
}
LightAccountVariant::#variant_name { .. } => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
}
})
.collect();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against >MAX_DECOMPRESS_ACCOUNTS to avoid out‑of‑bounds writes.

The generated code writes pda_indices[pda_count] without a capacity check. More than 16 PDAs will panic at runtime. Add a guard and return an error when the limit is exceeded.

🛠️ Suggested fix
-                LightAccountVariant::`#packed_variant_name` { .. } => {
-                    // PDA variant: only count if not already initialized (idempotent check)
-                    if solana_accounts[i].data_is_empty() {
-                        pda_indices[pda_count] = i;
-                        pda_count += 1;
-                    }
-                }
+                LightAccountVariant::`#packed_variant_name` { .. } => {
+                    // PDA variant: only count if not already initialized (idempotent check)
+                    if solana_accounts[i].data_is_empty() {
+                        if pda_count >= light_sdk::interface::MAX_DECOMPRESS_ACCOUNTS {
+                            return std::result::Result::Err(
+                                light_sdk::error::LightSdkError::ConstraintViolation.into(),
+                            );
+                        }
+                        pda_indices[pda_count] = i;
+                        pda_count += 1;
+                    }
+                }

Also applies to: 187-214

🤖 Prompt for AI Agents
In `@sdk-libs/macros/src/light_pdas/account/decompress_context.rs` around lines 23
- 43, The generated match arms write into pda_indices[pda_count] without
checking bounds, which can panic if more than MAX_DECOMPRESS_ACCOUNTS PDAs are
encountered; update the code in the match arms generated for
collect_layout_and_tokens (the block using pda_indices, pda_count) to first
check if pda_count >= MAX_DECOMPRESS_ACCOUNTS and return an appropriate error
(e.g., LightSdkError::TooManyDecompressAccounts or map to an existing error)
before assigning to pda_indices and incrementing pda_count; apply the same guard
to the other similar generated block around lines 187-214 so both sites perform
the capacity check and return on overflow instead of writing out of bounds.

Comment on lines +216 to 246
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn create_and_write_pda<'b, 'c>(
&self,
cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>,
address_space: &solana_pubkey::Pubkey,
compressed_data: &Self::CompressedData,
solana_account: &solana_account_info::AccountInfo<#lifetime>,
seed_params: std::option::Option<&Self::SeedParams>,
zc_info: &mut light_sdk::interface::ZCompressedAccountInfoMut<'c>,
) -> std::result::Result<bool, solana_program_error::ProgramError> {
let post_system_offset = cpi_accounts.system_accounts_end_offset();
let all_infos = cpi_accounts.account_infos();
let post_system_accounts = &all_infos[post_system_offset..];
let program_id = crate::ID;
let compressed_meta = &compressed_data.meta;
let mut variant_seed_params = SeedParams::default();
let _ = &variant_seed_params; // Suppress unused warning when no params-only fields

match &compressed_data.data {
#(#create_pda_match_arms)*
LightAccountVariant::PackedCTokenData(_) => {
// Tokens are handled separately, skip here
std::result::Result::Ok(false)
}
LightAccountVariant::CTokenData(_) => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
#empty_variant_arm_create
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent panics when slicing post‑system accounts.

&all_infos[post_system_offset..] will panic on malformed account lists. Use get() and return a ProgramError instead.

🛠️ Suggested fix
-                let post_system_accounts = &all_infos[post_system_offset..];
+                let post_system_accounts = all_infos
+                    .get(post_system_offset..)
+                    .ok_or(solana_program_error::ProgramError::NotEnoughAccountKeys)?;
🤖 Prompt for AI Agents
In `@sdk-libs/macros/src/light_pdas/account/decompress_context.rs` around lines
216 - 246, The code panics when slicing all_infos with
&all_infos[post_system_offset..]; change that to check bounds via
all_infos.get(post_system_offset..) and handle the None case by returning an
appropriate ProgramError instead of panicking. In the create_and_write_pda
function, replace let post_system_accounts = &all_infos[post_system_offset..];
with a match or if-let on all_infos.get(post_system_offset..) binding
post_system_accounts on Some and returning Err(...) on None (for example
Err(light_sdk::error::LightSdkError::UnexpectedAccountListLength.into()) or
another suitable ProgramError).

Comment on lines 3 to 6
use light_compressed_account::{
address::derive_address, instruction_data::with_account_info::OutAccountInfo,
address::derive_address,
instruction_data::with_account_info::{CompressedAccountInfo, OutAccountInfo},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file in question
fd -t f "decompress_idempotent.rs" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 123


🏁 Script executed:

# Check the file size and structure
wc -l sdk-libs/sdk/src/interface/decompress_idempotent.rs

Repository: Lightprotocol/light-protocol

Length of output: 127


🏁 Script executed:

# Read the imports and initial structure
head -50 sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 2200


🏁 Script executed:

# Search for all uses of CompressedAccountInfo in the file
rg "CompressedAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs -n

Repository: Lightprotocol/light-protocol

Length of output: 702


🏁 Script executed:

# Search for all uses of OutAccountInfo in the file
rg "OutAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs -n

Repository: Lightprotocol/light-protocol

Length of output: 191


🏁 Script executed:

# Check the Cargo.toml to understand feature flags
cat sdk-libs/sdk/Cargo.toml | grep -A 20 "features"

Repository: Lightprotocol/light-protocol

Length of output: 2364


🏁 Script executed:

# Check context around line 113
sed -n '105,125n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check context around line 157
sed -n '150,170n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check context around line 315
sed -n '305,330n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check the function signatures and their feature gates
rg -B 5 "CompressedAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs | head -80

Repository: Lightprotocol/light-protocol

Length of output: 1528


🏁 Script executed:

# Get the full context for functions at lines 97-170
sed -n '97,170n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Also verify which features guard the functions
rg "fn prepare_account_for_decompression" -A 20 sdk-libs/sdk/src/interface/decompress_idempotent.rs | head -40

Repository: Lightprotocol/light-protocol

Length of output: 1261


🏁 Script executed:

# Check all cfg attributes in the file
rg "#\[cfg" sdk-libs/sdk/src/interface/decompress_idempotent.rs -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 1260


🏁 Script executed:

# Verify the exact feature guards on all functions using these types
sed -n '1,360n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | grep -E "(^#\[cfg|fn (prepare_account|write_to_zero|set_decompressed))" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Double-check the return types more carefully
sed -n '97,125n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Read the entire relevant section with line numbers
cat -n sdk-libs/sdk/src/interface/decompress_idempotent.rs | sed -n '1,40p'

Repository: Lightprotocol/light-protocol

Length of output: 1669


🏁 Script executed:

# Check lines around functions with CompressedAccountInfo
cat -n sdk-libs/sdk/src/interface/decompress_idempotent.rs | sed -n '97,145p'

Repository: Lightprotocol/light-protocol

Length of output: 1878


🏁 Script executed:

# Check if there are any uses of CompressedAccountInfo outside cfg gates
rg "CompressedAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs -B 10 | grep -E "(#\[cfg|CompressedAccountInfo)"

Repository: Lightprotocol/light-protocol

Length of output: 775


🏁 Script executed:

# Let me verify the exact feature guards on the functions that use these types
sed -n '144,165n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check if v2 is the default feature
grep -A 2 "^\[features\]" sdk-libs/sdk/Cargo.toml | head -5

Repository: Lightprotocol/light-protocol

Length of output: 169


🏁 Script executed:

# Let me get the complete picture of function signatures
sed -n '144,200n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Verify when CI would actually report this as an unused import
# Check if there's a build without v2 feature
rg "no-default-features" . --type toml -i

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check what features are used in tests/CI
fd -t f "Cargo.toml" | xargs grep -l "sdk" | head -5

Repository: Lightprotocol/light-protocol

Length of output: 225


🏁 Script executed:

# Let me get the complete function definitions to understand feature gating better
sed -n '144,180n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check the derive_verify_create_and_write_pda function signature
sed -n '225,265n' sdk-libs/sdk/src/interface/decompress_idempotent.rs | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Use cat to view specific line ranges
cat sdk-libs/sdk/src/interface/decompress_idempotent.rs | sed -n '144,180p' | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 1444


🏁 Script executed:

# Check the derive_verify_create_and_write_pda function
cat sdk-libs/sdk/src/interface/decompress_idempotent.rs | sed -n '225,270p' | cat -n

Repository: Lightprotocol/light-protocol

Length of output: 2064


🏁 Script executed:

# Let me try a different approach - check the entire file for feature gates on functions
grep -n "^#\[cfg\|^pub fn\|^fn" sdk-libs/sdk/src/interface/decompress_idempotent.rs | tail -30

Repository: Lightprotocol/light-protocol

Length of output: 721


🏁 Script executed:

# Find all lines where CompressedAccountInfo appears in function signatures with context
grep -n "CompressedAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs

Repository: Lightprotocol/light-protocol

Length of output: 702


🏁 Script executed:

# For each line, check what function it belongs to and its feature gate
awk '
/^#\[cfg/ {
  cfg = $0
  getline
}
/^(pub )?fn / {
  fn = $0
}
/CompressedAccountInfo/ {
  print NR": "cfg" || "fn" || "$0
  cfg = ""
}
' sdk-libs/sdk/src/interface/decompress_idempotent.rs

Repository: Lightprotocol/light-protocol

Length of output: 1271


🏁 Script executed:

# Let me just manually check each CompressedAccountInfo usage with its feature guard
python3 << 'EOF'
import re

with open('sdk-libs/sdk/src/interface/decompress_idempotent.rs', 'r') as f:
    lines = f.readlines()

# Track current feature guard
current_cfg = None
current_fn = None

for i, line in enumerate(lines, 1):
    # Check for cfg attribute
    if line.strip().startswith('#[cfg'):
        current_cfg = line.strip()
    # Check for function definition
    elif line.strip().startswith('fn ') or line.strip().startswith('pub fn '):
        current_fn = line.strip()[:50]
    # Check for CompressedAccountInfo usage
    if 'CompressedAccountInfo' in line:
        print(f"Line {i}: cfg='{current_cfg}' fn='{current_fn}'\n  {line.rstrip()}")

EOF

Repository: Lightprotocol/light-protocol

Length of output: 158


🏁 Script executed:

# Let me directly examine the usage pattern more carefully without timing out
grep -B 2 "CompressedAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs | grep -E "(#\[cfg|CompressedAccountInfo)"

Repository: Lightprotocol/light-protocol

Length of output: 729


🏁 Script executed:

# Check how OutAccountInfo is used
grep -B 5 "OutAccountInfo" sdk-libs/sdk/src/interface/decompress_idempotent.rs | head -15

Repository: Lightprotocol/light-protocol

Length of output: 464


Correct the feature gating for the CompressedAccountInfo import.

The import needs feature gating, but the proposed fix incorrectly restricts CompressedAccountInfo to all(feature = "v2", feature = "cpi-context"). However, CompressedAccountInfo appears in return types of functions guarded only by #[cfg(feature = "v2")] (lines 113, 157). Both types should be gated together:

-use light_compressed_account::{
-    address::derive_address,
-    instruction_data::with_account_info::{CompressedAccountInfo, OutAccountInfo},
-};
+use light_compressed_account::address::derive_address;
+#[cfg(feature = "v2")]
+use light_compressed_account::instruction_data::with_account_info::{CompressedAccountInfo, OutAccountInfo};
🧰 Tools
🪛 GitHub Actions: examples-tests

[error] 5-5: unused import: CompressedAccountInfo

🪛 GitHub Actions: programs

[error] 5-5: unused import: CompressedAccountInfo

🪛 GitHub Check: programs (compressed-token-and-e2e, ["cargo test -p light-compressed-token", "cargo-test-sbf -p c...

[failure] 5-5:
unused import: CompressedAccountInfo

🪛 GitHub Check: programs (compressed-token-batched-tree, ["cargo-test-sbf -p compressed-token-test -- test_transf...

[failure] 5-5:
unused import: CompressedAccountInfo


[failure] 5-5:
unused import: CompressedAccountInfo

🪛 GitHub Check: system-programs (anchor & pinocchio, ["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk...

[failure] 5-5:
unused import: CompressedAccountInfo

🤖 Prompt for AI Agents
In `@sdk-libs/sdk/src/interface/decompress_idempotent.rs` around lines 3 - 6, The
CompressedAccountInfo import is over-gated; change the cfg so
CompressedAccountInfo and OutAccountInfo are gated together under the same
feature used by the functions that return CompressedAccountInfo (i.e., use
#[cfg(feature = "v2")] for both imports rather than all(feature = "v2", feature
= "cpi-context")). Locate the use of CompressedAccountInfo and OutAccountInfo in
the import list and replace the current cfg that only allows
CompressedAccountInfo under both features with a single #[cfg(feature = "v2")]
(or apply the same cfg to both types) so the return types compile when feature
"v2" is enabled.

Comment on lines +97 to +142
/// Maximum number of seeds for PDA derivation.
pub const MAX_SEEDS: usize = 16;

/// Convert Vec seeds to fixed array and call PDA creation.
/// Isolated in separate function to reduce stack usage (seed_refs array is on its own frame).
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_with_vec_seeds<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
seeds_vec: &[Vec<u8>],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ HasCompressionInfo
+ 'info,
{
// Convert Vec seeds to fixed array on this stack frame
let mut seed_refs: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
let len = seeds_vec.len().min(MAX_SEEDS);
for j in 0..len {
seed_refs[j] = seeds_vec[j].as_slice();
}

prepare_account_for_decompression_idempotent(
program_id,
data,
compressed_meta,
solana_account,
rent_sponsor,
cpi_accounts,
&seed_refs[..len],
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid silently truncating PDA seeds.

Using min(MAX_SEEDS) drops extra seeds. If the PDA was derived from the full list, the signer seeds become invalid and the CPI will fail. Prefer a hard error when the limit is exceeded.

🛠️ Suggested fix
-    let len = seeds_vec.len().min(MAX_SEEDS);
+    if seeds_vec.len() > MAX_SEEDS {
+        msg!("Too many PDA seeds: {}", seeds_vec.len());
+        return Err(LightSdkError::ConstraintViolation);
+    }
+    let len = seeds_vec.len();
🤖 Prompt for AI Agents
In `@sdk-libs/sdk/src/interface/decompress_idempotent.rs` around lines 97 - 142,
The function prepare_account_for_decompression_with_vec_seeds currently silently
truncates seeds via seeds_vec.len().min(MAX_SEEDS), which will break PDA signer
seeds; instead, check if seeds_vec.len() > MAX_SEEDS and return an
Err(LightSdkError) (with a clear message like "too many PDA seeds") before
attempting conversion; then proceed to build seed_refs and call
prepare_account_for_decompression_idempotent as before. Reference MAX_SEEDS and
prepare_account_for_decompression_with_vec_seeds when locating the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants