Skip to content

chore: csdk macros3#2062

Merged
ananas-block merged 6 commits intomainfrom
swen/csdk-macro-rent
Nov 24, 2025
Merged

chore: csdk macros3#2062
ananas-block merged 6 commits intomainfrom
swen/csdk-macro-rent

Conversation

@SwenSchaeferjohann
Copy link
Contributor

@SwenSchaeferjohann SwenSchaeferjohann commented Nov 18, 2025

stacked on top of #2020, merge 2020 first

  • rent impl for c-PDA.

reuses most of the rent impl of c-token, however we always transfer rent to the program's rent_sponsor pda, even if a different fee_payer paid for rent exemption.

for a rent v1 the benefits of this (saves account space, simpler) outweigh the cons. I think thats fair because the "fee" goes to the program's rent_sponsor PDA, funding the next user's PDA rent exemption.

  • added derive_rent_sponsor!(program_id) macro
  • allow rust expressions as seeds to add_compressible_instructions!() macro
  • allow seed references to other instruction accounts as well as instruction_data
  • made macros modular, moved processors to token-sdk and sdk (compress_runtime.rs, decompress_runtime.rs)

added tests for the macros at every level:

  • sdk-compressible-test is a manual implementation of the hooks
  • csdk-anchor-derived-test uses most smaller macros
  • csdk-anchor-full-derived-test uses add_compressible_instructions macro

Summary by CodeRabbit

  • New Features

    • New compressible macros/derives and runtime processors for idempotent compression/decompression (PDAs & tokens).
    • RentSponsor type and rent-sponsor PDA derivation helpers.
  • Documentation

    • Added comprehensive Compressible GUIDE and README with PDA-only, token, cToken, cMint, and client workflows.
  • Improvements

    • Config extended with compression authority, rent settings, and write top-up; simplified per-account idempotent compression flow and safer edge-case handling (fewer required parameters).

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 18, 2025

Walkthrough

Adds compressible macro infrastructure, rent‑sponsor driven compression model, idempotent compress/decompress runtimes, token decompression runtime and seed‑provider traits; updates client builders (removed delays/signers) and exposes compressed-token-sdk decompression APIs and an error module.

Changes

Cohort / File(s) Change Summary
Macro crate: compressible macros & utilities
sdk-libs/macros/src/lib.rs, sdk-libs/macros/Cargo.toml, sdk-libs/macros/{GUIDE.md,README.md} , sdk-libs/macros/src/compressible/{mod.rs,instructions.rs,traits.rs,pack_unpack.rs,variant_enum.rs,seed_providers.rs,decompress_context.rs,utils.rs}, sdk-libs/macros/src/rent_sponsor.rs
Adds a full compressible procedural macro subsystem: derives and attribute macros (Compressible, CompressAs, HasCompressionInfo, CompressiblePack, DecompressContext, add_compressible_instructions), seed/provider generation, pack/unpack helpers, utilities, rent_sponsor macros, docs, and a new workspace dependency.
Client instruction refactor
sdk-libs/compressible-client/src/lib.rs
Removes compression_delay, new_compression_delay, and signer_seeds; adds get_output_queue helper; updates idempotent compress/decompress builders, discriminator constants, and removes a public CompressibleInstruction re-export.
SDK: compression model, runtime & utilities
sdk-libs/sdk/src/compressible/{config.rs,compression_info.rs,compress_account.rs,compress_account_on_init.rs,compress_runtime.rs,decompress_idempotent.rs,decompress_runtime.rs,close.rs}, sdk-libs/sdk/src/utils.rs, sdk-libs/sdk/src/lib.rs, sdk-libs/sdk/src/compressible/mod.rs
Redesigns CompressionInfo and CompressibleConfig (rent_config, write_top_up, compression_authority), adds rent-topup/claim APIs, rent-sponsor PDA helper, idempotent compress runtime (CompressContext/process_compress_pda_accounts_idempotent), idempotent decompress runtime (DecompressContext, PdaSeedDerivation, HasTokenVariant), and re-exports behind feature "v2".
Compressed-token SDK: token decompression runtime & exports
sdk-libs/compressed-token-sdk/src/compressible/mod.rs, sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs, sdk-libs/compressed-token-sdk/src/lib.rs, sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs
Adds decompress_runtime module and re-exports process_decompress_tokens_runtime and CTokenSeedProvider; implements token decompression runtime and CTokenSeedProvider trait; adds pub mod error; and re-exports runtime items; small rent-sponsor fallback and lint attribute changes.
Types & constants
sdk-libs/sdk-types/src/lib.rs, sdk-libs/sdk-types/src/constants.rs
Adds RentSponsor struct and RENT_SPONSOR_SEED constant.
Program-test & tests
sdk-libs/program-test/src/{compressible.rs,program_test/compressible_setup.rs,program_test/config.rs,program_test/light_program_test.rs,program_test/test_rpc.rs}, sdk-libs/program-test/Cargo.toml
Updates tests to per-account idempotent compression, removes delay/signers, renames auto-compress state to auto_mine_cold_state_programs, updates instruction import paths and devenv defaults.
Token client manifest
sdk-libs/token-client/Cargo.toml
Removes light-compressible dependency.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant DecompressRuntime as DecompressRuntime (SDK)
  participant DecompressCtx as DecompressContext impl
  participant CPI as Light System/Token CPI

  Caller->>DecompressRuntime: process_decompress_accounts_idempotent(remaining_accounts, compressed_accounts, proof,...)
  DecompressRuntime->>DecompressCtx: collect_pda_and_token(..., compressed_accounts, remaining_accounts, seed_params)
  DecompressCtx-->>DecompressRuntime: (pda_infos, token_tuples)
  alt PDAs present
    DecompressRuntime->>CPI: invoke LightSystemProgram CPI with pda_infos (signed)
    CPI-->>DecompressRuntime: success/failure
  end
  alt Tokens present
    DecompressRuntime->>DecompressCtx: process_tokens(remaining_accounts, ctoken_program, token_tuples, proof,...)
    DecompressCtx->>CPI: create_ctoken_accounts & decompress_full_ctoken_accounts_with_indices (invoke_signed)
    CPI-->>DecompressCtx: success/failure
  end
  DecompressRuntime-->>Caller: Result
Loading
sequenceDiagram
  autonumber
  participant Caller
  participant TokenRuntime as process_decompress_tokens_runtime
  participant SeedProv as CTokenSeedProvider
  participant CPI as token CPI

  Caller->>TokenRuntime: process_decompress_tokens_runtime(accounts_for_seeds, ctoken_accounts, proof,...)
  TokenRuntime->>SeedProv: get_seeds(accounts_for_seeds, remaining_accounts)
  SeedProv-->>TokenRuntime: (seeds, derived_addr)
  TokenRuntime->>TokenRuntime: validate derived_addr, build packed token account(s)
  TokenRuntime->>CPI: create_ctoken_account_signed + decompress_full_ctoken_accounts_with_indices (invoke_signed)
  CPI-->>TokenRuntime: success/failure
  TokenRuntime-->>Caller: Result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

  • High heterogeneity: large proc-macro surface, runtime CPI logic, data-model and cross-crate API changes.
  • Pay special attention to:
    • sdk-libs/macros/src/compressible/instructions.rs, decompress_context.rs (macro parsing, lifetime/attribute handling, generated code correctness).
    • sdk-libs/sdk/src/compressible/decompress_runtime.rs and sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (trait bounds, seed derivation, CPI signer seeds, account indexing).
    • sdk-libs/sdk/src/compressible/compression_info.rs and config.rs (space constants, packing/unpack invariants, RentConfig layout).
    • sdk-libs/compressible-client/src/lib.rs (public builder signatures and discriminator changes).
    • Re-exports and feature-gating in sdk-libs/sdk/src/compressible/mod.rs and sdk-libs/compressed-token-sdk/src/lib.rs.

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • SwenSchaeferjohann
  • sergeytimoshin

Poem

✨ Rent sponsors hum, the macros weave,

Seeds and PDAs in patterns cleave.
Idempotent flows that quietly run,
Tokens unwind and work is done.
Small helpers stitched — a tidy job spun.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'chore: csdk macros3' is vague and generic. It does not clearly convey the primary focus—implementing rent handling for c-PDAs, adding derive_rent_sponsor macro, or enhancing add_compressible_instructions. A reader scanning history would struggle to understand what this PR accomplishes. Consider a more descriptive title such as 'chore: add rent handling and seed expressions to csdk macros' or 'chore: implement rent sponsor PDA derivation and enhance compressible instructions macro' to better reflect the main changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch swen/csdk-macro-rent

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68a3129 and a6003c5.

📒 Files selected for processing (1)
  • sdk-libs/program-test/Cargo.toml (1 hunks)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Forester e2e test
  • GitHub Check: Test program-libs-fast
  • GitHub Check: Test program-libs-slow
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: programs (compressed-token-and-e2e, ["cargo-test-sbf -p compressed-token-test --test v1", "cargo-...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...

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.

@SwenSchaeferjohann SwenSchaeferjohann marked this pull request as ready for review November 18, 2025 03:11
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: 66

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)

58-85: Instruction wiring matches new SDK builder; consider centralizing rent_sponsor derivation

The wrapper constructs CreateCompressibleTokenAccount with fields that line up with the new create_compressible_token_account_instruction helper, and the PDA derivations for compressible_config and rent_sponsor look consistent with the on-chain layout.

Given this PR introduces a derive_rent_sponsor!-style macro elsewhere, it might be worth routing the rent_sponsor PDA calculation through that shared helper to avoid seed/program-id drift in the future and keep derivation logic in one place.

sdk-libs/program-test/src/program_test/config.rs (1)

18-27: New auto_register_custom_programs_for_pda_compression flag is wired correctly; document its semantics

The new auto_register_custom_programs_for_pda_compression field is properly guarded by devenv, and Default initializes it to true, so all existing constructors that rely on ..Default::default() remain well-formed and pick up the new behavior without extra boilerplate.

Given this flag can materially change how program tests bootstrapping behaves, I’d recommend:

  • Adding a brief doc comment explaining exactly what gets auto-registered and in which scenarios.
  • (Optional) Providing a small helper like with_auto_register_custom_programs_for_pda_compression(bool) to make toggling this behavior explicit in call sites that care.

That will make it easier for future callers to reason about when this knob should be flipped.

Also applies to: 122-164

program-libs/batched-merkle-tree/Cargo.toml (1)

12-27: Keccak feature wiring is consistent but a bit redundant

Enabling keccak both via the base dependency features (light-hasher = { ..., features = ["poseidon", "keccak"] }, light-compressed-account = { ..., features = ["std", "keccak"] }) and again under solana/pinocchio (light-hasher/keccak, light-compressed-account/keccak) is functionally fine but slightly noisy.

If the intent is “keccak is always on” for this crate, you can drop the per-feature */keccak lines. If you want it gated by solana/pinocchio, move the keccak flag out of the base dependency features and rely solely on the feature lists here to make the behavior clearer.

Also applies to: 49-66

sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)

40-90: Perfect. I now have enough evidence to rewrite this review. The processor does parse the system_program account (line 68 of the processor), but then never uses it—it's dead code. The TODO comment at line 159 of the instruction file confirms the team is uncertain about whether the field is needed. Your concern is spot-on.


System program account is parsed but unused; passing Pubkey::default() creates a trap for future changes

The processor correctly expects six accounts in order: token_account, mint, payer, compressible_config, system_program, and rent_payer. However, the system_program field (line 68 of programs/compressed-token/program/src/create_token_account.rs) is parsed into the CompressibleAccounts struct but then never dereferenced or validated anywhere in process_create_token_account().

The actual account creation happens via create_pda_account() and create_account_with_custom_rent_payer(), neither of which references system_program. This means line 81 of the instruction builder—which passes Pubkey::default()—works today only because the field is ignored.

The risk: If the processor is ever updated to validate that this slot contains the actual system program, or if a CPI helper assumes it's wired correctly, the placeholder will silently cause failures. The TODO comment at line 159 acknowledges this uncertainty.

Recommendation: Either remove the unused account meta and update the account layout documentation, or—if the design intends to reserve this slot—wire the actual system program and document why it's held. Don't leave a dead field with a placeholder; it invites bugs down the road.

sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)

40-76: Remove the placeholder 4th account; provide only the 3 required accounts (source, destination, authority)

The official documentation and on-chain implementation confirm the issue. The CToken transfer instruction requires a minimum of 3 accounts: source, destination, and authority. The 4th account (payer) is truly optional—it's only needed when accounts have compressible extensions and require rent top-ups.

Your implementation unconditionally adds Pubkey::default() as the 4th account, which causes a critical problem: when the on-chain program attempts to use it as a payer for compressible account top-ups, it will try to charge an all-zeros pubkey, causing the transaction to fail or behave unexpectedly at runtime.

Compare your implementation to the correct one in sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (line 18), which provides only 3 accounts. Your TODO comment even directs you to "consume the variant from compressed-token-sdk instead"—that's the right call. Either:

  • Remove the 4th account entirely and let higher-level code add a payer if needed (preferred), or
  • Refactor to accept an optional payer: Option<Pubkey> parameter so callers can supply it when compressible top-ups are anticipated.

Location: sdk-libs/token-client/src/actions/ctoken_transfer.rs, line 64

forester/Cargo.toml (1)

31-83: Avoid duplicating light-compressed-token-sdk in both deps and dev-deps

You already depend on light-compressed-token-sdk in the main [dependencies] section:

light-compressed-token-sdk = { workspace = true }

and you add the same line again under [dev-dependencies].

Tests and examples can use crates listed in [dependencies], so the extra dev‑dependency entry is usually unnecessary unless you need a different feature set or version for tests (which you don’t here).

Consider dropping the [dev-dependencies] copy to keep the manifest minimal and avoid confusion about which entry is authoritative.

sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (1)

37-48: TODO flagged: Token program should be determined dynamically.

The token program is currently hardcoded to SPL_TOKEN_PROGRAM_ID, but Token-2022 accounts require a different program ID. The account data fetched on line 25-31 includes the owner field which identifies the token program.

Do you want me to generate an implementation that dynamically determines the correct token program from the source account's owner field? This would involve:

  1. Checking token_account_info.owner to distinguish between SPL Token and Token-2022
  2. Passing the correct program ID to the instruction builder

Alternatively, I can open an issue to track this work.

sdk-libs/program-test/src/program_test/test_rpc.rs (1)

5-20: Verified: cfg guards are sound; nit #1 (unnecessary clone) is a straightforward inefficiency

Your analysis is spot-on. I've confirmed:

  • The auto_mine_cold_state_programs field exists at light_program_test.rs:36-37, properly guarded by #[cfg(feature = "v2")]
  • Cargo.toml confirms devenv = ["v2", ...], so the field is always present when either feature enables compilation
  • The loop at test_rpc.rs:160-162 indeed calls .clone() unnecessarily on the vector

Two refinements to action:

  1. Nit #1 is a real inefficiency. Since Pubkey is Copy, iterate by reference and dereference:

    -        for program_id in self.auto_mine_cold_state_programs.clone() {
    -            crate::compressible::auto_compress_program_pdas(self, program_id).await?;
    -        }
    +        for &program_id in &self.auto_mine_cold_state_programs {
    +            crate::compressible::auto_compress_program_pdas(self, program_id).await?;
    +        }
  2. Nit #2 stands as written. The CompressibleAccountStore recreation on each warp is fine for correctness; if historical state tracking becomes needed later, storing it on LightProgramTest would be the play.

sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

171-227: Signer selection logic is reasonable but parameter naming is confusing

The new block:

if rent_sponsor_is_signer {
    packed_account_metas[idx.authority_index as usize].is_signer = true;
} else {
    packed_account_metas[idx.owner_index as usize].is_signer = true;
}

effectively means “if with_compression_authority (call site), mark the compression authority account as signer; otherwise, mark the owner as signer”. That’s exactly what you want for the two modes of compress_and_close_ctoken_accounts_with_indices, but the parameter name rent_sponsor_is_signer no longer reflects what’s happening (the rent sponsor itself never becomes signer here).

To avoid confusion for future readers, I’d suggest either:

  • Renaming the parameter to something like authority_is_signer or with_compression_authority, or
  • Adding a short doc comment near the function signature explaining that rent_sponsor_is_signer == true implies “compression authority closes & signs”.

Behavior looks correct; this is a clarity/maintainability tweak.

Please verify that all call sites indeed pass with_compression_authority into this parameter and not something rent‑sponsor related.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce2203 and 7d01bad.

⛔ Files ignored due to path filters (77)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/cli-v1.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • cli/src/commands/token-balance/index.ts is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/account-compression-test/Cargo.toml is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/create.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/shared.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/mint/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/v1.rs is excluded by none and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • program-tests/system-cpi-test/Cargo.toml is excluded by none and included by none
  • program-tests/system-cpi-test/tests/test.rs is excluded by none and included by none
  • program-tests/utils/src/assert_mint_to_compressed.rs is excluded by none and included by none
  • program-tests/utils/src/assert_token_tx.rs is excluded by none and included by none
  • program-tests/utils/src/assert_transfer2.rs is excluded by none and included by none
  • program-tests/utils/src/conversions.rs is excluded by none and included by none
  • program-tests/utils/src/e2e_test_env.rs is excluded by none and included by none
  • program-tests/utils/src/spl.rs is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/client-test/tests/light_client.rs is excluded by none and included by none
  • sdk-tests/client-test/tests/light_program_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/processor.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/variant.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/package.json is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/constants.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/mod.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/state.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/user_record_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_four_transfer2.rs is excluded by none and included by none
📒 Files selected for processing (93)
  • forester/Cargo.toml (1 hunks)
  • forester/tests/e2e_test.rs (1 hunks)
  • forester/tests/legacy/batched_state_async_indexer_test.rs (1 hunks)
  • program-libs/batched-merkle-tree/Cargo.toml (3 hunks)
  • program-libs/compressible/src/config.rs (1 hunks)
  • programs/compressed-token/program/src/shared/token_input.rs (1 hunks)
  • programs/system/Cargo.toml (1 hunks)
  • sdk-libs/client/Cargo.toml (3 hunks)
  • sdk-libs/client/src/constants.rs (1 hunks)
  • sdk-libs/client/src/indexer/types.rs (2 hunks)
  • sdk-libs/client/src/rpc/lut.rs (1 hunks)
  • sdk-libs/client/src/rpc/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/Cargo.toml (1 hunks)
  • sdk-libs/compressed-token-sdk/src/account2.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/error.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (5 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/mod.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs (0 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/pack.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/token_pool.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/utils.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/tests/pack_test.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/lib.rs (0 hunks)
  • sdk-libs/compressed-token-types/src/token_data.rs (0 hunks)
  • sdk-libs/compressible-client/Cargo.toml (1 hunks)
  • sdk-libs/compressible-client/src/get_compressible_account.rs (1 hunks)
  • sdk-libs/compressible-client/src/lib.rs (1 hunks)
  • sdk-libs/macros/ADDITIONAL_DRY_IMPROVEMENTS.md (1 hunks)
  • sdk-libs/macros/DRY_REFACTORING_VISUAL.md (1 hunks)
  • sdk-libs/macros/FINAL_AUDIT_REPORT.md (1 hunks)
  • sdk-libs/macros/REFACTORING_SUMMARY.md (1 hunks)
  • sdk-libs/macros/src/compressible/GUIDE.md (1 hunks)
  • sdk-libs/macros/src/compressible/README.md (1 hunks)
  • sdk-libs/macros/src/compressible/decompress_context.rs (1 hunks)
  • sdk-libs/macros/src/compressible/instructions.rs (1 hunks)
  • sdk-libs/macros/src/compressible/mod.rs (1 hunks)
  • sdk-libs/macros/src/compressible/pack_unpack.rs (1 hunks)
  • sdk-libs/macros/src/compressible/seed_providers.rs (1 hunks)
  • sdk-libs/macros/src/compressible/traits.rs (1 hunks)
  • sdk-libs/macros/src/compressible/utils.rs (1 hunks)
  • sdk-libs/macros/src/compressible/variant_enum.rs (1 hunks)
  • sdk-libs/macros/src/cpi_signer.rs (1 hunks)
  • sdk-libs/macros/src/lib.rs (4 hunks)
  • sdk-libs/macros/src/rent_sponsor.rs (1 hunks)
  • sdk-libs/macros/src/utils.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (3 hunks)
  • sdk-libs/program-test/src/compressible.rs (4 hunks)
  • sdk-libs/program-test/src/indexer/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/indexer/test_indexer.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/mod.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (7 hunks)
  • sdk-libs/program-test/src/utils/mod.rs (1 hunks)
  • sdk-libs/program-test/src/utils/simulation.rs (1 hunks)
  • sdk-libs/sdk-types/src/lib.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (3 hunks)
  • sdk-libs/sdk/src/account.rs (1 hunks)
  • sdk-libs/sdk/src/address.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/close.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (1 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
  • sdk-libs/sdk/src/proof.rs (1 hunks)
  • sdk-libs/sdk/src/token.rs (0 hunks)
  • sdk-libs/token-client/Cargo.toml (1 hunks)
  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1 hunks)
  • sdk-libs/token-client/src/actions/ctoken_transfer.rs (6 hunks)
  • sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3 hunks)
  • sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (4 hunks)
  • sdk-libs/token-client/src/instructions/create_spl_mint.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_action.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2 hunks)
  • sdk-libs/token-client/src/lib.rs (1 hunks)
💤 Files with no reviewable changes (4)
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs
  • sdk-libs/compressed-token-types/src/token_data.rs
  • sdk-libs/compressed-token-types/src/lib.rs
  • sdk-libs/sdk/src/token.rs
🧰 Additional context used
🧬 Code graph analysis (47)
sdk-libs/client/src/rpc/mod.rs (1)
sdk-libs/client/src/rpc/lut.rs (1)
  • load_lookup_table (20-37)
sdk-libs/program-test/src/utils/mod.rs (1)
sdk-libs/program-test/src/utils/simulation.rs (1)
  • simulate_cu (12-36)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)
  • create_compressible_token_account_instruction (40-90)
sdk-libs/sdk/src/address.rs (1)
program-libs/compressed-account/src/address.rs (1)
  • derive_address (14-26)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/token-client/src/instructions/mint_action.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs (1)
  • create_mint_action (444-446)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (2)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/compressed-token-sdk/src/lib.rs (2)
sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (35-160)
sdk-libs/compressed-token-sdk/src/utils.rs (2)
  • account_meta_from_account_info (48-54)
  • is_ctoken_account (22-37)
sdk-libs/macros/src/compressible/pack_unpack.rs (2)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • is_copy_type (45-76)
  • is_pubkey_type (95-105)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • insert_or_get (301-303)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • last_claimed_slot (126-128)
sdk-libs/macros/src/compressible/decompress_context.rs (4)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (11)
  • compressible (201-201)
  • handle_packed_pda_variant (150-218)
  • rent_sponsor (67-67)
  • fee_payer (65-65)
  • config (66-66)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
  • collect_pda_and_token (78-88)
  • process_tokens (94-108)
sdk-libs/sdk-types/src/cpi_accounts/v2.rs (1)
  • system_accounts_end_offset (161-173)
sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (35-160)
sdk-libs/macros/src/lib.rs (1)
  • derive_decompress_context (511-516)
sdk-libs/macros/src/rent_sponsor.rs (1)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_rent_sponsor_pda (546-548)
  • derive_light_rent_sponsor (563-565)
sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (8)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
  • fee_payer (65-65)
  • ctoken_program (69-69)
  • ctoken_rent_sponsor (68-68)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
  • config (66-66)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (2)
  • create_token_account (92-118)
  • create_ctoken_account_signed (122-171)
sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (1)
  • decompress_full_ctoken_accounts_with_indices (48-130)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)
  • ctoken_rent_sponsor (68-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (4)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1)
  • transfer_ctoken_signed (54-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs (1)
  • create_transfer2_instruction (71-141)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • is_ctoken_account (22-37)
sdk-libs/compressed-token-sdk/src/account2.rs (5)
  • mint (362-364)
  • compress_spl (193-220)
  • decompress_ctoken (224-247)
  • compress_ctoken (169-190)
  • decompress_spl (250-279)
sdk-libs/macros/src/cpi_signer.rs (2)
sdk-libs/macros/src/lib.rs (1)
  • derive_light_cpi_signer (530-532)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/macros/src/compressible/variant_enum.rs (3)
sdk-libs/macros/src/compressible/decompress_context.rs (2)
  • parse (16-20)
  • parse (28-32)
sdk-libs/macros/src/compressible/traits.rs (3)
  • parse (25-30)
  • parse (34-38)
  • input (27-27)
sdk-libs/sdk/src/compressible/compression_info.rs (4)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
sdk-libs/macros/src/compressible/instructions.rs (1)
  • syn (303-303)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • compressible (201-201)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/macros/src/compressible/utils.rs (1)
sdk-libs/macros/src/accounts.rs (1)
  • type_path (494-502)
sdk-libs/compressed-token-sdk/src/ctoken.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (3)
  • create_compressed_mint (186-190)
  • derive_cmint_from_spl_mint (204-210)
  • find_spl_mint_address (212-217)
program-libs/compressible/src/config.rs (3)
  • ctoken_v1_config_pda (248-250)
  • ctoken_v1_rent_sponsor_pda (283-285)
  • ctoken_v1_compression_authority_pda (275-281)
sdk-libs/macros/src/compressible/traits.rs (2)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • extract_fields_from_item_struct (11-21)
  • is_copy_type (45-76)
sdk-libs/sdk/src/compressible/compression_info.rs (4)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)
  • compressible (201-201)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_decompressed (104-112)
sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1)
  • create_transfer_spl_to_ctoken_instruction (23-87)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (1)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1)
  • create_transfer_ctoken_to_spl_instruction (91-160)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (6)
  • create_compressed_mint (186-190)
  • create_compressed_mint_cpi (41-106)
  • create_compressed_mint_cpi_write (123-183)
  • derive_cmint_from_spl_mint (204-210)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/compressible-client/src/get_compressible_account.rs (1)
program-libs/compressible/src/config.rs (1)
  • try_deserialize (98-112)
sdk-libs/compressed-token-sdk/tests/pack_test.rs (1)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_unique (200-202)
sdk-libs/sdk/src/lib.rs (2)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)
  • compressible (201-201)
sdk-libs/sdk/src/compressible/config.rs (3)
  • process_initialize_compression_config_account_info (179-279)
  • process_initialize_compression_config_checked (463-502)
  • process_update_compression_config (298-365)
sdk-libs/program-test/src/program_test/compressible_setup.rs (1)
sdk-libs/compressible-client/src/lib.rs (2)
  • initialize_compression_config (81-128)
  • update_compression_config (131-165)
sdk-libs/macros/src/lib.rs (9)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (186-225)
sdk-libs/macros/src/cpi_signer.rs (1)
  • derive_light_cpi_signer (47-97)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (76-110)
  • derive_light_rent_sponsor (119-166)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • fee_payer (65-65)
  • config (66-66)
  • rent_sponsor (67-67)
  • compressible (201-201)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-40)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
sdk-libs/program-test/src/program_test/config.rs (1)
  • new (60-69)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (2)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_from_config (89-100)
sdk-libs/sdk/src/account.rs (1)
  • new_init (250-272)
sdk-libs/macros/src/compressible/instructions.rs (6)
sdk-libs/macros/src/compressible/variant_enum.rs (3)
  • syn (22-22)
  • parse (14-18)
  • compressed_account_variant (21-247)
sdk-libs/macros/src/compressible/decompress_context.rs (3)
  • parse (16-20)
  • parse (28-32)
  • generate_decompress_context_trait_impl (35-184)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (13)
  • compressible (201-201)
  • derive_pda_seeds_with_accounts (122-127)
  • handle_packed_pda_variant (150-218)
  • rent_sponsor (67-67)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
  • collect_pda_and_token (78-88)
  • fee_payer (65-65)
  • config (66-66)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
sdk-libs/macros/src/compressible/seed_providers.rs (2)
  • generate_ctoken_seed_provider_implementation (27-378)
  • generate_client_seed_functions (381-514)
sdk-libs/sdk/src/compressible/compress_runtime.rs (6)
  • rent_sponsor (13-13)
  • fee_payer (11-11)
  • config (12-12)
  • compression_authority (14-14)
  • compress_pda_account (16-23)
  • process_compress_pda_accounts_idempotent (28-109)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
  • prepare_account_for_compression (29-136)
sdk-libs/compressed-token-sdk/src/instructions/mod.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (4)
  • transfer_interface (354-437)
  • create_transfer_ctoken_to_spl_instruction (91-160)
  • create_transfer_spl_to_ctoken_instruction (23-87)
  • transfer_interface_signed (441-539)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3)
  • create_compressible_token_account_instruction (40-90)
  • create_ctoken_account_signed (122-171)
  • create_token_account (92-118)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1)
  • transfer_ctoken_signed (54-68)
sdk-libs/program-test/src/compressible.rs (3)
program-libs/compressible/src/config.rs (1)
  • new_ctoken (163-182)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (70-77)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (289-359)
sdk-libs/compressible-client/src/lib.rs (2)
sdk-libs/program-test/src/program_test/compressible_setup.rs (2)
  • initialize_compression_config (74-108)
  • update_compression_config (124-145)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (70-77)
sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (1)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • packed_accounts (348-351)
sdk-libs/token-client/src/instructions/create_spl_mint.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/sdk/src/compressible/config.rs (1)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • config (12-12)
  • rent_sponsor (13-13)
  • compression_authority (14-14)
sdk-libs/sdk/src/compressible/mod.rs (7)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-40)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • config (66-66)
  • check_account_types (132-145)
  • handle_packed_pda_variant (150-218)
  • process_decompress_accounts_idempotent (223-348)
sdk-libs/sdk/src/compressible/compress_runtime.rs (2)
  • config (12-12)
  • process_compress_pda_accounts_idempotent (28-109)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
  • prepare_account_for_compression (29-136)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1)
  • prepare_compressed_account_on_init (35-108)
sdk-libs/sdk/src/compressible/config.rs (3)
  • process_initialize_compression_config_account_info (179-279)
  • process_initialize_compression_config_checked (463-502)
  • process_update_compression_config (298-365)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • into_compressed_meta_with_address (25-44)
  • prepare_account_for_decompression_idempotent (82-148)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (5)
sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (2)
  • get_seeds (18-22)
  • get_authority_seeds (25-29)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • fee_payer (11-11)
  • config (12-12)
  • rent_sponsor (13-13)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • prepare_account_for_decompression_idempotent (82-148)
  • into_compressed_meta_with_address (25-44)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/compressed-token-sdk/src/pack.rs (1)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • get_token_account_balance (11-20)
sdk-libs/program-test/src/program_test/test_rpc.rs (2)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
  • new (68-382)
sdk-libs/program-test/src/compressible.rs (3)
  • new (53-73)
  • claim_and_compress (81-172)
  • auto_compress_program_pdas (175-229)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/program-test/src/compressible.rs (1)
  • new (53-73)
sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • derive_cmint_from_spl_mint (204-210)
sdk-libs/compressed-token-sdk/src/token_pool.rs (2)
  • derive_token_pool (29-36)
  • find_token_pool_pda_with_index (13-21)
🪛 GitHub Actions: examples-tests
sdk-libs/macros/src/lib.rs

[error] 319-320: Doc-test failed: cannot find attribute add_compressible_instructions in this scope (sdk-libs/macros/src/lib.rs:319-320)

🪛 GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-compr...
sdk-libs/macros/src/lib.rs

[failure] 325-325:
cannot find attribute program in this scope


[failure] 320-320:
cannot find attribute add_compressible_instructions in this scope

🪛 LanguageTool
sdk-libs/macros/ADDITIONAL_DRY_IMPROVEMENTS.md

[style] ~221-~221: This wording could be more concise.
Context: ...e subtle (field extraction patterns) 2. Smaller in size but widely spread (12+ duplicates) 3. E...

(ADJECTIVE_IN_ATTRIBUTE)

sdk-libs/macros/REFACTORING_SUMMARY.md

[grammar] ~76-~76: Use a hyphen to join words.
Context: ...## 1. Single Source of Truth - Type checking logic exists in exactly one pla...

(QB_NEW_EN_HYPHEN)

sdk-libs/macros/FINAL_AUDIT_REPORT.md

[grammar] ~329-~329: Use a hyphen to join words.
Context: ...ies 3. When checking types: Use type checking utilities 4. **When generating ...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
sdk-libs/macros/src/compressible/GUIDE.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


32-32: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


44-44: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


54-54: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


76-76: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


81-81: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


139-139: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


158-158: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


174-174: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)

sdk-libs/macros/REFACTORING_SUMMARY.md

111-111: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


130-130: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

sdk-libs/macros/FINAL_AUDIT_REPORT.md

105-105: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


126-126: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Forester e2e test
  • GitHub Check: Test program-libs-fast
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: programs (compressed-token-and-e2e, ["cargo-test-sbf -p compressed-token-test --test v1", "cargo-...
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
  • GitHub Check: programs (compressed-token-batched-tree, ["cargo-test-sbf -p compressed-token-test -- test_transf...
  • GitHub Check: stateless-js-v2
  • GitHub Check: stateless-js-v1

Comment on lines +1 to +37
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
use solana_message::AddressLookupTableAccount;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;

use crate::rpc::errors::RpcError;

/// Gets a lookup table account state from the network.
///
/// # Arguments
///
/// * `client` - The RPC client to use to get the lookup table account state.
/// * `lookup_table_address` - The address of the lookup table account to get.
///
/// # Returns
///
/// * `AddressLookupTableAccount` - The lookup table account state.
pub fn load_lookup_table(
client: &RpcClient,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, RpcError> {
let raw_account = client.get_account(lookup_table_address)?;
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
let address_lookup_table_account = AddressLookupTableAccount {
key: lookup_table_address.to_bytes().into(),
addresses: address_lookup_table
.addresses
.iter()
.map(|p| p.to_bytes().into())
.collect(),
};
Ok(address_lookup_table_account)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Validate LUT account owner and simplify Pubkey conversions

Two small but important points here:

  • load_lookup_table trusts any account at lookup_table_address and only relies on AddressLookupTable::deserialize. It’s usually safer to also assert raw_account.owner == program::id() (the LUT program) before deserializing, so callers don’t accidentally point this at a wrong account and get a confusing “deserialize” error instead of a clear owner mismatch.

  • AddressLookupTableAccount.key and .addresses can likely be constructed directly from the existing Pubkey values (e.g. key: *lookup_table_address, addresses: address_lookup_table.addresses.clone()), without the intermediate to_bytes().into() hop, assuming the Pubkey types from solana_message and solana_pubkey are compatible as expected. This would remove some noise and make intent clearer.

Please sanity‑check the LUT owner ID and the concrete Pubkey types in solana_message::AddressLookupTableAccount vs solana_pubkey::Pubkey against your Solana dependency docs to ensure these assumptions hold.

Comment on lines +406 to +493
/// CPI wrapper to create a compressible c-token associated token account.
#[allow(clippy::too_many_arguments)]
pub fn create_associated_ctoken_account<'info>(
payer: AccountInfo<'info>,
associated_token_account: AccountInfo<'info>,
system_program: AccountInfo<'info>,
compressible_config: AccountInfo<'info>,
rent_sponsor: AccountInfo<'info>,
authority: AccountInfo<'info>,
mint: Pubkey,
bump: u8,
pre_pay_num_epochs: Option<u8>,
lamports_per_write: Option<u32>,
) -> std::result::Result<(), solana_program_error::ProgramError> {
let inputs = CreateCompressibleAssociatedTokenAccountInputs {
payer: *payer.key,
owner: *authority.key,
mint,
compressible_config: *compressible_config.key,
rent_sponsor: *rent_sponsor.key,
pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1),
lamports_per_write,
token_account_version: TokenDataVersion::ShaFlat,
};

// TODO: switch to wrapper ixn using accounts instead of ixdata.
let ix = create_compressible_associated_token_account_with_bump(
inputs,
*associated_token_account.key,
bump,
)?;

solana_cpi::invoke(
&ix,
&[
payer,
associated_token_account,
system_program,
compressible_config,
rent_sponsor,
authority,
],
)
}

/// CPI wrapper to create a compressible c-token associated token account
/// idempotently.
#[allow(clippy::too_many_arguments)]
pub fn create_associated_ctoken_account_idempotent<'info>(
payer: AccountInfo<'info>,
associated_token_account: AccountInfo<'info>,
system_program: AccountInfo<'info>,
compressible_config: AccountInfo<'info>,
rent_sponsor: AccountInfo<'info>,
authority: Pubkey,
mint: Pubkey,
bump: u8,
pre_pay_num_epochs: Option<u8>,
lamports_per_write: Option<u32>,
) -> std::result::Result<(), solana_program_error::ProgramError> {
let inputs = CreateCompressibleAssociatedTokenAccountInputs {
payer: *payer.key,
owner: authority,
mint,
compressible_config: *compressible_config.key,
rent_sponsor: *rent_sponsor.key,
pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1),
lamports_per_write,
token_account_version: TokenDataVersion::ShaFlat,
};

let ix = create_compressible_associated_token_account_with_bump_and_mode::<true>(
inputs,
*associated_token_account.key,
bump,
)?;

solana_cpi::invoke(
&ix,
&[
payer,
associated_token_account,
system_program,
compressible_config,
rent_sponsor,
],
)
}
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

CPI wrappers will fail because system_program AccountMeta does not match the passed AccountInfo

In both create_associated_ctoken_account and create_associated_ctoken_account_idempotent you build the Instruction via create_compressible_associated_token_account_with_bump[_and_mode], which internally sets:

solana_instruction::AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false)

for the system_program slot, while the CPI wrappers pass a real system_program: AccountInfo<'info> into solana_cpi::invoke/invoke_signed. invoke compares each AccountMeta.pubkey with the corresponding AccountInfo.key and returns ProgramError::InvalidArgument if they differ, so these wrappers will always error unless your “system program” account actually has the all‑zeros pubkey.

Additionally:

  • The non‑idempotent wrapper passes an extra authority account to invoke, but there is no matching AccountMeta for it in the instruction, and it is not marked as a signer in the instruction either. That means the compressed-token program will never treat authority as a signer. If only payer is meant to sign, that’s fine, but then including authority here is misleading; if authority must sign, it needs to be reflected in the Instruction’s accounts.

To fix this, you’ll need to:

  • Change create_ata_instruction_unified (and the ATA2 variant) to use the real system program ID for the third AccountMeta so it matches the system_program AccountInfo you pass into invoke.
  • Either drop authority from the non‑idempotent CPI accounts list, or (if the c-token program expects it) add a corresponding AccountMeta::new_readonly(authority, true) in the instruction so it is actually used as a signer.

Please verify this behavior by checking how solana_cpi::invoke validates account keys and by confirming the expected account list for the c-token ATA creation instruction in your program docs.


I'll verify the critical claims in this review by examining the instruction builders and CPI invoke mechanics.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the definition of create_compressible_associated_token_account_with_bump
rg -n "fn create_compressible_associated_token_account_with_bump" --type rust

Length of output: 328


🏁 Script executed:

#!/bin/bash
# Find create_ata_instruction_unified to check system_program AccountMeta construction
rg -n "fn create_ata_instruction_unified" --type rust -A 30

Length of output: 3994


🏁 Script executed:

#!/bin/bash
# Search for where system_program AccountMeta is built with potential hardcoded zero pubkey
rg -n "AccountMeta.*\[0;\s*32\]" --type rust

Length of output: 379


🏁 Script executed:

#!/bin/bash
# Check if authority is included in instruction accounts
rg -n "authority.*AccountMeta\|AccountMeta.*authority" --type rust -B 2 -A 2

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# View the full instruction construction with all accounts
sed -n '190,250p' sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs

Length of output: 2447


🏁 Script executed:

#!/bin/bash
# Search for the accounts vec in create_ata_instruction_unified to see authority handling
rg -n "vec!\[" sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs -A 15 | grep -A 15 "line 206\|line 207\|line 208"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get a wider view of the instruction building—search for Instruction::new pattern
sed -n '200,230p' sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs

Length of output: 1204


🏁 Script executed:

#!/bin/bash
# Check solana_cpi documentation or implementation to confirm invoke validation
rg -n "solana_cpi" sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs -B 3 -A 3

Length of output: 238



---

🏁 Script executed:

```shell
#!/bin/bash
# Check where solana_cpi is imported from and understand its invoke signature
rg -n "use.*solana_cpi\|mod.*solana_cpi" --type rust -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the actual CPI invoke call in non-idempotent version (lines 438-450)
sed -n '438,450p' sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Look at the idempotent version CPI call (lines 483-495)
sed -n '483,495p' sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs

Length of output: 210


🏁 Script executed:

#!/bin/bash
# Check if authority is ever referenced as a signer in instruction metadata
rg -n "true.*signer\|signer.*true" sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs

Length of output: 0



---

🌐 Web query:

```web
solana_cpi invoke account validation mismatch error

💡 Result:

Common causes and fixes for "account validation mismatch" (CPI invoke) on Solana

Summary (quick):

  • The callee program validates the AccountMetas you passed and rejects when something differs (order, is_signer/is_writable, owner, missing account, or PDA signature mismatch). Fix by matching the callee’s expected AccountMeta list exactly and using invoke_signed when a PDA must sign. [1][2]

Checklist to debug & fix

  1. Ensure account order matches the callee’s instruction AccountMeta order exactly (client/order you pass to Instruction must match the callee). [1][5]
  2. Verify each AccountMeta flags: is_signer and is_writable must match what the callee expects; attempting to write without writable flagged or sign without signer will fail. [1][2]
  3. Use invoke_signed (not invoke) if the callee expects a PDA to be a signer, and supply the correct seeds — wrong seeds cause “signer privilege escalated” / validation mismatch. [1][2]
  4. Include every account the callee reads or checks (including program accounts like system_program or token program) in the AccountInfo slice passed to invoke. Missing accounts cause invalid-argument/validation errors. [1][3]
  5. Verify account ownership and types: callee may assert an account is owned by a specific program (e.g., token account owner) — mismatches cause failure. [2]
  6. For accounts created by CPI (e.g., system_instruction::create_account), ensure you passed the system program account and correct payer and rent_info as expected. [6]
  7. Use program logs and transaction inspection to see the callee’s error and which check failed. Add logging in both programs and inspect the transaction in Explorer/debugger. [1][4]

Useful references

  • Solana CPIs and invoke/invoke_signed docs (explains ordering, writable/signers, and PDA signing). [1][2]
  • solana-cpi invoke docs (runtime checks, RefCell borrow, common runtime invariants). [2]
  • Examples / community Q&A showing common issues (missing/incorrect program_id, missing account, wrong seeds). [4][6]

If you want, paste:

  • the Instruction creation (program_id + AccountMeta list),
  • the AccountInfo slice you pass to invoke,
  • whether any PDAs sign and the seeds,
    and I’ll point out the exact mismatch.

Fix system_program hardcoded pubkey and remove mismatched authority account from non-idempotent wrapper

The review's concerns are valid and critical. I've confirmed:

  1. system_program mismatch (both wrappers): The instruction at line 208 hardcodes system_program as [0; 32], but both create_associated_ctoken_account and create_associated_ctoken_account_idempotent pass the real system_program AccountInfo to solana_cpi::invoke. During CPI validation, the runtime compares each AccountMeta.pubkey with the corresponding AccountInfo.key; a mismatch causes ProgramError::InvalidArgument. These calls will fail unless your actual system program has the all-zeros pubkey.

  2. Extra authority account in non-idempotent wrapper: The instruction accounts list contains 5 entries (payer, associated_token_account, system_program, compressible_config, rent_sponsor), but create_associated_ctoken_account passes 6 accounts to invoke—including authority. The CPI runtime validates account count and order; passing an extra account not in the instruction will fail validation.

To fix:

  • Change line 208 (and the analogous line 386 for ATA2 variant) from Pubkey::new_from_array([0; 32]) to Pubkey::from(solana_program::system_program::ID) so the instruction's system_program AccountMeta matches what you pass to invoke.
  • Remove authority from the accounts slice passed to solana_cpi::invoke in the non-idempotent wrapper (line 438–450), since there is no corresponding AccountMeta in the instruction and authority is not marked as a signer. If the compressed-token program documentation specifies that authority must be included and must sign, then add it as an AccountMeta::new_readonly(authority, true) in the instruction builder instead.
🤖 Prompt for AI Agents
In
sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs
(context around lines 406-493) the CPI invocation has two issues: the
instruction builder hardcodes system_program as Pubkey::new_from_array([0; 32])
(occurs earlier at ~line 208 and similarly at ~line 386 for ATA2) which will
never match the real system program AccountInfo passed to solana_cpi::invoke,
and the non-idempotent wrapper passes an extra authority AccountInfo to invoke
(around lines 438-450) even though the instruction's AccountMeta list does not
include it. Fix by replacing the hardcoded all-zero pubkey with the real system
program id (use Pubkey::from(solana_program::system_program::ID)) in both
instruction builders, and remove the authority AccountInfo from the accounts
slice passed to create_associated_ctoken_account::solana_cpi::invoke; if
authority must be part of the instruction and be a signer, instead add it as an
AccountMeta (e.g., new_readonly/new) in the instruction builder so the invoke
account list and AccountMeta list match exactly.

Comment on lines +137 to +149
/// Derive address from PDA using Pubkey types.
pub fn derive_compressed_address(
account_address: &Pubkey,
address_tree_pubkey: &Pubkey,
program_id: &Pubkey,
) -> [u8; 32] {
derive_address(
&[account_address.to_bytes().as_ref()],
address_tree_pubkey,
program_id,
)
.0
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

derive_compressed_address helper is correct; add docs/tests to align with the rest of the module

The new derive_compressed_address simply delegates to derive_address with account_address.to_bytes() as the single seed and returns the derived address, which is consistent with the existing v2 APIs.

To keep this on par with the rest of the address helpers, I’d recommend:

  • Adding a short doc comment (with an example) explaining when to prefer this over calling derive_address directly.
  • Adding a small test (behind cfg(feature = "v2")) that asserts a stable mapping for a known (account_address, address_tree_pubkey, program_id) triple, similar in spirit to the existing v1 tests, so any future hash-function changes are caught early.

That will make this new API easier to discover and less likely to regress.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/address.rs around lines 137 to 149, add a short rustdoc for
derive_compressed_address explaining it derives a compressed address from a PDA
using a single Pubkey seed and when to prefer it over derive_address (include a
short usage example); also add a unit test gated with cfg(feature = "v2") that
constructs a fixed (account_address, address_tree_pubkey, program_id) triple and
asserts the returned [u8;32] matches a hard-coded expected value to catch future
hashing changes — place the test alongside existing v2 tests and ensure it uses
deterministic keys so the mapping is stable.

Comment on lines 6 to 12
use light_compressed_token_sdk::{
instructions::{
create_spl_mint_instruction as sdk_create_spl_mint_instruction, find_spl_mint_address,
CreateSplMintInputs,
},
token_pool::derive_token_pool,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Import re-org to light_compressed_token_sdk modules is consistent with the new layout

Using instructions::{create_spl_mint_instruction, find_spl_mint_address, CreateSplMintInputs} plus token_pool::derive_token_pool matches the restructured SDK modules, and the call sites line up correctly, so behavior is preserved.

While you’re in here, consider gating or removing the println!("instruction {:?}", instruction); at the end of the function, since unconditional stdout logging in a client library can get noisy for downstream consumers.

Also applies to: 102-117

🤖 Prompt for AI Agents
In sdk-libs/token-client/src/instructions/create_spl_mint.rs around lines 6 to
12 (and also apply the same change to lines 102 to 117), the import
re-organization to use instructions::{create_spl_mint_instruction,
find_spl_mint_address, CreateSplMintInputs} and token_pool::derive_token_pool is
correct; remove or gate the unconditional println!("instruction {:?}",
instruction); at the end of the function to avoid noisy stdout in a client
library — either delete the println or replace it with a debug-level log behind
a feature flag or configurable logger (e.g., using the crate's logging facility
or a cfg feature) so downstream consumers don't get unintentional console
output.

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.

Review continued from previous batch...

Comment on lines +162 to +247
/// Transfer SPL tokens to compressed tokens
#[allow(clippy::too_many_arguments)]
pub fn transfer_spl_to_ctoken<'info>(
payer: AccountInfo<'info>,
authority: AccountInfo<'info>,
source_spl_token_account: AccountInfo<'info>,
destination_ctoken_account: AccountInfo<'info>,
mint: AccountInfo<'info>,
spl_token_program: AccountInfo<'info>,
compressed_token_pool_pda: AccountInfo<'info>,
compressed_token_pool_pda_bump: u8,
compressed_token_program_authority: AccountInfo<'info>,
amount: u64,
) -> Result<(), ProgramError> {
let instruction = create_transfer_spl_to_ctoken_instruction(
*source_spl_token_account.key,
*destination_ctoken_account.key,
amount,
*authority.key,
*mint.key,
*payer.key,
*compressed_token_pool_pda.key,
compressed_token_pool_pda_bump,
*spl_token_program.key,
)
.map_err(|_| ProgramError::InvalidInstructionData)?;

// let mut account_infos = remaining_accounts.to_vec();
let account_infos = vec![
payer,
compressed_token_program_authority,
mint, // Index 0: Mint
destination_ctoken_account, // Index 1: Destination owner
authority, // Index 2: Authority (signer)
source_spl_token_account, // Index 3: Source SPL token account
compressed_token_pool_pda, // Index 4: Token pool PDA
spl_token_program, // Index 5: SPL Token program
];

invoke(&instruction, &account_infos)?;
Ok(())
}

// TODO: must test this.
/// Transfer SPL tokens to compressed tokens via CPI signer
#[allow(clippy::too_many_arguments)]
pub fn transfer_spl_to_ctoken_signed<'info>(
payer: AccountInfo<'info>,
authority: AccountInfo<'info>,
source_spl_token_account: AccountInfo<'info>,
destination_ctoken_account: AccountInfo<'info>,
mint: AccountInfo<'info>,
spl_token_program: AccountInfo<'info>,
compressed_token_pool_pda: AccountInfo<'info>,
compressed_token_pool_pda_bump: u8,
compressed_token_program_authority: AccountInfo<'info>,
amount: u64,
signer_seeds: &[&[&[u8]]],
) -> Result<(), ProgramError> {
let instruction = create_transfer_spl_to_ctoken_instruction(
*source_spl_token_account.key,
*destination_ctoken_account.key,
amount,
*authority.key,
*mint.key,
*payer.key,
*compressed_token_pool_pda.key,
compressed_token_pool_pda_bump,
*spl_token_program.key,
)
.map_err(|_| ProgramError::InvalidInstructionData)?;

let account_infos = vec![
payer,
compressed_token_program_authority,
mint, // Index 0: Mint
destination_ctoken_account, // Index 1: Destination owner
authority, // Index 2: Authority (signer)
source_spl_token_account, // Index 3: Source SPL token account
compressed_token_pool_pda, // Index 4: Token pool PDA
spl_token_program, // Index 5: SPL Token program
];

invoke_signed(&instruction, &account_infos, signer_seeds)?;
Ok(())
}
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

Confirm Transfer2 account order matches account_infos (especially program authority)

The SPL↔ctoken wrappers construct account_infos as [payer, compressed_token_program_authority, mint, dest_or_source, authority, token_account, pool_pda, spl_token_program], while the Instruction.accounts list is derived solely from Transfer2AccountsMetaConfig::new_decompressed_accounts_only(payer, packed_accounts). There’s no explicit mention of compressed_token_program_authority in packed_accounts, so a mismatch between the meta config’s expected ordering and the account_infos slice would cause CPI failures or silently unused accounts. Please verify that get_transfer2_instruction_account_metas includes an AccountMeta for compressed_token_program_authority in exactly the position assumed by these wrappers; if not, the meta config or wrapper ordering needs to be adjusted.

Also applies to: 249-333


🏁 Script executed:

#!/bin/bash
# Confirm how Transfer2AccountsMetaConfig lays out account metas.
rg -n "struct Transfer2AccountsMetaConfig" sdk-libs/compressed-token-sdk -S
rg -n "new_decompressed_accounts_only" sdk-libs/compressed-token-sdk -n -S

Length of output: 546


🏁 Script executed:

#!/bin/bash
# Inspect Transfer2AccountsMetaConfig struct and new_decompressed_accounts_only method
cat sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs

Length of output: 4721


🏁 Script executed:

#!/bin/bash
# Find create_transfer_spl_to_ctoken_instruction and understand its account meta construction
rg -n "fn create_transfer_spl_to_ctoken_instruction" sdk-libs/compressed-token-sdk -A 50

Length of output: 5390


🏁 Script executed:

#!/bin/bash
# Get the complete create_transfer_spl_to_ctoken_instruction function to see Instruction construction
sed -n '23,110p' sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs

Length of output: 3068


🏁 Script executed:

#!/bin/bash
# Find what compressed_token_program_authority is and if it equals CPI_AUTHORITY_PDA
rg -n "CPI_AUTHORITY_PDA" sdk-libs/compressed-token-sdk -B2 -A2
rg -n "compressed_token_program_authority" sdk-libs/compressed-token-sdk -B2 -A2 | head -60

Length of output: 20595


Account ordering mismatch causes CPI failures in transfer wrappers

The account_infos construction in transfer_spl_to_ctoken, transfer_spl_to_ctoken_signed, transfer_ctoken_to_spl, and transfer_ctoken_to_spl_signed (lines 162–247, 249–333) is misaligned with the instruction account metas.

The problem: The instruction expects [CPI_AUTHORITY_PDA, payer, ...] but account_infos provides [payer, compressed_token_program_authority, ...]. When invoke() matches accounts positionally, every account gets shifted by one position:

  • Position 0: payer gets matched to CPI_AUTHORITY_PDA
  • Position 1: compressed_token_program_authority gets matched to payer

This causes account validation failures or silent execution with wrong accounts.

Fix: Reorder account_infos to match the instruction meta layout: [CPI_AUTHORITY_PDA, payer, mint, destination/source, authority, source/destination, pool_pda, spl_token_program]. Note that compressed_token_program_authority should map to the CPI_AUTHORITY_PDA constant (verify this is the intended account).

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs around
lines 162–247 (and similarly lines 249–333 for the signed and reverse transfer
wrappers), the account_infos vector is ordered [payer,
compressed_token_program_authority, ...] which is shifted one position relative
to the instruction's expected metas (CPI_AUTHORITY_PDA, payer, ...), causing CPI
mismatches; reorder the account_infos for each transfer wrapper to match the
instruction meta layout so position 0 is compressed_token_program_authority
(CPI_AUTHORITY_PDA), position 1 is payer, then mint, destination/source account,
authority, source/destination token account, compressed_token_pool_pda,
spl_token_program; apply the equivalent reorder to the ctoken->spl functions and
signed variants and verify compressed_token_program_authority indeed represents
the CPI_AUTHORITY_PDA before invoking.

Comment on lines +335 to +437
/// Unified transfer interface for ctoken<->ctoken and ctoken<->spl transfers
///
/// # Arguments
/// * `source_account` - Source token account (can be ctoken or SPL)
/// * `destination_account` - Destination token account (can be ctoken or SPL)
/// * `authority` - Authority for the transfer (must be signer)
/// * `amount` - Amount to transfer
/// * `payer` - Payer for the transaction
/// * `compressed_token_program_authority` - Compressed token program authority
/// * `mint` - Optional mint account (required for SPL<->ctoken transfers)
/// * `spl_token_program` - Optional SPL token program (required for SPL<->ctoken transfers)
/// * `compressed_token_pool_pda` - Optional token pool PDA (required for SPL<->ctoken transfers)
/// * `compressed_token_pool_pda_bump` - Optional bump seed for token pool PDA
///
/// # Errors
/// * `SplBridgeConfigRequired` - If transferring to/from SPL without required accounts
/// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
/// * `CannotDetermineAccountType` - If account type cannot be determined
#[allow(clippy::too_many_arguments)]
pub fn transfer_interface<'info>(
source_account: &AccountInfo<'info>,
destination_account: &AccountInfo<'info>,
authority: &AccountInfo<'info>,
amount: u64,
payer: &AccountInfo<'info>,
compressed_token_program_authority: &AccountInfo<'info>,
mint: Option<&AccountInfo<'info>>,
spl_token_program: Option<&AccountInfo<'info>>,
compressed_token_pool_pda: Option<&AccountInfo<'info>>,
compressed_token_pool_pda_bump: Option<u8>,
) -> Result<(), ProgramError> {
let source_is_ctoken =
is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?;
let dest_is_ctoken =
is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?;

match (source_is_ctoken, dest_is_ctoken) {
(true, true) => transfer_ctoken(source_account, destination_account, authority, amount),

(true, false) => {
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_ctoken_to_spl(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
)
}

(false, true) => {
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_spl_to_ctoken(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
)
}

// spl -> spl: Not supported
(false, false) => Err(ProgramError::Custom(
TokenSdkError::UseRegularSplTransfer.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

Preserve TokenSdkError::CannotDetermineAccountType instead of collapsing to InvalidAccountData

In both transfer_interface and transfer_interface_signed, errors from is_ctoken_account are mapped to ProgramError::InvalidAccountData, but the documented behavior lists TokenSdkError::CannotDetermineAccountType as a distinct error case. Collapsing this into InvalidAccountData loses important diagnostics for misconfigured or unsupported accounts. Consider returning ProgramError::Custom(TokenSdkError::CannotDetermineAccountType.into()) on that path so callers can distinguish “owner not recognized” from actual data corruption, aligning runtime behavior with the documented error contract.

Also applies to: 439-539

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs around
lines 335-437 (and likewise for 439-539), the code maps errors from
is_ctoken_account to ProgramError::InvalidAccountData which loses the distinct
TokenSdkError::CannotDetermineAccountType documented for this path; change the
error mapping so that when is_ctoken_account returns an error you return
Err(ProgramError::Custom(TokenSdkError::CannotDetermineAccountType.into()))
instead of InvalidAccountData, ensuring both occurrences (for source and
destination checks and in transfer_interface_signed) are updated to preserve the
specific custom error.

Comment on lines +78 to +148
/// Helper function to decompress a compressed account into a PDA
/// idempotently with seeds.
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_idempotent<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
signer_seeds: &[&[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,
{
if !solana_account.data_is_empty() {
msg!("Account already initialized, skipping");
return Ok(None);
}
let rent = Rent::get().map_err(|err| {
msg!("Failed to get rent: {:?}", err);
LightSdkError::Borsh
})?;

let light_account = LightAccount::<T>::new_close(program_id, &compressed_meta, data)?;

// Account space needs to include discriminator + serialized data
// The compressed account has compression_info: None, but after decompression
// it will have compression_info: Some(...), so we need to add that space
let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
let base_space = discriminator_len + T::size(&light_account.account);
// Add space for CompressionInfo (Option::None is 1 byte, Option::Some is 1 + INIT_SPACE)
let space = base_space + CompressionInfo::INIT_SPACE;
let rent_minimum_balance = rent.minimum_balance(space);

invoke_create_account_with_heap(
rent_sponsor,
solana_account,
rent_minimum_balance,
space as u64,
&cpi_accounts.self_program_id(),
signer_seeds,
cpi_accounts.system_program()?,
)?;

let mut decompressed_pda = light_account.account.clone();
*decompressed_pda.compression_info_mut_opt() = Some(CompressionInfo::new_decompressed()?);

let mut account_data = solana_account.try_borrow_mut_data()?;
let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR);
decompressed_pda
.serialize(&mut &mut account_data[discriminator_len..])
.map_err(|err| {
msg!("Failed to serialize decompressed PDA: {:?}", err);
LightSdkError::Borsh
})?;

Ok(Some(light_account.to_account_info()?))
}
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

🧩 Analysis chain

Idempotent decompression flow is solid; tighten error mapping and program owner assumption

This function has a nice structure: it is idempotent (data_is_empty guard), computes space as discriminator + T::size + CompressionInfo::INIT_SPACE, and initializes compression_info to Some(CompressionInfo::new_decompressed()) before serializing into the PDA buffer. Two points to consider:

  1. Error mapping for Rent::get (Lines 108–111)
    Mapping Rent::get() failures to LightSdkError::Borsh is semantically odd; this isn’t a Borsh issue. Given you already use LightSdkError::ProgramError elsewhere in this file, it would be more accurate to reuse that:

  • let rent = Rent::get().map_err(|err| {
  •    msg!("Failed to get rent: {:?}", err);
    
  •    LightSdkError::Borsh
    
  • })?;
  • let rent = Rent::get().map_err(|err| {
  •    msg!("Failed to get rent: {:?}", err);
    
  •    LightSdkError::ProgramError(err)
    
  • })?;

2. **Owner consistency between `program_id` and `cpi_accounts.self_program_id()` (Lines 124–131)**  
The new account is created with `program_id = &cpi_accounts.self_program_id()`, while `LightAccount::<T>::new_close` receives the separate `program_id` parameter. This is fine if those two values are guaranteed to be identical, but if they can diverge you’d end up with a PDA owned by one program while the logical account type is associated with another.

It may be worth:
- Either passing the `program_id` parameter through to `invoke_create_account_with_heap` as the owner, or
- Adding a debug assertion that `*program_id == cpi_accounts.self_program_id()` to catch configuration mistakes early.

Overall, the flow is correct; these tweaks would make error reporting and invariants clearer.


Please confirm that `program_id` and `cpi_accounts.self_program_id()` are always equal in your call sites and that switching `Rent::get` to use `LightSdkError::ProgramError` is compatible with your existing error handling.

---

I'll verify the two major concerns raised in this review. Let me search the codebase systematically.











🏁 Script executed:

#!/bin/bash
# Find LightSdkError enum definition
echo "=== Searching for LightSdkError enum definition ==="
rg -n "enum LightSdkError" --type rust
echo ""
echo "=== Searching for LightSdkError variants ==="
rg -nA 20 "enum LightSdkError" --type rust | head -40

Length of output: 3233


🏁 Script executed:

#!/bin/bash
# Search for Rent::get() usage in the same file
echo "=== Rent::get() usage in decompress_idempotent.rs ==="
fd "decompress_idempotent.rs" --type f | xargs rg -n "Rent::get"

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Find where prepare_account_for_decompression_idempotent is called
echo "=== Call sites for prepare_account_for_decompression_idempotent ==="
rg -n "prepare_account_for_decompression_idempotent" --type rust | grep -v "fn prepare_account"

Length of output: 351


🏁 Script executed:

#!/bin/bash
# Check if program_id and self_program_id() comparison happens anywhere
echo "=== Checking program_id usage patterns ==="
rg -nC 3 "program_id.*self_program_id\|self_program_id.*program_id" --type rust

Length of output: 42



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full LightSdkError enum definition
echo "=== Full LightSdkError enum definition ==="
rg -nA 50 "pub enum LightSdkError" sdk-libs/sdk/src/error.rs | head -70

Length of output: 2495


🏁 Script executed:

#!/bin/bash
# Look at the call site in decompress_runtime.rs
echo "=== Call site context (line 201 and surrounding) ==="
fd "decompress_runtime.rs" --type f | xargs sed -n '190,220p' | cat -n

Length of output: 1346


🏁 Script executed:

#!/bin/bash
# Check LightAccount::<T>::new_close signature and how it uses program_id
echo "=== LightAccount::new_close implementation ==="
rg -nB 2 -A 15 "fn new_close" --type rust | head -40

Length of output: 2739


🏁 Script executed:

#!/bin/bash
# Check invoke_create_account_with_heap signature
echo "=== invoke_create_account_with_heap signature ==="
rg -nB 2 -A 10 "fn invoke_create_account_with_heap" --type rust

Length of output: 1245



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for complete LightSdkError enum to find ProgramError
echo "=== Searching for ProgramError in LightSdkError ==="
rg -n "ProgramError" sdk-libs/sdk/src/error.rs

Length of output: 284


🏁 Script executed:

#!/bin/bash
# Check if LightSdkError::ProgramError is used in decompress_idempotent.rs
echo "=== ProgramError usage in decompress_idempotent.rs ==="
rg -n "ProgramError\|LightSdkError::" sdk-libs/sdk/src/compressible/decompress_idempotent.rs

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Get the proper call site context in decompress_runtime.rs around line 201
echo "=== Actual call site in decompress_runtime.rs (lines 195-215) ==="
fd "decompress_runtime.rs" --type f | xargs sed -n '195,215p'

Length of output: 825


🏁 Script executed:

#!/bin/bash
# Check if there's any assertion or validation of program_id consistency
echo "=== Searching for program_id validation or assertions ==="
rg -n "program_id.*==" sdk-libs/sdk/src/compressible/decompress_idempotent.rs

Length of output: 57



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the actual function call more precisely
echo "=== Finding prepare_account_for_decompression_idempotent call in decompress_runtime.rs ==="
rg -nB 5 -A 10 "prepare_account_for_decompression_idempotent::<T>" sdk-libs/sdk/src/compressible/decompress_runtime.rs

Length of output: 732


🏁 Script executed:

#!/bin/bash
# Check the full file to understand CpiAccounts and self_program_id()
echo "=== CpiAccounts struct and self_program_id implementation ==="
rg -nB 3 -A 8 "fn self_program_id\|struct CpiAccounts" --type rust | head -50

Length of output: 61


Address error type mapping and add program_id consistency validation

Two refinements would strengthen this function:

  1. Lines 108–111: Map Rent::get() to LightSdkError::ProgramError instead of Borsh
    The LightSdkError enum includes a ProgramError variant (error.rs:101) that's semantically appropriate for system program failures. Borsh is for serialization errors, not rent queries.

  2. Lines 97, 124: Validate program_id and cpi_accounts.self_program_id() consistency
    The function receives program_id as a parameter and passes it to LightAccount::<T>::new_close(), but the account is created with cpi_accounts.self_program_id() as owner. If these diverge, the PDA will be owned by one program while its logical type is tied to another. Add a debug assertion: debug_assert_eq!(program_id, &cpi_accounts.self_program_id()); to catch configuration mismatches early, or thread program_id through to invoke_create_account_with_heap().

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

♻️ Duplicate comments (2)
sdk-libs/sdk/Cargo.toml (1)

58-58: [DUPLICATE] Unconditional light-ctoken-types dependency remains unjustified.

This mirrors the concern from your previous review: light-ctoken-types is listed as a non-optional workspace dependency but has no direct imports or usage in the SDK source. The prior reviewer found that program-test/Cargo.toml correctly treats it as optional (feature-gated behind devenv), which is the appropriate pattern for indirect or speculative dependencies.

Either remove the unconditional dependency or convert it to optional and feature-gate it (e.g., behind a devenv feature). If the crate is genuinely required by the new compressed-account logic, add explicit use light_ctoken_types::... statements in the SDK source to justify the hard requirement.

Proposed fix—remove or gate the dependency:

- light-ctoken-types = { workspace = true }
  light-compressible = { workspace = true }

Or, if required, make it optional:

+ light-ctoken-types = { workspace = true, optional = true }
  light-compressible = { workspace = true }
+ 
+ [features]
+ # ... existing features ...
+ devenv = ["light-ctoken-types"]
sdk-libs/macros/src/lib.rs (1)

232-237: DataHasher documentation/implementation mismatch persists.

The documentation on line 232 states DataHasher is an "Alias of LightHasher", but the implementation on line 236 calls derive_light_hasher_sha, which uses SHA256 hashing instead of Poseidon.

This is the same issue flagged in past reviews. Users following the documentation will expect Poseidon-based hashing but receive SHA256-based hashing instead, which could lead to incorrect hash values and confusion.

Fix option 1 (if SHA256 is intended): Update the documentation to clarify this aliases the SHA variant:

-/// Alias of `LightHasher`.
+/// Alias of `LightHasherSha`. Uses SHA256 hashing instead of Poseidon.

Fix option 2 (if Poseidon is intended): Change the implementation to match the documented behavior:

 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }

Based on learnings from past reviews.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d01bad and 385d3a9.

📒 Files selected for processing (2)
  • sdk-libs/macros/src/lib.rs (4 hunks)
  • sdk-libs/sdk/Cargo.toml (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk-libs/macros/src/lib.rs (10)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (2)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (186-225)
sdk-libs/macros/src/cpi_signer.rs (1)
  • derive_light_cpi_signer (47-97)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (76-110)
  • derive_light_rent_sponsor (119-166)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test program-libs-fast
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Forester e2e test
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
🔇 Additional comments (9)
sdk-libs/sdk/Cargo.toml (4)

38-41: New Solana system crates properly workspace-pinned.

The additions of solana-clock, solana-sysvar, solana-system-interface, and solana-loader-v3-interface (with serde feature enabled) are all correctly locked to workspace versions. This ensures consistency across the workspace and prevents version drift. Bincode is used for system program and native program instruction serialization, hidden behind APIs like Instruction::new_with_bincode, which justifies the use of solana-loader-v3-interface for loader state handling.


15-15: Feature expansion is coherent and well-scoped.

  • Line 15 (idl-build): Expansion to include "anchor" alongside "anchor-lang/idl-build" is sensible for unified IDL build configuration.
  • Line 20 (anchor feature augmented): Adding "light-compressible/anchor" is aligned with the PR's goal of integrating compressible account runtime/macros.
  • Line 29 (new anchor-discriminator feature): Clean feature gate for discriminator macro support, properly delegates to light-sdk-macros/anchor-discriminator.

All three changes follow workspace feature-gating patterns and maintain idiomatic Cargo structure.

Also applies to: 20-20, 29-29


59-59: light-compressible dependency addition is justified by PR scope.

The new unconditional workspace dependency on light-compressible aligns with the PR objectives: rent implementation for c-PDA, derived macros, and enhanced compressible instructions. This crate is genuinely needed for the new compression-related runtime and configuration modules being added across the SDK. The corresponding feature gate in the anchor feature (line 20) ensures downstream consumers can opt-in to the enhanced Anchor integration.


48-48: bincode dependency is properly declared and actively used—no action needed.

Verification confirms bincode is a direct, necessary dependency. It's used at sdk-libs/sdk/src/compressible/config.rs:391 to deserialize UpgradeableLoaderState from the Solana program loader, a core protocol requirement for the compressible account configuration. The dependency is correctly declared and should remain as-is.

sdk-libs/macros/src/lib.rs (5)

1-19: LGTM - Clean module organization.

The new modules (compressible, cpi_signer, rent_sponsor, utils) are properly declared and the imports are used consistently throughout the file.


239-516: LGTM - Well-structured compressible account macros.

The new compression-related derives (HasCompressionInfo, CompressAs, Compressible, CompressiblePack, DecompressContext) and the add_compressible_instructions attribute are properly implemented:

  • Consistent use of into_token_stream for error handling
  • Comprehensive documentation with practical examples
  • Proper delegation to internal module functions
  • Doctest failures from past reviews have been addressed (line 319 correctly uses ignore)

518-565: LGTM - Compile-time PDA derivation macros are well-implemented.

The CPI signer and rent sponsor macros correctly compute PDAs at compile time, which provides better performance than runtime derivation. The documentation clearly explains usage and the return types.


365-369: Verify that the generic account attribute name is intentional.

The proc macro attribute on line 366 is named simply account, which is quite generic. While namespace conflicts are unlikely since users must explicitly import from this crate, consider whether:

  1. The rename from light_account to account is intentional for API simplification
  2. This could create confusion with other account-related attributes (e.g., Anchor's #[account])
  3. Documentation clearly distinguishes this from similar attributes in other frameworks

If the generic name is intentional, consider adding a note in the documentation to clarify the distinction from other frameworks' account attributes.


54-54: LGTM - Consistent error handling through into_token_stream.

The refactoring to wrap all macro implementations with into_token_stream provides standardized error handling across the codebase. This ensures that syn::Error results are properly converted to compilation errors, improving the developer experience with better error messages.

Also applies to: 60-60, 66-66, 124-124, 130-130, 206-206, 229-229

Comment on lines +13 to +20
# needed for auto-compress
"dep:account-compression",
"dep:light-compressed-token",
"dep:light-ctoken-types",
"dep:light-compressible",
"dep:light-registry",
"dep:light-batched-merkle-tree",
"dep:light-concurrent-merkle-tree",
Copy link
Contributor

@ananas-block ananas-block Nov 18, 2025

Choose a reason for hiding this comment

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

v2 feature must not depend on anchor programs.
That's why compressible functions are only available in devenv for now.
Please don't change the way the features work. We should add manual instruction builders for registry program instructions in a separate pr then we can make compressible available in non devenv.

light-ctoken-types = { workspace = true, optional = true }
light-compressible = { workspace = true, optional = true }
light-compressed-token-sdk = { workspace = true, optional = true }
light-compressed-token-sdk = { workspace = true }
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this optional again else we cannot publish a new version of light-program-test until we published light-compressed-token-sdk.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
sdk-libs/program-test/src/compressible.rs (1)

52-73: FundingPoolConfig wiring into cToken compressible config is reasonable

Using CtokenCompressibleConfig::new_ctoken plus CtokenCompressibleConfig::derive_pda to materialize a synthetic config and then copying out rent_sponsor / compression_authority and their bumps is a clean way to keep this helper structurally aligned with the on-chain config layout while staying test-only.

One small nit: if version here is effectively the config discriminator/bump rather than a semantic version, consider a brief doc comment or a more specific name to avoid confusion for future readers.

sdk-libs/program-test/src/indexer/test_indexer.rs (1)

455-613: Unwrap panic risk is valid; From trait concern is unfounded

The code at line 594–595 does have a panic vulnerability: accessing .0.unwrap() on the ValidityProof's inner Option<CompressedProof> will panic if that Option is ever None. This is a legitimate invariant risk if _get_validity_proof_v1_implementation ever constructs a ValidityProofWithContext with a None proof value.

However, the concern about the From trait implementation is incorrect: impl From<Option<CompressedProof>> for ValidityProof is already implemented in both sdk-libs (proof.rs:83–87) and program-libs (compressed_proof.rs:106–110), and the line compiles and executes as intended. No trait impl is missing.

Recommendation: Document or enforce the invariant that proof is always Some when returned from _get_validity_proof_v1_implementation, or switch to a fallible conversion that propagates an IndexerError instead of panicking. The From trait concern can be safely disregarded.

sdk-libs/program-test/src/program_test/rpc.rs (3)

265-282: get_state_tree_infos gating mirrors get_latest_active_state_trees

Same devenv-based split here: non‑devenv builds see only v1 trees, devenv builds see only v2 trees. This keeps the behavior consistent with get_latest_active_state_trees, which is good, but it also means there’s no single build that can enumerate both sets. If you still need “mixed” scenarios for migration tests, you’ll need separate helpers or explicit selection instead of cfg‑gating.


246-263: Critical: Feature gate mismatch between test and client RPC implementations

The program-test rpc.rs now gates tree selection on devenv feature, but the actual client RPC (lines 693, 783 in sdk-libs/client/src/rpc/client.rs) still gates on v2 feature. These are orthogonal controls—someone building with feature = "v2" but without feature = "devenv" will get v2 trees from the client but v1 trees from the test RPC, causing a mismatch. The two implementations need consistent feature gating to ensure tests operate on the same tree types as production.


284-307: Update documentation: fix incorrect feature name in lib.rs

The code change itself is solid—get_random_state_tree_info() correctly uses #[cfg(feature = "devenv")] to compile-time select between v1 and v2 trees, and all 9 active call sites are version-agnostic (they treat TreeInfo uniformly regardless of whether it contains a v1 or v2 tree). The devenv feature correctly gates program dependencies per CLAUDE.md guidance.

However, there's a documentation issue: sdk-libs/program-test/src/lib.rs claims the feature is v2, but the actual feature defined in Cargo.toml is devenv. The documentation should reflect the real feature name to avoid user confusion.

Line ~13 in lib.rs currently states:

//! - `v2` - Enables v2 batched Merkle trees.

Should be:

//! - `devenv` - Enables v2 batched Merkle trees and local test environment support.
sdk-libs/program-test/src/program_test/config.rs (2)

18-57: New auto_register_custom_programs_for_pda_compression flag could use documentation

The new auto_register_custom_programs_for_pda_compression flag is an important behavioral switch but is currently undocumented in the struct. Given it’s public and alters how additional programs are treated in LightProgramTest, a short doc comment (what “auto‑register” means, how it interacts with additional_programs and cold‑state mining) would help test authors understand the implications.


122-165: Defaulting auto_register_custom_programs_for_pda_compression to true changes test behavior

In the Default impl, auto_register_custom_programs_for_pda_compression is now true under devenv, which means all default configs will auto‑register additional_programs for PDA compression unless explicitly turned off. That’s convenient, but it’s also a behavior change that can surprise existing tests (e.g., extra compression work or unexpected rent flows for newly‑added programs).

Consider whether false as a default plus an explicit “opt‑in” helper (e.g., ProgramTestConfig::with_auto_pda_compression()) would make behavior clearer, or at least call this out in docs / release notes so downstream tests can set it to false when they need the old behavior.

sdk-libs/program-test/src/program_test/light_program_test.rs (2)

28-38: auto_mine_cold_state_programs wiring looks sound; consider small API polish

The new auto_mine_cold_state_programs field, its initialization in new, and disable_cold_state_mining form a coherent configuration surface for cold‑state PDA mining under devenv. The retain call is correct and avoids duplicate entries via the earlier contains check.

Two small polish suggestions:

  • Since this list is now part of the public surface and drives behavior in warp_slot_forward, a short doc comment on the field (what “cold state mining” means and when entries are auto‑added) would help future readers.
  • For disable_cold_state_mining, taking impl Into<Pubkey> (or at least &Pubkey) would make it more ergonomic to call from sites that already have a reference rather than a by‑value Pubkey.

Overall, the implementation is correct and matches the intended usage from test_rpc.rs.

Also applies to: 82-84, 428-432


96-172: Auto‑registration of additional programs for PDA compression alters default test setup

The new block under #[cfg(feature = "devenv")] that pulls auto_register_custom_programs_for_pda_compression and additional_programs from config and seeds auto_mine_cold_state_programs is behaviorally important:

  • It ensures additional programs are automatically included in cold‑state mining when the flag is true, which is convenient for most tests.
  • It runs regardless of skip_protocol_init — which is fine, since lack of a compressible config causes auto_compress_program_pdas to no‑op — but it does mean the registration of programs into the auto‑mine list is decoupled from protocol initialization.

Two things to verify explicitly:

  1. Call sites that want additional programs but don’t want them auto‑compressed are correctly setting auto_register_custom_programs_for_pda_compression = false.
  2. There’s no scenario where tests accidentally end up doing more work than expected (e.g., long warp_slot_forward times) because they pass a large additional_programs list and forget about this default behavior.

If those are acceptable trade‑offs, the implementation itself looks good: it avoids duplicates and cleanly reuses the config data.

♻️ Duplicate comments (3)
sdk-libs/program-test/Cargo.toml (1)

10-10: Gate optional dependencies via feature flags in devenv.

The devenv feature declares conditional dependencies like dep:light-compressed-token but omits dep:light-compressed-token-sdk and dep:light-compressible-client. Since these are only used in feature-gated code, they must be explicitly enabled via the feature.

This also resolves the publishing constraint: with both marked optional and enabled only under devenv, light-program-test can be published independently.

Apply this diff to add the missing feature gates:

-devenv = ["light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]
+devenv = ["light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-compressed-token-sdk", "dep:light-compressible-client", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]

Based on past review feedback from ananas-block regarding publishing constraints.

sdk-libs/program-test/src/compressible.rs (2)

175-229: Honor compression_authority from on-chain config in auto_compress_program_pdas

Right now you ignore the authority stored in the config and hard-code compression_authority to the test payer:

let rent_sponsor = cfg.rent_sponsor;
// TODO: add coverage for external compression_authority
let compression_authority = payer.pubkey();

For any program that configures a distinct compression authority, this helper will build incorrect account metas. Because try_compress_chunk swallows errors, the effect is a silent no-op: compression will just never happen once cfg.compression_authority != payer.pubkey().

At minimum, this should use the authority from config so that tests behave like real programs:

-    let rent_sponsor = cfg.rent_sponsor;
-    // TODO: add coverage for external compression_authority
-    let compression_authority = payer.pubkey();
+    let rent_sponsor = cfg.rent_sponsor;
+    let compression_authority = cfg.compression_authority;

You can still configure compression_authority == payer.pubkey() in tests today, but this keeps auto_compress_program_pdas correct once you add coverage for distinct authorities. If/when the authority must sign, you’ll want to thread the corresponding keypair (or a signer abstraction) into this helper so the transaction signer set matches the config.


231-289: try_compress_chunk best-effort behavior is fine; consider minor ergonomics & perf tweaks (optional)

The per-PDA, best-effort compression loop is defensively written: you skip missing compressed accounts, missing proofs, and instruction build failures, and you keep going. That’s a good fit for a devenv helper.

Two optional refinements to consider:

  1. Reuse the payer instead of cloning per-PDA

    You re-fetch and clone the payer inside the inner loop:

    let payer = rpc.get_payer().insecure_clone();
    let payer_pubkey = payer.pubkey();

    Hoisting this out of the loop (or passing the payer from auto_compress_program_pdas) would avoid redundant clones and make it explicit that all compressions in a batch are paid by the same key.

  2. Batch validity proof requests for better RPC efficiency (previously noted)

    get_validity_proof is called once per PDA. If you ever run this over many PDAs, it will issue a lot of small RPC calls. A scalable pattern is:

    • Collect all cacc.hash values for the chunk.
    • Call get_validity_proof once with the vector of hashes.
    • Map returned proofs back to their PDAs (skipping any missing proofs).
    • Build and send per-PDA instructions as you do now.

    That preserves the current “skip on error and continue” semantics while substantially reducing RPC overhead.

These are non-blocking; the current implementation is acceptable for moderate test workloads.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 385d3a9 and 240cd0a.

⛔ Files ignored due to path filters (5)
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
📒 Files selected for processing (8)
  • sdk-libs/program-test/Cargo.toml (2 hunks)
  • sdk-libs/program-test/src/accounts/initialize.rs (3 hunks)
  • sdk-libs/program-test/src/compressible.rs (3 hunks)
  • sdk-libs/program-test/src/indexer/test_indexer.rs (8 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (3 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/rpc.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
sdk-libs/program-test/src/program_test/test_rpc.rs (1)
sdk-libs/program-test/src/compressible.rs (3)
  • new (53-73)
  • claim_and_compress (81-172)
  • auto_compress_program_pdas (175-229)
sdk-libs/program-test/src/compressible.rs (3)
program-libs/compressible/src/config.rs (1)
  • new_ctoken (163-182)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (70-77)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (289-359)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
sdk-libs/program-test/src/program_test/config.rs (1)
  • new (60-69)
🪛 GitHub Check: Forester e2e test
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (compressed-token-and-e2e, ["cargo-test-sbf -p compressed-token-test --test v1", "cargo-...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (compressed-token-batched-tree, ["cargo-test-sbf -p compressed-token-test -- test_transf...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk


[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (light-system-program-address, ["cargo-test-sbf -p system-test -- test_with_address", "c...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (light-system-program-compression, ["cargo-test-sbf -p system-test -- test_with_compress...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: system-programs (anchor & pinocchio, ["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-compr...
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: system-programs (token test, ["cargo-test-sbf -p sdk-token-test"])
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: Test batched-merkle-tree-simulate
sdk-libs/program-test/src/indexer/test_indexer.rs

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🔇 Additional comments (7)
sdk-libs/program-test/src/accounts/initialize.rs (3)

168-180: LGTM!

The feature gate change for v2 state tree creation is consistent with the import gate change at line 20 and maintains the existing conditional logic structure.


181-190: LGTM!

The feature gate change for v2 address tree creation completes the consistent realignment of v2-related functionality under the devenv feature gate.


20-24: Incomplete feature gate refactoring—verify PR scope.

The verification reveals a critical issue: while your changes to program-test/src/accounts/initialize.rs switch from v2 to devenv, the v2 feature gate remains active in 40+ locations across the codebase (sdk, sdk-pinocchio, client, and others). More importantly, sdk-libs/client/Cargo.toml shows devenv = ["v2"]—meaning devenv is a meta-feature that enables v2, not a replacement for it.

This indicates one of two scenarios:

  1. PR is intentionally scoped to only program-test (acceptable if documented, since devenv will transitively enable v2)
  2. PR is incomplete—if the goal is codebase-wide feature gate alignment, additional files need updating

Clarify the PR's intended scope. If it's targeted refactoring, that's fine; if it's meant to be comprehensive, this needs expansion across the affected packages.

sdk-libs/program-test/src/compressible.rs (1)

11-23: Devenv-only aliasing for cToken vs PDA configs looks consistent

The separation between CtokenCompressibleConfig (program-libs cToken config), RentConfig/SLOTS_PER_EPOCH, and CpdaCompressibleConfig (SDK-side PDA config) under a uniform #[cfg(feature = "devenv")] gate is coherent and matches the described architecture for test-only helpers. I don’t see issues with these imports as wired here.

sdk-libs/program-test/src/indexer/test_indexer.rs (2)

18-21: Cfg gating for MerkleProofWithContext aligned with devenv-only usage

Switching the import to #[cfg(feature = "devenv")] matches how MerkleProofWithContext is only used in the devenv-only queue methods below, so this keeps the non-devenv build lean while avoiding unused-import warnings. Looks good.


70-71: Hash import looks consistent with existing usage

Importing Hash from light_sdk::light_hasher matches its usage in get_compressed_account_by_hash and other methods in this file and keeps the hash type consistent with the rest of the SDK. No issues here.

sdk-libs/program-test/src/program_test/config.rs (1)

71-85: Verification complete: no actual code usages found

The original concern was well-founded as a verification step. However, the search confirms no compilation risk: the only reference to ProgramTestConfig::new_v2 in actual code is a documentation example (lib.rs line 109), which rustdoc handles gracefully and doesn't affect compilation.

The unrelated AddressMerkleTreeBundle::new_v2 (address_tree.rs line 57) is ungated and called from ungated code (test_indexer.rs line 1416), which is consistent and poses no issues.

light-zero-copy = { workspace = true }
litesvm = { workspace = true }
spl-token-2022 = { workspace = true }
light-compressible-client = { workspace = true, features = ["anchor"] }
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

light-compressible-client should be optional to unblock publishing.

This dependency is unconditional but should only be needed under devenv (as noted in past reviews). Making it unconditional reintroduces the publishing blocker: light-program-test cannot be published until light-compressible-client is published.

Align this with the pattern used for similar compression-related deps (line 22: light-compressed-token-sdk).

Apply this diff to make the dependency optional and gate it via feature:

-light-compressible-client = { workspace = true, features = ["anchor"] }
+light-compressible-client = { workspace = true, features = ["anchor"], optional = true }

Then update the devenv feature on line 10 to include it (see next comment).

📝 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
light-compressible-client = { workspace = true, features = ["anchor"] }
light-compressible-client = { workspace = true, features = ["anchor"], optional = true }
🤖 Prompt for AI Agents
In sdk-libs/program-test/Cargo.toml around line 32, the dependency on
light-compressible-client is unconditional; change it to an optional,
feature-gated dependency by replacing the line with a workspace entry that sets
optional = true and keeps the existing features (e.g. light-compressible-client
= { workspace = true, optional = true, features = ["anchor"] }), and then add
this crate to the devenv feature list on line 10 (include
"light-compressible-client" alongside the other compression-related feature
entries).

Comment on lines +43 to 45
use light_compressed_token_sdk::compat::{TokenData, TokenDataWithMerkleContext};
use light_event::event::PublicTransactionEvent;
use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon};
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

Fix unresolved crate import for light_compressed_token_sdk

Static analysis shows a hard compile error at Line 43: failed to resolve: use of unresolved module or unlinked crate 'light_compressed_token_sdk'. This means the workspace members that build this file (e.g. the various system/forester tests) don’t see a light_compressed_token_sdk dependency.

Because TokenData / TokenDataWithMerkleContext are used throughout TestIndexer (not behind any cfg), you can’t fix this with cfg gating alone; the crate must resolve in all relevant builds. Please either:

  • Add light_compressed_token_sdk as a dependency of sdk-libs/program-test (and any other crate that compiles this module), for example:
[dependencies]
light_compressed_token_sdk = { workspace = true }

or

  • If there is a re-export (e.g. light_sdk::compat::*), change the import here to use that existing, already-resolved path instead.

Until one of those is done, this file will fail to compile in the pipelines listed in the static-analysis hints.


🏁 Script executed:

#!/bin/bash
# Quick check: which crates currently reference `light_compressed_token_sdk`
rg -n 'light_compressed_token_sdk' -S

Length of output: 5612


I'll examine the Cargo configuration for the sdk-libs/program-test crate to verify whether light_compressed_token_sdk is explicitly listed as a dependency, and compare the two files that both import from it.


🏁 Script executed:

#!/bin/bash
# Check Cargo.toml for sdk-libs/program-test
if [ -f sdk-libs/program-test/Cargo.toml ]; then
  echo "=== sdk-libs/program-test/Cargo.toml ===" 
  cat sdk-libs/program-test/Cargo.toml
else
  echo "Cargo.toml not found at sdk-libs/program-test/"
fi

Length of output: 2904


Now I can see the issue more precisely. The dependency is declared but it's marked optional = true, and it's not enabled in the devenv feature (note: light-compressed-token-sdk is missing from the devenv feature enablement, unlike light-compressed-token, light-ctoken-types, etc.).

Let me check if test_indexer.rs and the extensions.rs files are behind any cfg guards or feature gates that might explain the difference:


🏁 Script executed:

#!/bin/bash
# Check test_indexer.rs and extensions.rs for cfg attributes or module visibility
echo "=== test_indexer.rs header ===" 
head -50 sdk-libs/program-test/src/indexer/test_indexer.rs | grep -E "^#\[|^use |^pub|^mod"

echo ""
echo "=== program_test/extensions.rs header ===" 
head -20 sdk-libs/program-test/src/program_test/extensions.rs | grep -E "^#\[|^use |^pub|^mod"

echo ""
echo "=== indexer/extensions.rs header ===" 
head -20 sdk-libs/program-test/src/indexer/extensions.rs | grep -E "^#\[|^use |^pub|^mod"

Length of output: 2082


🏁 Script executed:

#!/bin/bash
# Check test_indexer.rs and extensions.rs for cfg attributes or feature gates
echo "=== test_indexer.rs (first 50 lines) ===" 
head -50 sdk-libs/program-test/src/indexer/test_indexer.rs

echo ""
echo "=== program_test/extensions.rs (first 30 lines) ===" 
head -30 sdk-libs/program-test/src/program_test/extensions.rs

echo ""
echo "=== indexer/extensions.rs (first 30 lines) ===" 
head -30 sdk-libs/program-test/src/indexer/extensions.rs

Length of output: 4569


Enable light_compressed_token_sdk in the devenv feature of sdk-libs/program-test

The dependency is declared in Cargo.toml but marked optional = true, and the devenv feature does not enable it. However, line 43 (and the extensions files) import it unconditionally—there's no #[cfg(feature = "devenv")] guarding the import. This causes a compile error when the module is built without explicitly enabling the dependency.

Fix: Add dep:light-compressed-token-sdk to the devenv feature list in sdk-libs/program-test/Cargo.toml, like you've done with other optional dependencies (dep:light-compressed-token, dep:light-ctoken-types, etc.):

devenv = ["light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree", "dep:light-compressed-token-sdk"]

This aligns with the pattern already in the feature definition and ensures the optional dependency resolves when this test crate is built.

🧰 Tools
🪛 GitHub Check: Forester e2e test

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

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

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

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

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk


[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (light-system-program-address, ["cargo-test-sbf -p system-test -- test_with_address", "c...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (light-system-program-compression, ["cargo-test-sbf -p system-test -- test_with_compress...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

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

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-compr...

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: system-programs (token test, ["cargo-test-sbf -p sdk-token-test"])

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🪛 GitHub Check: Test batched-merkle-tree-simulate

[failure] 43-43:
failed to resolve: use of unresolved module or unlinked crate light_compressed_token_sdk

🤖 Prompt for AI Agents
In sdk-libs/program-test/src/indexer/test_indexer.rs around lines 43 to 45, the
code imports light_compressed_token_sdk unconditionally but the dependency in
sdk-libs/program-test/Cargo.toml is optional and not enabled by the devenv
feature; update Cargo.toml by adding dep:light-compressed-token-sdk to the
devenv feature list (following the existing pattern used for other optional
deps) so the optional dependency is enabled when building with the devenv
feature.

@SwenSchaeferjohann SwenSchaeferjohann force-pushed the swen/csdk-macro-rent branch 2 times, most recently from 9076271 to a02a0cb Compare November 18, 2025 18:29
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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
sdk-libs/program-test/src/compressible.rs (1)

232-290: Significant performance regression: per-account processing instead of batching.

The refactored implementation processes each PDA individually with its own RPC calls and transaction, which is substantially slower than bulk chunk processing:

  1. N separate transactions instead of N/BATCH_SIZE transactions (BATCH_SIZE=5 from Line 211 is now unused)
  2. N separate get_validity_proof RPC calls instead of one batched call per chunk
  3. N separate get_compressed_account RPC calls instead of bulk fetching

For a program with 50 PDAs, this sends 50 transactions instead of 10, and makes 100 extra RPC calls. Each transaction incurs signature verification and network overhead.

The underlying compress_accounts_idempotent API (from relevant snippets) accepts arrays of accounts, so batching is supported. The outer loop (Lines 211-226) still batches PDAs into chunks but try_compress_chunk ignores the batch and processes individually.

Suggested fix: Restore bulk chunk processing:

 async fn try_compress_chunk(
     rpc: &mut LightProgramTest,
     program_id: &Pubkey,
     chunk: &[(Pubkey, solana_sdk::account::Account)],
     program_metas: &[solana_instruction::AccountMeta],
     address_tree: &Pubkey,
 ) {
     use light_client::indexer::Indexer;
     use light_compressed_account::address::derive_address;
     use light_compressible_client::compressible_instruction;
     use solana_sdk::signature::Signer;

-    // Attempt compression per-account idempotently.
-    for (pda, acc) in chunk.iter() {
-        let addr = derive_address(
-            &pda.to_bytes(),
-            &address_tree.to_bytes(),
-            &program_id.to_bytes(),
-        );
-
-        // Only proceed if a compressed account exists
-        let Ok(resp) = rpc.get_compressed_account(addr, None).await else {
-            continue;
-        };
-        let Some(cacc) = resp.value else {
-            continue;
-        };
-
-        // Fetch proof for this single account hash
-        let Ok(proof_with_context) = rpc
-            .get_validity_proof(vec![cacc.hash], vec![], None)
-            .await
-            .map(|r| r.value)
-        else {
-            continue;
-        };
-
-        // Build single-PDA compress instruction
-        let Ok(ix) = compressible_instruction::compress_accounts_idempotent(
-            program_id,
-            &compressible_instruction::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR,
-            &[*pda],
-            std::slice::from_ref(acc),
-            program_metas,
-            proof_with_context,
-        )
-        .map_err(|e| e.to_string()) else {
-            continue;
-        };
-
-        let payer = rpc.get_payer().insecure_clone();
-        let payer_pubkey = payer.pubkey();
-
-        // Ignore errors to continue compressing other PDAs
-        let _ = rpc
-            .create_and_send_transaction(std::slice::from_ref(&ix), &payer_pubkey, &[&payer])
-            .await;
-    }
+    // Collect addresses and hashes for the chunk
+    let mut pdas = Vec::new();
+    let mut accounts = Vec::new();
+    let mut hashes = Vec::new();
+    
+    for (pda, acc) in chunk.iter() {
+        let addr = derive_address(
+            &pda.to_bytes(),
+            &address_tree.to_bytes(),
+            &program_id.to_bytes(),
+        );
+        
+        let Ok(resp) = rpc.get_compressed_account(addr, None).await else {
+            continue;
+        };
+        let Some(cacc) = resp.value else {
+            continue;
+        };
+        
+        pdas.push(*pda);
+        accounts.push(acc.clone());
+        hashes.push(cacc.hash);
+    }
+    
+    if pdas.is_empty() {
+        return;
+    }
+    
+    // Fetch validity proof for all hashes at once
+    let Ok(proof_with_context) = rpc
+        .get_validity_proof(hashes, vec![], None)
+        .await
+        .map(|r| r.value)
+    else {
+        return;
+    };
+    
+    // Build instruction for all PDAs in the chunk
+    let Ok(ix) = compressible_instruction::compress_accounts_idempotent(
+        program_id,
+        &compressible_instruction::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR,
+        &pdas,
+        &accounts,
+        program_metas,
+        proof_with_context,
+    )
+    .map_err(|e| e.to_string()) else {
+        return;
+    };
+    
+    let payer = rpc.get_payer().insecure_clone();
+    let payer_pubkey = payer.pubkey();
+    
+    // Send single transaction for the entire chunk
+    let _ = rpc
+        .create_and_send_transaction(std::slice::from_ref(&ix), &payer_pubkey, &[&payer])
+        .await;
 }

This processes chunks efficiently: one proof fetch and one transaction per chunk instead of per-account.

sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

343-369: Verify rent sponsor fallback logic and error handling.

The updated rent sponsor resolution logic has a subtle issue:

Line 343: actual_rent_sponsor = if let Some(sponsor) = rent_sponsor_pubkey — but at this point, rent_sponsor_pubkey is declared but not yet initialized.

Lines 346-369: The else block tries to:

  1. Derive from compressible extension (lines 347-357)
  2. Fallback to finding first signer in packed_accounts (lines 360-367)
  3. Return error if still None (line 368)

Problem: The code attempts to assign to rent_sponsor_pubkey (an Option<Pubkey>) inside the else block and then unwrap it at line 368, but the outer actual_rent_sponsor binding shadows this. The logic appears to be trying to mutate rent_sponsor_pubkey after checking it, which creates confusing control flow.

Recommendation: Simplify this by directly computing actual_rent_sponsor without the shadowing:

let actual_rent_sponsor = if let Some(sponsor) = rent_sponsor_pubkey {
    sponsor
} else {
    // Try extension first
    let from_extension = if let Some(extensions) = &compressed_token.extensions {
        extensions.iter().find_map(|ext| {
            if let ZExtensionStruct::Compressible(e) = ext {
                if e.rent_sponsor != [0u8; 32] {
                    return Some(Pubkey::from(e.rent_sponsor));
                }
            }
            None
        })
    } else {
        None
    };
    
    // Fallback to first signer
    from_extension.or_else(|| {
        packed_accounts.iter()
            .find(|acc| acc.is_signer)
            .map(|acc| *acc.key)
    })
    .ok_or(TokenSdkError::InvalidAccountData)?
};

This eliminates the mutation of rent_sponsor_pubkey within the branch and makes the fallback chain explicit.

Do you want me to generate the simplified version or help verify the current logic's correctness?

sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

49-76: Fix missing signer for rent sponsor PDA

Line [60] debits rent_sponsor without ever supplying its signer seeds, so system_instruction::create_account will fail with MissingRequiredSignature as soon as the rent sponsor is the PDA derived by derive_rent_sponsor!. Please plumb the rent-sponsor signer seeds and pass them to invoke_signed alongside the PDA’s seeds. For example:

-fn invoke_create_account_with_heap<'info>(
-    rent_sponsor: &AccountInfo<'info>,
+fn invoke_create_account_with_heap<'info>(
+    rent_sponsor: &AccountInfo<'info>,
+    rent_sponsor_signer_seeds: &[&[u8]],
@@
-    invoke_signed(
+    invoke_signed(
         &create_account_ix,
         &[
             rent_sponsor.clone(),
             solana_account.clone(),
             system_program.clone(),
         ],
-        &[seeds],
+        &[seeds, rent_sponsor_signer_seeds],
     )

and then pass the program’s rent-sponsor seeds from cpi_accounts when calling this helper. Without that change the PDA can never fund decompression rent.

♻️ Duplicate comments (28)
sdk-libs/program-test/src/compressible.rs (1)

194-195: Still using hardcoded payer instead of config's compression_authority.

This issue was flagged in the previous review and remains unresolved. Using payer.pubkey() instead of cfg.compression_authority causes compress instructions to fail authorization checks whenever the config specifies a different authority. Because try_compress_chunk swallows errors (Line 286-288), this silently becomes a no-op instead of compressing PDAs.

Apply this diff:

-    // TODO: add coverage for external compression_authority
-    let compression_authority = payer.pubkey();
+    let compression_authority = cfg.compression_authority;

Then ensure your test transaction is signed by the compression authority. For tests that want payer to be the authority, set cfg.compression_authority equal to payer.pubkey() when initializing the config.

sdk-libs/sdk/Cargo.toml (1)

15-21: New feature flags and dependencies look correct; verify unused dependencies.

The added feature dependencies are coherent:

  • idl-build properly includes anchor for IDL generation ✓
  • anchor feature correctly gates light-compressible/anchor
  • light-compressible workspace dependency is used ✓

However, past review comments indicate light-ctoken-types (line 58) may be an unconditional dependency with no direct usage in the SDK source. If that's still true, consider making it optional and feature-gated (like program-test does with devenv), or remove it if truly unused.

Verify whether light-ctoken-types is actually used:

#!/bin/bash
# Search for direct imports of light_ctoken_types in sdk source
rg -n "use\s+light_ctoken_types" sdk-libs/sdk/src --type rust

Also applies to: 59-59

sdk-libs/program-test/src/indexer/test_indexer.rs (1)

20-21: Feature flag consolidation is consistent; address past review concerns.

The v2devenv feature flag updates are applied consistently across:

  • Import of MerkleProofWithContext (line 20-21)
  • get_validity_proof v2 implementation with queue handling (lines 455-599)
  • get_queue_elements implementation (lines 627-865)
  • get_subtrees implementation (lines 875-911)
  • get_address_queue_with_proofs implementation (lines 923-997)

However, past review comments flagged critical issues that should be addressed:

  1. Line 43: Unresolved crate import for light_compressed_token_sdk causes compilation failures. The dependency needs to be added to the devenv feature in sdk-libs/program-test/Cargo.toml.

  2. Lines 625-627, 873-875, 921-923: The unimplemented!() macros in non-devenv builds will panic if called. Consider returning a structured error instead.

Also applies to: 455-614, 625-627, 873-875, 921-923

sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

108-111: Restore ProgramError mapping for Rent::get()

Line [110] again maps Rent::get() failures to LightSdkError::Borsh, undoing the fix we landed earlier. Please switch back to LightSdkError::ProgramError(err) (or LightSdkError::from(err)) so sysvar access errors surface with the correct variant.

sdk-libs/compressible-client/src/lib.rs (1)

205-214: Avoid panicking when mixing tokens and PDAs

Line [208] still does compressed_accounts[0].0.tree_info.cpi_context.unwrap(). If the first element is a pure token account (no CPI context), this panics before we ever look at the PDA entry. Please scan the slice for the first Some(context) instead:

-        if has_pdas && has_tokens {
-            let cpi_context_of_first_input =
-                compressed_accounts[0].0.tree_info.cpi_context.unwrap();
+        if has_pdas && has_tokens {
+            let cpi_context_of_first_input = compressed_accounts
+                .iter()
+                .find_map(|(acc, _)| acc.tree_info.cpi_context)
+                .ok_or("Expected CPI context for PDA inputs but found none")?;

Then proceed with SystemAccountMetaConfig::new_with_cpi_context. That keeps the flow panic-free even when the first account is a token.

sdk-libs/sdk/src/compressible/config.rs (1)

504-513: Detect duplicate pubkeys by value

Line [506] still builds a HashSet<&Pubkey>, so duplicate entries with equal value but distinct backing slots aren’t caught. Store Pubkey by value instead:

-    let mut seen = HashSet::new();
-    for pubkey in address_space {
-        if !seen.insert(pubkey) {
+    let mut seen: HashSet<Pubkey> = HashSet::new();
+    for pubkey in address_space {
+        if !seen.insert(*pubkey) {
             msg!("Duplicate pubkey found in address_space: {}", pubkey);
             return Err(LightSdkError::ConstraintViolation);
         }
     }

This keeps the validation effective once MAX_ADDRESS_TREES_PER_SPACE grows beyond 1.

sdk-libs/sdk/src/compressible/compression_info.rs (2)

319-320: Return None when no rent was claimed

Line [319] still returns Ok(Some(0)), contradicting the doc comment that promises None when nothing is claimable. Please return Ok(None) in that branch so callers can distinguish “no payout” from “zero lamports claimed”.


339-356: Use the real system program ID in rent CPI

transfer_lamports_cpi builds the instruction with Pubkey::from([0;32]), so the CPI always fails (Cross-program invocation: unauthorized program). Please use the provided account and assert it really is the system program:

-    const SYSTEM_PROGRAM_ID: [u8; 32] = [0; 32];
-
-    let transfer_instruction = Instruction {
-        program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
+    require_keys_eq!(*system_program.key, solana_program::system_program::ID)?;
+
+    let transfer_instruction = Instruction {
+        program_id: *system_program.key,
         accounts: vec![
             AccountMeta::new(*from.key, true),
             AccountMeta::new(*to.key, false),
         ],
         data: instruction_data,
     };

(Replace require_keys_eq! with your local equivalent if needed.) Without this fix, top_up_rent can never move lamports.

programs/system/Cargo.toml (1)

28-29: Profile feature refactoring looks intentional.

The profile-program feature is now empty and profile-heap no longer depends on light-program-profiler. Per past review comments, this was flagged and addressed in earlier commits.

sdk-libs/macros/src/compressible/GUIDE.md (2)

1-1: Fix markdown lint violation for consistency.

The first line should be a top-level heading (#) rather than a second-level heading (##) to satisfy MD041.

Apply this diff:

-## Compressible macros — caller program usage (first draft)
+# Compressible macros — caller program usage (first draft)

32-174: Normalize ordered list prefixes to satisfy markdown linter.

Lines 32, 44, 54, 76, 81, 139, 158, and 174 use incremental numeric prefixes (2., 3., 4.), but the configured style expects all items to use 1. (Markdown will render the correct sequential numbers).

sdk-libs/sdk/src/compressible/compress_account.rs (2)

35-36: Guard against empty address_space before indexing

prepare_account_for_compression still indexes address_space[0] directly when deriving the compressed PDA. This will panic if address_space is ever empty, which is risky for an on‑chain SDK helper even if callers “should” provide at least one tree.

Consider failing gracefully instead of panicking:

@@
-    use light_compressed_account::address::derive_address;
-
-    let derived_c_pda = derive_address(
-        &account_info.key.to_bytes(),
-        &address_space[0].to_bytes(),
-        &program_id.to_bytes(),
-    );
+    use light_compressed_account::address::derive_address;
+
+    let address_tree = address_space.first().ok_or_else(|| {
+        msg!(
+            "prepare_account_for_compression requires a non-empty address_space"
+        );
+        LightSdkError::ConstraintViolation
+    })?;
+
+    let derived_c_pda = derive_address(
+        &account_info.key.to_bytes(),
+        &address_tree.to_bytes(),
+        &program_id.to_bytes(),
+    );

Please verify that all existing call‑sites either already guarantee a non‑empty slice or will handle this explicit error correctly.

Also applies to: 54-60


68-100: Enforce owner/program ID consistency when constructing the compressed account

The new rent‑based gating looks coherent (it builds an AccountRentState, checks is_compressible, and logs detailed context), but the function still allows owner_program_id = cpi_accounts.self_program_id() to diverge from *program_id. In that case, you’d derive the compressed PDA with one program ID and attach it to an account owned by another, which is a subtle but serious mismatch.

You can enforce this invariant cheaply:

@@
-    let meta_with_address = CompressedAccountMeta {
+    let meta_with_address = CompressedAccountMeta {
         tree_info: compressed_account_meta.tree_info,
         address: derived_c_pda,
         output_state_tree_index: compressed_account_meta.output_state_tree_index,
     };
@@
-    let owner_program_id = cpi_accounts.self_program_id();
+    let owner_program_id = cpi_accounts.self_program_id();
+    if owner_program_id != *program_id {
+        msg!(
+            "prepare_account_for_compression: owner_program_id mismatch. \
+             expected {}, got {}",
+            program_id,
+            owner_program_id
+        );
+        return Err(LightSdkError::ConstraintViolation.into());
+    }
     let mut compressed_account =
         LightAccount::<A::Output>::new_empty(&owner_program_id, &meta_with_address)?;

Please re‑run any CPI/macro‑generated compress flows after adding this check to catch misconfigured CpiAccounts early.

Also applies to: 114-117

sdk-libs/macros/src/rent_sponsor.rs (2)

11-12: Tighten macro argument validation and avoid silently defaulting invalid versions

Two correctness issues remain in the proc‑macro API:

  1. Extra arguments are silently ignored

    Args::parse accepts an arbitrary number of Expr arguments but only consumes elems[0] and elems[1]. If a caller passes a third parameter, it’s dropped without any error, which is very hard to debug at call‑sites.

  2. Invalid version literals quietly become 1

    Both macros parse the version via lit.base10_parse::<u16>().unwrap_or(1u16). Any parse error (including out‑of‑range literals) silently coerces to version 1, deriving the wrong PDA/config without a compile‑time signal. For something governing rent routing, this should fail loudly.

You can address both with small, targeted changes:

 impl Parse for Args {
     fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
         let elems = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
         if elems.is_empty() {
             return Err(syn::Error::new(
                 input.span(),
                 "Expected at least a program id string literal",
             ));
         }
+        if elems.len() > 2 {
+            return Err(syn::Error::new(
+                input.span(),
+                "Expected at most 2 arguments: program id string and optional u16 version",
+            ));
+        }
@@
 pub fn derive_light_rent_sponsor_pda(input: TokenStream) -> TokenStream {
     let args = parse_macro_input!(input as Args);
     let program_id_str = args.program_id.value();
-    let version_u16: u16 = match args.version.as_ref() {
-        Some(lit) => lit.base10_parse::<u16>().unwrap_or(1u16),
-        None => 1u16,
-    };
+    let version_u16: u16 = match args.version.as_ref() {
+        Some(lit) => match lit.base10_parse::<u16>() {
+            Ok(v) => v,
+            Err(e) => return e.to_compile_error().into(),
+        },
+        None => 1u16,
+    };
@@
 pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream {
     let args = parse_macro_input!(input as Args);
     let program_id_str = args.program_id.value();
-    let version_u16: u16 = match args.version.as_ref() {
-        Some(lit) => lit.base10_parse::<u16>().unwrap_or(1u16),
-        None => 1u16,
-    };
+    let version_u16: u16 = match args.version.as_ref() {
+        Some(lit) => match lit.base10_parse::<u16>() {
+            Ok(v) => v,
+            Err(e) => return e.to_compile_error().into(),
+        },
+        None => 1u16,
+    };

This keeps the default (version = 1 when omitted) but turns bad literals and superfluous arguments into clear compile‑time errors.

Also applies to: 37-59, 77-82, 120-125


67-110: DRY up shared rent-sponsor PDA derivation logic

Both derive_light_rent_sponsor_pda and derive_light_rent_sponsor perform the same sequence: parse args, validate the program ID, compute version, build seeds, call Pubkey::find_program_address, and convert bytes/bump to literals. Having this logic duplicated in two public macros makes it easy for them to drift over time.

Consider extracting a private helper that returns the raw data the macros need, for example:

fn derive_rent_sponsor_inner(args: &Args) -> Result<([u8; 32], [u8; 32], u8, u16), syn::Error> {
    use std::str::FromStr;

    let program_id = solana_pubkey::Pubkey::from_str(&args.program_id.value())
        .map_err(|_| syn::Error::new(
            args.program_id.span(),
            "Invalid program ID format. Expected a base58 encoded public key",
        ))?;

    let version_u16: u16 = match args.version.as_ref() {
        Some(lit) => lit.base10_parse::<u16>()?,
        None => 1u16,
    };

    let seeds = &[b"rent_sponsor".as_slice(), &version_u16.to_le_bytes()[..]];
    let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

    Ok((program_id.to_bytes(), pda.to_bytes(), bump, version_u16))
}

Then each macro only needs to call this helper and focus on shaping the final TokenStream. That keeps the PDA derivation rules single‑sourced and easier to evolve.

Also applies to: 112-166

sdk-libs/program-test/Cargo.toml (1)

10-11: Make light-compressible-client optional and gate it via devenv

light-compressible-client is pulled in unconditionally, while all of the other compression‑related crates here are optional and enabled via dep: in the devenv feature. As noted in earlier reviews, keeping this dependency non‑optional will block publishing light-program-test until light-compressible-client is published, and it also drags anchor‑based code into consumers that don’t enable devenv.

A minimal change that aligns with the existing pattern:

 [features]
-default = []
-devenv = ["light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]
+default = []
+devenv = [
+    "light-client/devenv",
+    "light-prover-client/devenv",
+    "dep:account-compression",
+    "dep:light-compressed-token",
+    "dep:light-ctoken-types",
+    "dep:light-compressible",
+    "dep:light-registry",
+    "dep:light-batched-merkle-tree",
+    "dep:light-concurrent-merkle-tree",
+    "dep:light-compressible-client",
+]
@@
-light-compressible-client = { workspace = true, features = ["anchor"] }
+light-compressible-client = { workspace = true, features = ["anchor"], optional = true }

Please confirm this matches your publishing strategy for light-program-test and that devenv builds continue to pass after gating this dependency.

Also applies to: 22-23, 32-32

sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1)

58-136: Guard packed-account indexing and gate CPI context lookup

The two safety bugs called out previously are still present:

  1. packed_accounts[mint_index as usize] and [owner_index as usize] will panic if the indices are malformed. That’s a transaction-aborting crash on-chain. Validate bounds and return ProgramError::InvalidAccountData instead.

  2. cpi_accounts.cpi_context() is invoked unconditionally, so callers without a CPI-context PDA hit MissingRequiredSignature even when has_pdas is false. Derive the optional pubkey only when needed and feed that into the instruction builder.

While you’re here, the boxed Vec is still unnecessary overhead.

-    let mut token_decompress_indices: Box<Vec<crate::instructions::DecompressFullIndices>> =
-        Box::new(Vec::with_capacity(ctoken_accounts.len()));
+    let mut token_decompress_indices: Vec<crate::instructions::DecompressFullIndices> =
+        Vec::with_capacity(ctoken_accounts.len());
@@
-    let cpi_context = cpi_accounts
-        .cpi_context()
-        .map_err(|_| ProgramError::MissingRequiredSignature)?;
+    let cpi_context_pubkey = if has_pdas {
+        Some(
+            *cpi_accounts
+                .cpi_context()
+                .map_err(|_| ProgramError::MissingRequiredSignature)?
+                .key,
+        )
+    } else {
+        None
+    };
@@
-        let mint_info = &packed_accounts[mint_index as usize];
-        let owner_info = &packed_accounts[owner_index as usize];
+        let mint_usize = mint_index as usize;
+        let owner_usize = owner_index as usize;
+        if mint_usize >= packed_accounts.len() || owner_usize >= packed_accounts.len() {
+            msg!(
+                "Packed account index out of bounds: owner={}, mint={}, len={}",
+                owner_index,
+                mint_index,
+                packed_accounts.len()
+            );
+            return Err(ProgramError::InvalidAccountData);
+        }
+        let mint_info = &packed_accounts[mint_usize];
+        let owner_info = &packed_accounts[owner_usize];
@@
-        if has_pdas {
-            Some(*cpi_context.key)
-        } else {
-            None
-        },
+        cpi_context_pubkey,
sdk-libs/sdk/src/compressible/compress_runtime.rs (2)

46-54: Use structured SDK error instead of ProgramError::Custom(0) for config mismatches

Hard-coding ProgramError::Custom(0) for both rent-sponsor and compression-authority mismatches makes debugging and monitoring harder and deviates from the error pattern used elsewhere in the compressible module (where LightSdkError::ConstraintViolation is mapped into a non-zero custom code). This also risks conflating unrelated failures that might also use code 0.

You already have a crate-wide error type and From<LightSdkError> for ProgramError impls; I’d recommend aligning these checks with that convention:

-    let compression_config =
-        crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
-
-    if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
-        return Err(ProgramError::Custom(0));
-    }
-    if *ctx.compression_authority().key != compression_config.compression_authority {
-        return Err(ProgramError::Custom(0));
-    }
+    let compression_config =
+        crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
+
+    if *ctx.rent_sponsor().key != compression_config.rent_sponsor
+        || *ctx.compression_authority().key != compression_config.compression_authority
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

This keeps compress runtime behavior consistent with config::load_checked and other callers.


56-71: Add bounds checks before slicing PDA/account arrays to avoid panics on malformed inputs

The current indexing logic assumes consistency between remaining_accounts, compressed_accounts, and the internal CpiAccounts layout:

let cpi_accounts = CpiAccounts::new(
    ctx.fee_payer(),
    &remaining_accounts[system_accounts_offset as usize..],
    cpi_signer,
);

let system_accounts_start = cpi_accounts.system_accounts_end_offset();
let all_post_system = &cpi_accounts.to_account_infos()[system_accounts_start..];

// PDAs are at the end of remaining_accounts, after all the merkle tree/queue accounts
let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
let solana_accounts = &all_post_system[pda_start_in_all_accounts..];

If system_accounts_offset or system_accounts_start exceeds the available account count, or if compressed_accounts.len() > all_post_system.len(), these slices will panic (underflow / out-of-bounds) with attacker-controlled account arrays.

Given this is an on-chain ABI surface, it’s safer to validate lengths and return a structured error than rely on implicit panics. For example:

-    let system_accounts_start = cpi_accounts.system_accounts_end_offset();
-    let all_post_system = &cpi_accounts.to_account_infos()[system_accounts_start..];
-
-    // PDAs are at the end of remaining_accounts, after all the merkle tree/queue accounts
-    let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
-    let solana_accounts = &all_post_system[pda_start_in_all_accounts..];
+    let system_accounts_start = cpi_accounts.system_accounts_end_offset();
+    let all_infos = cpi_accounts.to_account_infos();
+    if system_accounts_start > all_infos.len() {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }
+    let all_post_system = &all_infos[system_accounts_start..];
+
+    // PDAs are at the end of remaining_accounts, after all the merkle tree/queue accounts
+    if compressed_accounts.len() > all_post_system.len() {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }
+    let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
+    let solana_accounts = &all_post_system[pda_start_in_all_accounts..];

This mirrors the defensive style used elsewhere in the SDK and avoids panics from malformed CPI inputs.

sdk-libs/macros/src/compressible/variant_enum.rs (2)

21-31: Make the generated Default semantics for CompressedAccountVariant explicit

Default currently picks the first provided account type:

let first_type = account_types[0];
impl Default for CompressedAccountVariant {
    fn default() -> Self {
        Self::#first_type(#first_type::default())
    }
}

This is reasonable but quite implicit—changing the order of macro arguments silently changes the default variant, which may surprise callers.

Two options that would make this safer:

  1. Generate a doc comment on CompressedAccountVariant explaining that the first account type becomes the default (with an example), or
  2. Extend the macro input syntax to allow specifying an explicit default type (preferred), and emit Default based on that, falling back to “first type” only if none is provided.

Either approach would make the behavior much clearer to downstream users.

Also applies to: 40-56


58-76: Document or tighten invariants around unreachable!() variants (esp. packed/CToken cases)

Across the generated impls you treat several variants as unreachable:

  • CompressedAccountVariant::Packed{Type} in DataHasher, HasCompressionInfo, Size, Pack, and Unpack
  • CompressedAccountVariant::PackedCTokenData / CTokenData in the same traits, with PackedCTokenData’s Unpack doing Ok(self.clone())

This is fine if the macro is only ever used in tightly controlled flows, but the current API surface doesn’t communicate those invariants:

  • Calling hash, compression_info, or size on a packed or CToken variant will panic at runtime.
  • PackedCTokenData::unpack returning Ok(self.clone()) is a bit surprising given the trait name and how the other variants behave.

I’d recommend:

  • Adding a generated doc comment on CompressedAccountVariant that spells out which variants are valid for which traits and that using the others will panic, and
  • Making PackedCTokenData’s Unpack branch consistent with the rest (e.g., unreachable!() or a panic message), so invalid-variant use fails uniformly.

That keeps the current design but makes the contracts explicit for downstream users.

Also applies to: 85-151, 153-171, 173-195, 197-219

sdk-libs/macros/src/compressible/seed_providers.rs (1)

60-176: Centralize the “standard account field” classification to avoid drift

Within generate_ctoken_seed_provider_implementation, you repeat the same string list to decide whether a ctx.accounts.* field is a plain AccountInfo or an Option<AccountInfo> that needs .as_ref().ok_or_else(...):

let is_standard_field = matches!(
    field_name_str.as_str(),
    "fee_payer"
        | "rent_sponsor"
        | "config"
        | "ctoken_rent_sponsor"
        | "ctoken_program"
        | "ctoken_cpi_authority"
        | "ctoken_config"
        | "compression_authority"
        | "ctoken_compression_authority"
);

This pattern appears in both the token seed bindings and authority seed bindings (multiple times). It’s easy for this list to get out of sync if you add a new “standard” field (e.g., another required account) or rename one.

Given this is compile-time infrastructure, I’d strongly suggest factoring this into a small helper in the macro crate, e.g.:

fn is_standard_account_field(name: &str) -> bool {
    matches!(
        name,
        "fee_payer"
            | "rent_sponsor"
            | "config"
            | "ctoken_rent_sponsor"
            | "ctoken_program"
            | "ctoken_cpi_authority"
            | "ctoken_config"
            | "compression_authority"
            | "ctoken_compression_authority"
    )
}

and then using it everywhere you currently inline the matches! expression. That way, adding a new standard field is a one-line change and all code paths (token seeds, authority seeds, any future helpers) stay consistent.

Also applies to: 193-348

sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)

147-218: Treat PDA derivation mismatches as hard errors, not just logs

In handle_packed_pda_variant, a derived PDA mismatch is only logged:

let (seeds_vec, derived_pda) = /* ... */;
if derived_pda != *solana_account.key {
    msg!(
        "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
        index,
        solana_account.key,
        derived_pda,
        seeds_vec
    );
}

and the function then continues to prepare/decompress the account. In practice, the subsequent invoke_* calls will likely fail if seeds and account don’t match, but relying on that implicit failure makes debugging harder and can obscure mis-wired accounts.

Given this is a correctness/security invariant (“these seeds must derive exactly this PDA”), it’s safer to fail fast. For example:

@@
-    if derived_pda != *solana_account.key {
-        msg!(
-            "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
-            index,
-            solana_account.key,
-            derived_pda,
-            seeds_vec
-        );
-    }
+    if derived_pda != *solana_account.key {
+        msg!(
+            "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
+            index,
+            solana_account.key,
+            derived_pda,
+            seeds_vec
+        );
+        return Err(ProgramError::InvalidArgument);
+    }

(or map to your LightSdkError::ConstraintViolation equivalent if you prefer a custom code). That way, any seed/account mismatch is immediately visible and the transaction aborts before any further state changes.


223-279: Add rent-sponsor validation and account-length guards to match compress-side invariants

Two related issues in process_decompress_accounts_idempotent:

  1. No rent-sponsor (or authority) check vs config

    You load CompressibleConfig and use its address_space:

    let compression_config =
        crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
    let address_space = compression_config.address_space[0];

    but never verify that ctx.rent_sponsor() (or other authorities) matches the values recorded in compression_config. On the compress side, process_compress_pda_accounts_idempotent does enforce rent-sponsor and compression-authority equality before proceeding. Having asymmetric checks between compress and decompress makes it easier to accidentally pass a wrong rent sponsor into decompression.

    Mirroring the compress path would tighten this:

  • let compression_config =
  •    crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
    
  • let address_space = compression_config.address_space[0];
  • let compression_config =
  •    crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
    
  • if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
  •    return Err(crate::error::LightSdkError::ConstraintViolation.into());
    
  • }
  • // Add similar checks here if you want to enforce any other authorities.
  • let address_space = compression_config.address_space[0];

2. **Unchecked subtraction when slicing PDA accounts**

Later you compute:

```rust
let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
let solana_accounts = &remaining_accounts[pda_accounts_start..];

If compressed_accounts.len() > remaining_accounts.len(), this underflows and causes an out-of-bounds slice. Even if macros usually keep these in sync, this is still attacker-controlled input at runtime.

Adding a simple guard avoids panics:

-    let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
-    let solana_accounts = &remaining_accounts[pda_accounts_start..];
+    if compressed_accounts.len() > remaining_accounts.len() {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    }
+    let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
+    let solana_accounts = &remaining_accounts[pda_accounts_start..];

Together, these changes bring decompression behavior in line with the compress runtime and make the entrypoint more robust against malformed account arrays.

sdk-libs/macros/src/lib.rs (2)

232-237: DataHasher still aliases the SHA variant, not LightHasher as documented

The docs say “Alias of LightHasher,” but the implementation delegates to derive_light_hasher_sha, i.e., the SHA-based variant. That’s inconsistent and will surprise users relying on the Poseidon default.

Either update the docs to say it aliases the SHA variant, or (more in line with the wording) wire it to derive_light_hasher:

 #[proc_macro_derive(DataHasher, attributes(skip, hash))]
 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }

133-160: Large commented-out macro implementations should be removed or tracked

The fully commented LightDiscriminatorSha and add_native_compressible_instructions blocks add noise and ambiguity about whether these APIs are supported.

Either delete them if they’re truly deprecated, or replace each block with a short TODO referencing a tracking issue (e.g. // TODO(#1234): Reintroduce LightDiscriminatorSha when SHA256 discriminator design is finalized), so readers know the current status.

Also applies to: 341-364

sdk-libs/macros/src/compressible/instructions.rs (2)

424-461: TokenSeedSpec lookup compares Ident to String and will not compile

In pda_seed_provider_impls, you look up the TokenSeedSpec for each account type by comparing a syn::Ident to a String:

let name_str = name.to_string();
let spec = if let Some(ref pda_seed_specs) = pda_seeds {
    pda_seed_specs
        .iter()
        .find(|s| s.variant == name_str) // Ident == String
        .ok_or_else(|| macro_error!(... name_str))?
} else { ... };

Ident does not implement PartialEq<String>, so this comparison will not compile and the macros crate will fail to build.

Compare like-types instead, e.g.:

-        .map(|name| {
-            let name_str = name.to_string();
+        .map(|name| {
             let spec = if let Some(ref pda_seed_specs) = pda_seeds {
                 pda_seed_specs
                     .iter()
-                    .find(|s| s.variant == name_str)
+                    .find(|s| s.variant == *name)
                     .ok_or_else(|| {
                         macro_error!(
                             name,
                             "No seed specification for account type '{}'. All accounts must have seed specifications.",
-                            name_str
+                            name
                         )
                     })?
             } else {

This preserves the error message while fixing the type mismatch.


1182-1302: Fix generated PDA seed access: use .key() instead of .key and avoid brittle unwrap rewrites

The expression rewriter in generate_pda_seed_derivation_for_trait currently maps ctx-based paths to invalid field accesses:

  • ctx.accounts.foo becomes *accounts.foo.as_ref().unwrap().key
  • ctx.foo becomes *accounts.foo.as_ref().unwrap().key

Problems:

  1. Invalid .key field access

    Types like Signer<'info> and UncheckedAccount<'info> (and the Option<UncheckedAccount<'info>> wrapper used in DecompressAccountsIdempotent) don’t expose a public .key field. They implement the Key trait and provide a .key() method, typically returning a Pubkey. Generating .key will not compile.

  2. Incorrect handling of method calls

    For an expression like ctx.accounts.foo.key(), the mapper first rewrites the receiver to *accounts.foo.as_ref().unwrap().key, and the method-call branch then appends .key() again, effectively generating (*accounts.foo.as_ref().unwrap().key).key(). Pubkey does not have a key() method, so this also fails to compile.

  3. Unchecked unwrap() on optional accounts

    For optional seed accounts (e.g. fields added as Option<UncheckedAccount<'info>> in DecompressAccountsIdempotent), .as_ref().unwrap() will panic if the caller forgets to pass the account, rather than surfacing a structured CompressibleInstructionError::MissingSeedAccount.

At minimum, the generated code should use the key() method directly and avoid the extra dereference:

-                                        if segment.ident == "ctx" {
-                                            return syn::parse_quote! { *accounts.#field_name.as_ref().unwrap().key };
+                                        if segment.ident == "ctx" {
+                                            // ctx.accounts.field_name -> accounts.field_name.as_ref().unwrap().key()
+                                            return syn::parse_quote! {
+                                                accounts.#field_name
+                                                    .as_ref()
+                                                    .unwrap()
+                                                    .key()
+                                            };

And for direct ctx.field access, use to_account_info().key() instead of assuming an Option:

-                            } else if segment.ident == "ctx" {
-                                // ctx.field -> accounts.field.as_ref().unwrap().key() (unwrap if optional)
-                                return syn::parse_quote! { *accounts.#field_name.as_ref().unwrap().key };
+                            } else if segment.ident == "ctx" {
+                                // ctx.field -> accounts.field.to_account_info().key()
+                                return syn::parse_quote! {
+                                    accounts.#field_name.to_account_info().key()
+                                };

Longer term, it would be safer to mirror the pattern used in the token seed provider path and have this derivation return a Result<_, CompressibleInstructionError> so missing optional seed accounts yield MissingSeedAccount instead of panicking, but that would require adjusting the PdaSeedDerivation trait and its call sites.

#!/bin/bash
# Quick check: confirm there are no remaining `.as_ref().unwrap().key` expansions
rg -n "as_ref\\(\\)\\.unwrap\\(\\)\\.key\\b" sdk-libs/macros/src/compressible/instructions.rs

Comment on lines +985 to +1071
let compress_arms: Vec<_> = account_types.iter().map(|name| {
quote! {
d if d == #name::LIGHT_DISCRIMINATOR => {
drop(data);
let data_borrow = account_info.try_borrow_data().map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
let code = match program_error {
anchor_lang::prelude::ProgramError::Custom(code) => code,
_ => 0,
};
solana_program_error::ProgramError::Custom(code)
})?;
let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
let code = match program_error {
anchor_lang::prelude::ProgramError::Custom(code) => code,
_ => 0,
};
solana_program_error::ProgramError::Custom(code)
})?;
drop(data_borrow);

let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>(
program_id,
account_info,
&mut account_data,
meta,
cpi_accounts,
&compression_config.address_space,
)?;
// Compute rent-based close distribution and transfer lamports:
// - Completed epochs to rent sponsor
// - Partial epoch (unused) to fee payer (user refund)
#[cfg(target_os = "solana")]
let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get()
.map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)?
.slot;
#[cfg(not(target_os = "solana"))]
let current_slot = 0;
let bytes = account_info.data_len() as u64;
let current_lamports = account_info.lamports();
let rent_exemption = anchor_lang::solana_program::sysvar::rent::Rent::get()
.map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)?
.minimum_balance(bytes as usize);
let ci_ref = account_data.compression_info();
let state = light_compressible::rent::AccountRentState {
num_bytes: bytes,
current_slot,
current_lamports,
last_claimed_slot: ci_ref.last_claimed_slot(),
};
let dist = state.calculate_close_distribution(&ci_ref.rent_config, rent_exemption);
// Transfer partial epoch back to fee payer (user)
if dist.to_user > 0 {
let fee_payer_info = self.fee_payer.to_account_info();
let mut src = account_info.try_borrow_mut_lamports().map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
program_error
})?;
let mut dst = fee_payer_info.try_borrow_mut_lamports().map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
program_error
})?;
**src = src.checked_sub(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?;
**dst = dst.checked_add(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;
}
// Transfer completed epochs (and base) to rent sponsor
if dist.to_rent_sponsor > 0 {
let rent_sponsor_info = self.rent_sponsor.to_account_info();
let mut src = account_info.try_borrow_mut_lamports().map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
program_error
})?;
let mut dst = rent_sponsor_info.try_borrow_mut_lamports().map_err(|e| {
let err: anchor_lang::error::Error = e.into();
let program_error: anchor_lang::prelude::ProgramError = err.into();
program_error
})?;
**src = src.checked_sub(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?;
**dst = dst.checked_add(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;
}
Ok(Some(compressed_info))
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Rent distribution logic is sound but should use explicit error codes for overflows

The new rent-based close distribution in the compress path (Lines 1009–1070) correctly:

  • Fetches Clock and Rent sysvars,
  • Builds an AccountRentState with num_bytes, current_slot, current_lamports, and last_claimed_slot,
  • Calls calculate_close_distribution and then moves dist.to_user to the fee payer and dist.to_rent_sponsor to the rent sponsor, with checked_sub/checked_add to guard under/overflow.

Two minor refinements:

  1. Both checked_add failures are mapped to ProgramError::Custom(0), which collapses a rare but distinct condition into a generic code that may collide with other errors. Consider adding a dedicated variant to CompressibleInstructionError (e.g. OverflowDuringRentDistribution) and mapping to that instead.

  2. If this ever becomes a hot path over many accounts, consider hoisting the Clock::get() and Rent::get() calls out of the per-account match arm (e.g. into the caller) to avoid repeated sysvar fetches.

🤖 Prompt for AI Agents
sdk-libs/macros/src/compressible/instructions.rs lines 985-1071: the checked_add
error branches currently map to ProgramError::Custom(0) which conflates overflow
with other errors; add a dedicated CompressibleInstructionError variant like
OverflowDuringRentDistribution (update its conversion to ProgramError) and
change both checked_add failure mappings to return that specific error via the
conversion; additionally (optional) hoist Clock::get() and Rent::get() calls out
of the per-account match arm into the caller so you fetch sysvars once for the
batch and pass current_slot and rent_exemption into this code path.

Comment on lines +516 to +897
#[inline(never)]
fn analyze_seed_spec_for_client(
spec: &TokenSeedSpec,
instruction_data: &[InstructionDataSpec],
) -> Result<(Vec<TokenStream>, Vec<TokenStream>)> {
let mut parameters = Vec::new();
let mut expressions = Vec::new();

for seed in &spec.seeds {
match seed {
SeedElement::Literal(lit) => {
let value = lit.value();
expressions.push(quote! { #value.as_bytes() });
}
SeedElement::Expression(expr) => {
match &**expr {
syn::Expr::Field(field_expr) => {
if let syn::Member::Named(field_name) = &field_expr.member {
match &*field_expr.base {
syn::Expr::Field(nested_field) => {
if let syn::Member::Named(base_name) = &nested_field.member {
if base_name == "accounts" {
if let syn::Expr::Path(path) = &*nested_field.base {
if let Some(_segment) = path.path.segments.first() {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions
.push(quote! { #field_name.as_ref() });
} else {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions
.push(quote! { #field_name.as_ref() });
}
} else {
parameters.push(
quote! { #field_name: &solana_pubkey::Pubkey },
);
expressions.push(quote! { #field_name.as_ref() });
}
} else {
parameters.push(
quote! { #field_name: &solana_pubkey::Pubkey },
);
expressions.push(quote! { #field_name.as_ref() });
}
} else {
parameters
.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions.push(quote! { #field_name.as_ref() });
}
}
syn::Expr::Path(path) => {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "data" {
if let Some(data_spec) = instruction_data
.iter()
.find(|d| d.field_name == *field_name)
{
let param_type = &data_spec.field_type;
let param_with_ref = if is_pubkey_type(param_type) {
quote! { #field_name: &#param_type }
} else {
quote! { #field_name: #param_type }
};
parameters.push(param_with_ref);
expressions.push(quote! { #field_name.as_ref() });
} else {
return Err(syn::Error::new_spanned(
field_name,
format!("data.{} used in seeds but no type specified", field_name),
));
}
} else {
parameters.push(
quote! { #field_name: &solana_pubkey::Pubkey },
);
expressions.push(quote! { #field_name.as_ref() });
}
} else {
parameters
.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions.push(quote! { #field_name.as_ref() });
}
}
_ => {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions.push(quote! { #field_name.as_ref() });
}
}
}
}
syn::Expr::MethodCall(method_call) => {
if let syn::Expr::Field(field_expr) = &*method_call.receiver {
if let syn::Member::Named(field_name) = &field_expr.member {
if let syn::Expr::Path(path) = &*field_expr.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "data" {
if let Some(data_spec) = instruction_data
.iter()
.find(|d| d.field_name == *field_name)
{
let param_type = &data_spec.field_type;
let param_with_ref = if is_pubkey_type(param_type) {
quote! { #field_name: &#param_type }
} else {
quote! { #field_name: #param_type }
};
parameters.push(param_with_ref);

let method_name = &method_call.method;
expressions.push(
quote! { #field_name.#method_name().as_ref() },
);
} else {
return Err(syn::Error::new_spanned(
field_name,
format!("data.{} used in seeds but no type specified", field_name),
));
}
} else if segment.ident == "ctx" {
// ctx.field.method() -> add field as Pubkey parameter
parameters.push(
quote! { #field_name: &solana_pubkey::Pubkey },
);
let method_name = &method_call.method;
expressions.push(
quote! { #field_name.#method_name().as_ref() },
);
}
}
}
}
} else if let syn::Expr::Path(path_expr) = &*method_call.receiver {
if let Some(ident) = path_expr.path.get_ident() {
parameters.push(quote! { #ident: &solana_pubkey::Pubkey });
expressions.push(quote! { #ident.as_ref() });
}
}
}
syn::Expr::Path(path_expr) => {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str
.chars()
.all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit())
{
if ident_str == "LIGHT_CPI_SIGNER" {
expressions.push(quote! { #ident.cpi_signer.as_ref() });
} else {
expressions.push(quote! { #ident.as_bytes() });
}
} else {
parameters.push(quote! { #ident: &solana_pubkey::Pubkey });
expressions.push(quote! { #ident.as_ref() });
}
} else {
expressions.push(quote! { (#expr).as_ref() });
}
}
syn::Expr::Call(call_expr) => {
// Recursively map data.* to parameter names in function call arguments
fn map_client_call_arg(
arg: &syn::Expr,
instruction_data: &[InstructionDataSpec],
parameters: &mut Vec<TokenStream>,
) -> TokenStream {
match arg {
syn::Expr::Reference(ref_expr) => {
let inner = map_client_call_arg(
&ref_expr.expr,
instruction_data,
parameters,
);
quote! { &#inner }
}
syn::Expr::Field(field_expr) => {
if let syn::Member::Named(field_name) = &field_expr.member {
if let syn::Expr::Path(path) = &*field_expr.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "data" {
// Add parameter if needed
if let Some(data_spec) = instruction_data
.iter()
.find(|d| d.field_name == *field_name)
{
let param_type = &data_spec.field_type;
let param_with_ref =
if is_pubkey_type(param_type) {
quote! { #field_name: &#param_type }
} else {
quote! { #field_name: #param_type }
};
if !parameters.iter().any(|p| {
p.to_string()
.contains(&field_name.to_string())
}) {
parameters.push(param_with_ref);
}
}
return quote! { #field_name };
} else if segment.ident == "ctx" {
// ctx.field -> add as Pubkey parameter
if !parameters.iter().any(|p| {
p.to_string()
.contains(&field_name.to_string())
}) {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
}
return quote! { #field_name };
}
}
}
}
quote! { #field_expr }
}
syn::Expr::MethodCall(method_call) => {
let receiver = map_client_call_arg(
&method_call.receiver,
instruction_data,
parameters,
);
let method = &method_call.method;
let args: Vec<_> = method_call
.args
.iter()
.map(|a| {
map_client_call_arg(a, instruction_data, parameters)
})
.collect();
quote! { (#receiver).#method(#(#args),*) }
}
syn::Expr::Call(nested_call) => {
let func = &nested_call.func;
let args: Vec<_> = nested_call
.args
.iter()
.map(|a| {
map_client_call_arg(a, instruction_data, parameters)
})
.collect();
quote! { (#func)(#(#args),*) }
}
_ => quote! { #arg },
}
}

let mut mapped_args: Vec<TokenStream> = Vec::new();
for arg in &call_expr.args {
let mapped =
map_client_call_arg(arg, instruction_data, &mut parameters);
mapped_args.push(mapped);
}
let func = &call_expr.func;
expressions.push(quote! { (#func)(#(#mapped_args),*).as_ref() });
}
syn::Expr::Reference(ref_expr) => {
let (ref_params, ref_exprs) =
analyze_seed_spec_for_client_expr(&ref_expr.expr, instruction_data)?;
parameters.extend(ref_params);
if let Some(first_expr) = ref_exprs.first() {
expressions.push(quote! { (#first_expr).as_ref() });
}
}
_ => {
expressions.push(quote! { (#expr).as_ref() });
}
}
}
}
}

Ok((parameters, expressions))
}

#[inline(never)]
fn analyze_seed_spec_for_client_expr(
expr: &syn::Expr,
instruction_data: &[InstructionDataSpec],
) -> Result<(Vec<TokenStream>, Vec<TokenStream>)> {
let mut parameters = Vec::new();
let mut expressions = Vec::new();

match expr {
syn::Expr::Field(field_expr) => {
if let syn::Member::Named(field_name) = &field_expr.member {
if let syn::Expr::Field(nested_field) = &*field_expr.base {
if let syn::Member::Named(base_name) = &nested_field.member {
if base_name == "accounts" {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions.push(quote! { #field_name });
} else if base_name == "data" {
// Use declared instruction_data types to determine parameter type
if let Some(data_spec) = instruction_data
.iter()
.find(|d| d.field_name == *field_name)
{
let param_type = &data_spec.field_type;
let param_with_ref = if is_pubkey_type(param_type) {
quote! { #field_name: &#param_type }
} else {
quote! { #field_name: #param_type }
};
parameters.push(param_with_ref);
expressions.push(quote! { #field_name });
} else {
return Err(syn::Error::new_spanned(
field_name,
format!(
"data.{} used in seeds but no type specified",
field_name
),
));
}
}
}
} else if let syn::Expr::Path(path) = &*field_expr.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "ctx" {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
expressions.push(quote! { #field_name });
}
}
}
}
}
syn::Expr::MethodCall(method_call) => {
let (recv_params, _) =
analyze_seed_spec_for_client_expr(&method_call.receiver, instruction_data)?;
parameters.extend(recv_params);
}
syn::Expr::Call(call_expr) => {
for arg in &call_expr.args {
let (arg_params, _) = analyze_seed_spec_for_client_expr(arg, instruction_data)?;
parameters.extend(arg_params);
}
}
syn::Expr::Reference(ref_expr) => {
let (ref_params, _) =
analyze_seed_spec_for_client_expr(&ref_expr.expr, instruction_data)?;
parameters.extend(ref_params);
}
syn::Expr::Path(path_expr) => {
if let Some(ident) = path_expr.path.get_ident() {
let name = ident.to_string();
if !(name == "ctx"
|| name == "data"
|| name
.chars()
.all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()))
{
parameters.push(quote! { #ident: &solana_pubkey::Pubkey });
}
}
}
_ => {}
}

Ok((parameters, expressions))
}

fn camel_to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
}
result
}

fn is_pubkey_type(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
type_name == "Pubkey" || type_name.contains("Pubkey")
} else {
false
}
} else {
false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid duplicating is_pubkey_type logic; reuse the shared utility helper

This file defines its own is_pubkey_type at the bottom:

fn is_pubkey_type(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty {
        if let Some(segment) = type_path.path.segments.last() {
            let type_name = segment.ident.to_string();
            type_name == "Pubkey" || type_name.contains("Pubkey")
        } else {
            false
        }
    } else {
        false
    }
}

but sdk-libs/macros/src/compressible/utils.rs already has an is_pubkey_type helper (slightly different, but semantically similar). Keeping two versions around increases the risk they diverge (e.g., one treats wrapper types differently than the other), and makes it harder to reason about how instruction-data fields are inferred into client function parameters.

It would be cleaner to:

  • Move to a single canonical is_pubkey_type in compressible::utils (with the behavior you actually want), and
  • Import and reuse it here instead of re-implementing.

That keeps the type inference logic consistent across macro entrypoints.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/seed_providers.rs around lines 516-897,
there is a duplicated is_pubkey_type implementation at the bottom; replace this
local duplicate by importing and reusing the canonical is_pubkey_type from
sdk-libs/macros/src/compressible/utils.rs: remove the local function, add the
appropriate use/import for compressible::utils::is_pubkey_type (or the correct
module path), and ensure any differences in behavior are reconciled by updating
the single helper in utils.rs so callers here keep the expected semantics.

Comment on lines +93 to +125
for field in fields {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;

if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
continue;
}

let has_override = compress_as_fields
.as_ref()
.is_some_and(|cas| cas.fields.iter().any(|f| &f.name == field_name));

if has_override {
let override_value = compress_as_fields
.as_ref()
.unwrap()
.fields
.iter()
.find(|f| &f.name == field_name)
.unwrap();
let value = &override_value.value;
field_assignments.push(quote! {
#field_name: #value,
});
} else if is_copy_type(field_type) {
field_assignments.push(quote! {
#field_name: self.#field_name,
});
} else {
field_assignments.push(quote! {
#field_name: self.#field_name.clone(),
});
}
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

Skip compression_info in the generated field loops

The macro currently emits compression_info: None in the struct literal and re-inserts compression_info: self.compression_info.clone() via the per-field loop, which fails to compile (“field specified more than once”). The same oversight makes size() double-count the compression info bytes. Treat compression_info like a reserved field and leave it to the dedicated logic.

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
         {
             continue;
         }
@@
-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
         {
             continue;
         }

Also applies to: 154-164

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/traits.rs around lines 93-125 (also apply
same change at 154-164), the per-field generation loop currently includes the
reserved field compression_info which causes duplicate field assignments and
double-counting of its size; update the loop to skip any field whose ident is
"compression_info" (treat it like other skip-annotated fields) so that
compression_info is only emitted and accounted for by the dedicated logic
elsewhere, i.e., check field_name == "compression_info" and continue before
generating assignments.

Comment on lines +41 to +76
/// Determines if a type is a Copy type (primitives, Pubkey, and Options of Copy types).
///
/// This is used to decide whether to use `.clone()` or direct copy during field assignments.
#[inline(never)]
pub(crate) fn is_copy_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
matches!(
type_name.as_str(),
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "f32"
| "f64"
| "bool"
| "char"
| "Pubkey"
) || (type_name == "Option" && has_copy_inner_type(&segment.arguments))
} else {
false
}
}
Type::Array(_) => true,
_ => false,
}
}
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 | 🟡 Minor

🧩 Analysis chain

Refine is_copy_type handling for array types

is_copy_type currently treats any Type::Array(_) as Copy:

Type::Array(_) => true,

In Rust, [T; N] is Copy only when T: Copy. This heuristic will therefore classify arrays of non‑Copy types as Copy, which can cause generated code to prefer direct copies where a .clone() (or a different pattern) is required and lead to confusing compile‑time errors in macro expansions.

A more precise (but still cheap) approach would be to introspect the element type and reuse your existing logic:

pub(crate) fn is_copy_type(ty: &Type) -> bool {
    match ty {
        Type::Path(type_path) => { /* existing logic */ }
-       Type::Array(_) => true,
+       Type::Array(arr) => {
+           // For `[T; N]`, treat it as Copy only if `T` is Copy.
+           is_copy_type(&*arr.elem)
+       }
        _ => false,
    }
}

That keeps the heuristic aligned with Rust’s Copy rules while still providing the information the macros need for deciding between copy vs clone in generated code.

Also applies to: 80-90


I'll verify this review by examining the actual codebase context.


🏁 Script executed:

#!/bin/bash
# Inspect the full file around the is_copy_type function
cat sdk-libs/macros/src/compressible/utils.rs | head -100

Length of output: 3310


🏁 Script executed:

#!/bin/bash
# Search for usages of is_copy_type
rg -n 'is_copy_type' --type rust -B2 -A2

Length of output: 6137


🏁 Script executed:

#!/bin/bash
# Look at the file more carefully to understand array handling
rg -n 'Type::Array' --type rust -B5 -A5

Length of output: 16830


Fix array element type checking in is_copy_type

The function incorrectly classifies all Type::Array(_) as Copy. In Rust, [T; N] is Copy only when T: Copy. Arrays containing non-Copy types (e.g., [String; 5]) will be treated as copyable, generating direct assignment in macro code instead of .clone(), causing compile errors.

Your proposed fix is correct. A matching implementation already exists in program-libs/zero-copy-derive/src/shared/utils.rs (lines 372–374), which recursively checks the element type:

Type::Array(array) => {
    // Arrays are Copy if their element type is Copy
    return is_copy_type(&array.elem);
}

Apply the same pattern to sdk-libs/macros/src/compressible/utils.rs line 73.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/utils.rs around lines 41 to 76, the match
arm Type::Array(_) currently returns true for all arrays which incorrectly
treats arrays of non-Copy element types as Copy; change that arm to recurse on
the element type (i.e., Type::Array(array) => is_copy_type(&array.elem)) so
arrays are considered Copy only when their element type is Copy (replace the
unconditional true with a call to is_copy_type on array.elem at the line ~73).

Comment on lines +22 to +28
#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
pub struct RentSponsor {
pub program_id: [u8; 32],
pub rent_sponsor: [u8; 32],
pub bump: u8,
pub version: u16,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding Default derive for easier placeholder construction.

The RentSponsor struct is well-shaped with appropriate derives for serialization. However, if this is frequently constructed with zeroed values (e.g., as a placeholder in configs or options), adding #[derive(Default)] would improve ergonomics. This would enable RentSponsor::default() with all fields zeroed.

If there's no need for placeholder construction, the current implementation is fine.

🤖 Prompt for AI Agents
In sdk-libs/sdk-types/src/lib.rs around lines 22 to 28, the RentSponsor struct
lacks a Default implementation which would simplify creating zeroed/place-holder
instances; add #[derive(Default)] to the struct's derive list (or provide an
explicit impl Default that returns zeroed arrays and zero values) so callers can
use RentSponsor::default() for ergonomic placeholder construction.

Comment on lines +69 to +75
// Initialize CompressionInfo from config
// Note: Rent sponsor is not stored per-account; compression always sends rent to config's rent_sponsor
use solana_sysvar::{clock::Clock, Sysvar};
let current_slot = Clock::get()?.slot;
*account_data.compression_info_mut_opt() = Some(
super::compression_info::CompressionInfo::new_from_config(compression_config, current_slot),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

CompressionInfo initialization using Clock is correct, but verify error handling.

The new initialization path:

  1. Gets current slot via Clock::get()?.slot (line 72) ✓
  2. Creates CompressionInfo from config with new_from_config(compression_config, current_slot) (lines 73-75) ✓

Potential issue: Clock::get()? can fail if the Clock sysvar account is not available or corrupted. In a well-formed Solana program context this should never happen, but consider whether the error message is sufficient. The current error propagation relies on the ? operator, which will return a generic ProgramError.

If Clock access fails, the user gets a non-descriptive error. Consider wrapping it:

let current_slot = Clock::get()
    .map_err(|e| {
        msg!("Failed to access Clock sysvar: {:?}", e);
        LightSdkError::ConstraintViolation
    })?
    .slot;
🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/compress_account_on_init.rs around lines 69 to
75, replace the direct use of Clock::get()? with explicit error handling: call
Clock::get(), map the error to log a descriptive msg! including the original
error, convert it to a specific LightSdkError (e.g.,
LightSdkError::ConstraintViolation) and propagate that via ?; then extract .slot
from the successful result and continue to call CompressionInfo::new_from_config
as before.

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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

306-385: actual_rent_sponsor branch currently can’t hit the Some arm

Inside compress_and_close_ctoken_accounts, rent_sponsor_pubkey is always initialized to None at the start of each loop iteration and only set inside the else branch of the actual_rent_sponsor if let:

let mut rent_sponsor_pubkey: Option<Pubkey> = None;

let actual_rent_sponsor = if let Some(sponsor) = rent_sponsor_pubkey {
    sponsor
} else {
    // ... possibly set rent_sponsor_pubkey from extension or signers ...
    rent_sponsor_pubkey.ok_or(TokenSdkError::InvalidAccountData)?
};

As written, the Some(sponsor) arm is unreachable, which makes the control flow harder to follow and suggests an unused override hook.

If you don’t plan to pre-populate rent_sponsor_pubkey from elsewhere, consider simplifying to a single branch that derives the sponsor from the extension or signer list:

-        let actual_rent_sponsor = if let Some(sponsor) = rent_sponsor_pubkey {
-            sponsor
-        } else {
-            // Check if there's a rent recipient in the compressible extension
-            if let Some(extensions) = &compressed_token.extensions {
-                for extension in extensions {
-                    if let ZExtensionStruct::Compressible(ext) = extension {
-                        // Check if rent_sponsor is set (non-zero)
-                        if ext.rent_sponsor != [0u8; 32] {
-                            rent_sponsor_pubkey = Some(Pubkey::from(ext.rent_sponsor));
-                        }
-                        break;
-                    }
-                }
-            }
-
-            // If still no rent recipient, find the fee payer (first signer)
-            if rent_sponsor_pubkey.is_none() {
-                for account in packed_accounts.iter() {
-                    if account.is_signer {
-                        rent_sponsor_pubkey = Some(*account.key);
-                        break;
-                    }
-                }
-            }
-            rent_sponsor_pubkey.ok_or(TokenSdkError::InvalidAccountData)?
-        };
+        let actual_rent_sponsor = {
+            // Check if there's a rent recipient in the compressible extension
+            if let Some(extensions) = &compressed_token.extensions {
+                for extension in extensions {
+                    if let ZExtensionStruct::Compressible(ext) = extension {
+                        if ext.rent_sponsor != [0u8; 32] {
+                            rent_sponsor_pubkey = Some(Pubkey::from(ext.rent_sponsor));
+                        }
+                        break;
+                    }
+                }
+            }
+
+            // If still no rent recipient, fall back to the first signer (typically fee payer)
+            if rent_sponsor_pubkey.is_none() {
+                for account in packed_accounts.iter() {
+                    if account.is_signer {
+                        rent_sponsor_pubkey = Some(*account.key);
+                        break;
+                    }
+                }
+            }
+
+            rent_sponsor_pubkey.ok_or(TokenSdkError::InvalidAccountData)?
+        };

That keeps the intended behavior while removing an effectively dead path.

♻️ Duplicate comments (27)
programs/system/Cargo.toml (1)

28-29: Feature refactoring incomplete—light-program-profiler remains required despite unused features.

This appears to be the same concern previously flagged. Both profile-program (line 28) and profile-heap (line 29) no longer reference light-program-profiler, yet the dependency remains required on line 55 with no optional flag or feature activation.

The previous review marked this as addressed, but the pattern persists: an unused required dependency after refactoring.

Please verify one of the following:

  1. If light-program-profiler is unused across the codebase: Remove the dependency from line 55 entirely.
  2. If light-program-profiler is needed conditionally: Make it optional and restore the feature gate:
    profile-program = ["dep:light-program-profiler"]
    light-program-profiler = { workspace = true, optional = true }

Run a workspace build and search the codebase to confirm no code expects this as a required dependency.

Also applies to: 55-55

sdk-libs/program-test/Cargo.toml (1)

32-32: Critical: Unresolved dependency blocking publishing.

Per past review comments, light-compressible-client should be made optional to unblock publishing light-program-test. The previous review marked this as critical, noting:

"Make this optional again else we cannot publish a new version of light-program-test until we published light-compressible-client."

This remains unresolved.

Apply this diff:

-light-compressible-client = { workspace = true, features = ["anchor"] }
+light-compressible-client = { workspace = true, features = ["anchor"], optional = true }

Then add dep:light-compressible-client to the devenv feature on line 10.

sdk-libs/compressible-client/src/lib.rs (4)

79-99: Config PDA bump handling is still inconsistent between initialize and update

initialize_compression_config allows a caller-specified config_bump:

let config_bump = config_bump.unwrap_or(0);
let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump);

but update_compression_config always derives the config PDA with a hardcoded bump of 0:

let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0);

If anyone initializes with a non-zero bump, update_compression_config will target a different PDA than the one actually created, causing silent misdirected updates or failures.

To keep the API truthful and safe, either:

  • Add a config_bump: u8 parameter to update_compression_config (and thread it through to derive_pda), or
  • If 0 is the only supported bump, remove the Option<u8> from initialize_compression_config and hardcode 0 there too, documenting that only a single config instance is supported.

Right now the surface suggests arbitrary bumps are supported, but only 0 is actually handled end-to-end.

Also applies to: 130-150


205-217: Still unwrapping CPI context from the first compressed account (possible panic and wrong source)

When both tokens and PDAs are present, you still derive the CPI context from the first compressed account and unwrap it:

if has_pdas && has_tokens {
    let cpi_context_of_first_input =
        compressed_accounts[0].0.tree_info.cpi_context.unwrap();
    let system_config = SystemAccountMetaConfig::new_with_cpi_context(
        *program_id,
        cpi_context_of_first_input,
    );
    remaining_accounts.add_system_accounts_v2(system_config)?;
}

Two problems remain:

  1. compressed_accounts[0] might be a pure token with cpi_context == None, which will panic on unwrap().
  2. Even when it’s Some, it might not be the PDA whose context you actually need; you want “first account that needs CPI context,” not “first element in the slice.”

A safer pattern is:

if has_pdas && has_tokens {
    let cpi_context = compressed_accounts
        .iter()
        .find_map(|(acc, _)| acc.tree_info.cpi_context)
        .ok_or("Expected CPI context for PDA inputs but found none")?;
    let system_config =
        SystemAccountMetaConfig::new_with_cpi_context(*program_id, cpi_context);
    remaining_accounts.add_system_accounts_v2(system_config)?;
} else {
    ...
}

This avoids panics and makes the data dependency explicit.


75-77: Clarify or remove the commented-out legacy discriminator

Leaving the old COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR value commented out without context is confusing:

pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] =
    // [89, 130, 165, 88, 12, 207, 178, 185];
    [70, 236, 171, 120, 164, 93, 113, 181];

Either remove the legacy bytes entirely or replace that commented line with a clear note like // legacy value retained for reference; do not use so it’s obvious which discriminator is authoritative.


287-299: accounts_to_compress parameter is still unused beyond a length check

In compress_accounts_idempotent, you accept both account_pubkeys and accounts_to_compress, but after the length check you never actually use the Account data:

if account_pubkeys.len() != accounts_to_compress.len() {
    return Err("Accounts pubkeys length must match accounts length".into());
}

// accounts_to_compress is not referenced again; only pubkeys are used
for account in account_pubkeys {
    accounts.push(AccountMeta::new(*account, false));
}

This makes the parameter look like a partially implemented validation step and can mislead callers into thinking additional checks happen here.

Either drop accounts_to_compress from the signature (and the length check) to keep the API minimal, or start using it meaningfully (e.g., sanity-checking owners/lamports/rent against the proof) so its presence is justified.

Also applies to: 317-332

sdk-libs/sdk-types/src/lib.rs (1)

22-28: Consider deriving Default for RentSponsor placeholders

Since RentSponsor is a simple POD-like struct used across crates, supporting RentSponsor::default() can make it easier to construct “zero” placeholders in configs or tests (all-zero pubkeys, bump = 0, version = 0). Adding #[derive(Default)] would give that at no runtime cost.

sdk-libs/sdk/src/compressible/compression_info.rs (2)

254-320: claim_completed_epoch_rent return value still contradicts its documentation

The doc says:

Returns Some(claimed) if any lamports were claimed; None if account is compressible or nothing to claim.

The implementation:

  • Returns Ok(None) when the account is compressible (good), but
  • Returns Ok(Some(0)) when calculate_claimable_rent yields None or Some(0).

That makes it impossible for callers to distinguish “no claim performed” from “claim succeeded but had zero payout,” and it doesn’t match the documented contract.

You probably want “no lamports claimed for any reason” to be Ok(None) and only return Ok(Some(amount)) when amount > 0, e.g.:

-    if let Some(amount) = claimable {
-        if amount > 0 {
+    if let Some(amount) = claimable {
+        if amount > 0 {
             // ... transfer logic ...
             return Ok(Some(amount));
         }
-    }
-    Ok(Some(0))
+    }
+    Ok(None)

This keeps the API aligned with the docs and gives callers a clear signal.


322-362: System Program ID is incorrect in transfer_lamports_cpi

transfer_lamports_cpi currently hard-codes a zeroed pubkey as the System Program ID:

const SYSTEM_PROGRAM_ID: [u8; 32] = [0, 0, ..., 0];

let transfer_instruction = Instruction {
    program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
    ...
};

This will not match Solana’s real System Program (11111111111111111111111111111111), so any CPI built here will fail when top_up_rent tries to transfer lamports.

You already receive system_program: &AccountInfo as a parameter; that should be used as the instruction’s program_id instead, and the hard-coded constant can be removed:

-    // System Program ID
-    const SYSTEM_PROGRAM_ID: [u8; 32] = [ /* ... zeroes ... */ ];
-    let transfer_instruction = Instruction {
-        program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
+    let transfer_instruction = Instruction {
+        program_id: *system_program.key,
         accounts: vec![
             AccountMeta::new(*from.key, true),
             AccountMeta::new(*to.key, false),
         ],
         data: instruction_data,
     };

Optionally, you can assert that *system_program.key is the canonical System Program ID and return a clear error if not.

sdk-libs/macros/src/compressible/pack_unpack.rs (1)

7-20: Macro still assumes non-generic, Clone-able structs and a magic compression_info field

The derive is powerful, but there are a few important implicit assumptions that are still not enforced in code:

  1. Generics are not handled at all
    derive_compressible_pack and the helper fns emit impls for #struct_name and Packed#struct_name without threading through input.generics or where-clauses. If someone writes struct Foo<T> { .. } and applies this derive, the expansion will try to impl Pack for Foo (no <T>) and fail in a non-obvious way.

    A minimal, user-friendly improvement is to reject generic structs up-front with a clear error:

    #[inline(never)]
    pub fn derive_compressible_pack(input: DeriveInput) -> Result<TokenStream> {
  • let struct_name = &input.ident;
  • if !input.generics.params.is_empty() {
  •    return Err(syn::Error::new_spanned(
    
  •        &input.ident,
    
  •        "derive(CompressiblePack) does not support generic structs yet",
    
  •    ));
    
  • }
  • let struct_name = &input.ident;

That prevents confusing downstream errors until full generic support is implemented.

2. **Implicit `Clone` requirement for non-`Copy` fields**  
Both the packed and unpacked impls use `.clone()` when `is_copy_type` is false:

```rust
#field_name: self.#field_name.clone()

This effectively requires the original struct to be Clone, but there’s no explicit bound, so users just get a “no method named clone” error from inside the generated code. At minimum, call this out clearly in the derive’s docs (GUIDE + rustdoc) so consumers know to derive Clone on any struct using this macro.

  1. Magic by-name behavior for compression_info
    Any field literally named compression_info is forced to None in both pack and unpack:

    if *field_name == "compression_info" {
        quote! { #field_name: None }
    }

    That’s fine as a convention for transient metadata, but it’s also very “magical”: an unrelated field accidentally named compression_info would silently lose its data. The new GUIDE does mention the expected compression_info: Option<CompressionInfo> field, but it would be safer to either:

    • Keep documenting this convention very prominently, or
    • Switch to an explicit attribute (e.g. #[compression_info]) instead of relying purely on the field name.

Overall, the generated code paths look correct for the intended use cases, but tightening these constraints (or at least surfacing them explicitly) will save downstream users a lot of head-scratching when they step outside the happy path.

Also applies to: 22-143, 145-186

sdk-libs/program-test/src/compressible.rs (1)

184-209: auto_compress_program_pdas still ignores compression_authority from config and can silently no-op

The new config handling is a big improvement (deriving the PDA via CpdaCompressibleConfig::derive_pda, fetching the account, and returning early when it’s missing), but the compression authority is still hard-coded to the payer:

let cfg = CpdaCompressibleConfig::try_from_slice(&cfg_acc.data)?;
let rent_sponsor = cfg.rent_sponsor;
// TODO: add coverage for external compression_authority
let compression_authority = payer.pubkey();

and later:

let program_metas = vec![
    AccountMeta::new(payer.pubkey(), true),
    AccountMeta::new_readonly(config_pda, false),
    AccountMeta::new(rent_sponsor, false),
    AccountMeta::new_readonly(compression_authority, false),
];

In combination with try_compress_chunk swallowing per-account errors, this has two practical consequences:

  1. Wrong authority when config uses a distinct key
    As soon as cfg.compression_authority differs from payer, the compress instruction will be constructed with the wrong authority account. The on-chain program will (correctly) reject it, but the helper just continues to the next PDA, so auto_compress_program_pdas quietly becomes a no-op for those programs.

  2. Tests can give a false sense of coverage
    Any test that sets a “real” external compression authority in the config will appear to run fine, but no PDAs will actually be compressed unless that authority happens to equal the payer.

To align the helper with the real semantics and avoid silent failures, you probably want to:

  • Read the compression authority from the config:
-    let rent_sponsor = cfg.rent_sponsor;
-    // TODO: add coverage for external compression_authority
-    let compression_authority = payer.pubkey();
+    let rent_sponsor = cfg.rent_sponsor;
+    let compression_authority = cfg.compression_authority;
  • And ensure you either:
    • Mark it as a signer in program_metas and sign the transaction with the corresponding keypair in tests, or
    • Explicitly document that in test setups cfg.compression_authority must equal the payer (and enforce that invariant before calling this helper).

Separately, derive_pda(&program_id, 0) hard-codes version/bump 0. If you introduce multiple config versions for a program, this helper will quietly skip programs whose active config lives at a different version. It might be worth either parameterizing the version/bump or asserting that the expected version is 0 when you call this in tests.

Right now the overall behavior is “best-effort and silent on failure”, which is reasonable for a dev helper, but wiring it to the stored compression_authority would make it match production semantics and avoid hard-to-notice no-ops.

Also applies to: 231-289

sdk-libs/macros/src/rent_sponsor.rs (2)

5-64: Reject extra macro arguments and stop defaulting invalid versions to 1

Two behaviors here are risky for a PDA that controls rent flows:

  1. Args::parse accepts an arbitrary number of expressions but only uses the first two. If a caller accidentally passes a third (or more) argument, it’s silently ignored instead of producing a clear compile‑time error.

  2. Both macros parse the version via lit.base10_parse::<u16>().unwrap_or(1u16). Any parse failure (typo, out‑of‑range literal, etc.) quietly falls back to version 1, deriving the wrong rent‑sponsor PDA with no compiler signal.

I’d strongly recommend:

  • Enforce elems.len() <= 2 in Args::parse, returning syn::Error when there are extra arguments.
  • Replace unwrap_or(1u16) with explicit error propagation, preserving the default of 1 only when the version argument is omitted, not when it’s invalid.

For example:

// In Args::parse
if elems.len() > 2 {
    return Err(syn::Error::new(
        input.span(),
        "Expected at most 2 arguments: program id string and optional u16 version",
    ));
}

// In both macros
let version_u16: u16 = match args.version.as_ref() {
    Some(lit) => match lit.base10_parse::<u16>() {
        Ok(v) => v,
        Err(e) => return e.to_compile_error().into(),
    },
    None => 1u16,
};

This keeps the “default version = 1” behavior but turns bad literals or extra arguments into explicit compile‑time errors instead of silently deriving the wrong PDA.

Also applies to: 79-82, 122-125


76-110: Factor out shared rent‑sponsor PDA derivation logic

derive_light_rent_sponsor_pda and derive_light_rent_sponsor both repeat the same core steps: parse Args, validate the program id string, compute version_u16, build seeds, call Pubkey::find_program_address, and turn the resulting bytes/bump into literals.

Extracting that into a private helper (e.g. returning (program_id_bytes, pda_bytes, bump, version_u16) or similar) would:

  • Keep PDA derivation single‑sourced.
  • Reduce the chance of one macro drifting from the other as behavior evolves.
  • Make the public entry points focus solely on shaping their respective output token streams.

This is purely a structural refactor but worthwhile for maintainability.

Also applies to: 119-166

sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

15-20: Decompression space/compression_info handling looks good; tighten Rent::get error mapping and owner/payer assumptions

The new pieces here are mostly solid:

  • Computing space as discriminator_len + T::size(&light_account.account) + CompressionInfo::INIT_SPACE correctly accounts for switching compression_info: NoneSome(...) on the decompressed PDA, assuming INIT_SPACE is the serialized size of CompressionInfo.
  • Initializing compression_info with Some(CompressionInfo::new_decompressed()?) before serializing into the PDA keeps the on‑chain representation consistent with the v2 rent model.
  • Threading rent_sponsor through to invoke_create_account_with_heap and using it as the payer matches the new naming.

Two previously‑raised issues remain, though:

  1. Rent::get error mapping (Lines 108–111, unchanged here)
    Mapping a sysvar access failure to LightSdkError::Borsh is misleading; this isn’t a serialization problem. It would be clearer and more consistent with other CPI/sysvar sites to wrap the underlying ProgramError in LightSdkError::ProgramError (or via From<ProgramError>), while still logging the cause.

  2. Program owner consistency & payer semantics

    • The created account is owned by cpi_accounts.self_program_id(), while LightAccount::<T>::new_close is parameterized by program_id. If those ever diverge, you get a PDA whose owner doesn’t match the logical account type’s program. Adding at least a debug assertion that *program_id == cpi_accounts.self_program_id() (or threading program_id through to the helper) would guard against configuration bugs.
    • Now that rent_sponsor is the payer, call sites must ensure this account is actually capable of paying (has lamports and the required signature semantics, PDA or not). It’s worth double‑checking that the DecompressContext implementations always satisfy System Program’s expectations for the from account in create_account.

None of these block the correctness of the new space/compression handling, but tightening them will make failures easier to reason about and invariants more robust.

Also applies to: 50-76, 82-93, 115-122, 124-132, 135-145

sdk-libs/macros/src/compressible/decompress_context.rs (1)

186-225: Tighten lifetime/attribute handling in derive helper

derive_decompress_context currently:

  • Uses input.generics.lifetimes().next() and silently takes the first lifetime.
  • Parses #[pda_types(...)] and #[token_variant(...)] payloads as bare Idents.

If additional lifetimes are ever added or attributes need qualified paths, this may fail in surprising ways. Consider:

  • Checking that there is exactly one lifetime and erroring otherwise.
  • Parsing attribute payloads as syn::Path instead of Ident so you can later support qualified types without redesigning the macro.

This concern was already raised on a previous revision and still applies.

sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1)

12-30: Critical: unchecked indices and unconditional CPI context requirement remain in decompression runtime

The core structure of process_decompress_tokens_runtime is solid, but the previously reported critical issues are still present:

  1. Packed account indexing can panic (on-chain abort):
    Lines 72–75 index packed_accounts[mint_index as usize] and packed_accounts[owner_index as usize] without bounds checks. Malformed packed data or an upstream bug can produce out‑of‑range indices, which will panic and abort the program. Guard both indices against packed_accounts.len() and return ProgramError::InvalidAccountData on failure instead of panicking.

  2. has_pdas flag does not guard cpi_context() acquisition:
    Lines 64–69 call cpi_accounts.cpi_context() unconditionally, even when has_pdas is false. This means a caller that legitimately has no CPI context account will still fail with MissingRequiredSignature before the flag is consulted. Compute an Option<Pubkey> instead, only calling cpi_context() when has_pdas is true, and pass that down to decompress_full_ctoken_accounts_with_indices.

  3. Unnecessary boxing of token_decompress_indices:
    Lines 58–59 allocate a Box<Vec<_>>, but only &token_decompress_indices is ever used. A plain Vec avoids an extra heap indirection with no downside.

  4. Unused get_authority_seeds in CTokenSeedProvider:
    The trait exposes get_authority_seeds but this runtime never uses it, which can confuse implementors. Either wire it into the flow where authority seeds are actually needed, or document that it’s reserved for future extensions so people know it’s safe to stub.

All four issues were called out in a prior review and still need to be addressed before this path is safe for production on-chain use.

Also applies to: 35-160

sdk-libs/macros/src/compressible/traits.rs (1)

41-61: Fix duplicate compression_info initialization and size double-counting in generated impls

The structural issues around compression_info noted earlier are still present:

  • generate_compress_as_impl always emits compression_info: None, in the struct literal.
  • generate_compress_as_field_assignments walks all fields (except #[skip]), so it will also emit an assignment for the compression_info field (self.compression_info or an override), producing a duplicate field initializer and a hard compile error.
  • generate_size_impl adds a compression_info_size constant, but generate_size_fields will also include compression_info in the serialized-length sum, effectively double-counting it and overestimating account size.

You can fix this by treating compression_info as a special field handled only by the dedicated logic, and by enforcing its presence consistently:

 fn generate_compress_as_field_assignments(
     fields: &Punctuated<Field, Token![,]>,
     compress_as_fields: &Option<CompressAsFields>,
 ) -> Vec<TokenStream> {
     let mut field_assignments = Vec::new();

     for field in fields {
         let field_name = field.ident.as_ref().unwrap();
         let field_type = &field.ty;

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is always handled explicitly in `generate_compress_as_impl`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
@@
 fn generate_size_fields(fields: &Punctuated<Field, Token![,]>) -> Vec<TokenStream> {
     let mut size_fields = Vec::new();

     for field in fields.iter() {
         let field_name = field.ident.as_ref().unwrap();

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is accounted for via `compression_info_size` in `generate_size_impl`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
@@
 pub fn derive_compress_as(input: ItemStruct) -> Result<TokenStream> {
     let struct_name = &input.ident;
     let fields = extract_fields_from_item_struct(&input)?;

-    let compress_as_attr = input
+    let compress_as_attr = input
         .attrs
         .iter()
         .find(|attr| attr.path().is_ident("compress_as"));
@@
-    let compress_as_fields = if let Some(attr) = compress_as_attr {
+    let compress_as_fields = if let Some(attr) = compress_as_attr {
         Some(attr.parse_args::<CompressAsFields>()?)
     } else {
         None
     };
 
+    // Fail early if `compression_info` is missing or misnamed
+    validate_compression_info_field(fields, struct_name)?;
+
     let field_assignments = generate_compress_as_field_assignments(fields, &compress_as_fields);
     Ok(generate_compress_as_impl(struct_name, &field_assignments))
 }

This keeps compression_info under explicit control in both CompressAs and Size, avoids duplicate initialization, and ensures a consistent error message when the field is missing. The same concern was raised in an earlier review and still needs to be addressed.

Also applies to: 86-148, 150-181, 192-209, 219-256

sdk-libs/macros/src/compressible/variant_enum.rs (2)

40-56: Document or parameterize Default variant choice

Default for CompressedAccountVariant still implicitly picks the first provided account type:

impl Default for CompressedAccountVariant {
    fn default() -> Self {
        Self::#first_type(#first_type::default())
    }
}

This means that simply reordering the macro input changes the default variant, which is easy to miss. Either (a) generate a doc comment on the enum clearly stating that the first type defines Default, or (b) extend the macro input to allow specifying an explicit default variant (preferred) so the intent is stable and obvious.

This was raised previously and still applies.


58-76: Clarify invariants for packed/CToken variants and make Unpack behavior consistent

Across the generated impls (DataHasher, HasCompressionInfo, Size, Pack, Unpack), most Packed* and CToken variants are handled via unreachable!(), except for:

Self::PackedCTokenData(_data) => Ok(self.clone()),

in Unpack, which silently treats the packed CToken variant as already “unpacked”. That makes the trait contract non-obvious:

  • Calling these traits on the “wrong” variant panics at runtime.
  • PackedCTokenData::unpack behaves differently from other packed variants.

To make this clearer and more consistent:

  • Add a generated doc comment on CompressedAccountVariant explaining which variants are valid for which traits and that others will panic if used incorrectly. Mention that PackedCTokenData is the only supported input for token decompression paths, if that’s the intent.
  • Consider making PackedCTokenData’s Unpack arm match the others (i.e., unreachable!() or a panic with an explicit message) and handle the “already packed” case in a separate API if needed. That way, invalid-variant usage fails uniformly instead of sometimes cloning and sometimes panicking.

These concerns were already flagged in earlier reviews and remain relevant for downstream ergonomics.

Also applies to: 85-151, 153-195, 197-220

sdk-libs/sdk/src/compressible/config.rs (1)

504-513: Fix duplicate-check to compare Pubkey values, not references

validate_address_space_no_duplicates is still building a HashSet over &Pubkey:

fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
    let mut seen = HashSet::new();
    for pubkey in address_space {
        if !seen.insert(pubkey) {
            msg!("Duplicate pubkey found in address_space: {}", pubkey);
            return Err(LightSdkError::ConstraintViolation);
        }
    }
    Ok(())
}

Because the set stores references, this effectively checks for duplicate pointers, not duplicate pubkey values. If address_space ever allows len > 1, two identical Pubkey values at different indices will not be flagged.

Switch the set to store Pubkey by value so duplicates are detected correctly:

-fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
-    let mut seen = HashSet::new();
-    for pubkey in address_space {
-        if !seen.insert(pubkey) {
+fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
+    let mut seen: HashSet<Pubkey> = HashSet::new();
+    for pubkey in address_space {
+        if !seen.insert(*pubkey) {
             msg!("Duplicate pubkey found in address_space: {}", pubkey);
             return Err(LightSdkError::ConstraintViolation);
         }
     }
     Ok(())
 }

This keeps the invariant correct when you eventually support multiple trees per address space.

sdk-libs/sdk/src/compressible/compress_runtime.rs (1)

46-71: Use structured errors and add bounds checks to avoid panics in runtime helper

Two things worth tightening here:

  1. Opaque ProgramError::Custom(0) for config mismatches (Lines 49–53)
    Returning ProgramError::Custom(0) when ctx.rent_sponsor() or ctx.compression_authority() disagrees with CompressibleConfig obscures the failure mode and diverges from the rest of the compressible module, which already maps validation issues to LightSdkError::ConstraintViolation via From<LightSdkError> for ProgramError. Using the structured error keeps on-chain error codes consistent and easier to diagnose.

    Consider something along the lines of:

  • if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
  •    return Err(ProgramError::Custom(0));
    
  • }
  • if *ctx.compression_authority().key != compression_config.compression_authority {
  •    return Err(ProgramError::Custom(0));
    
  • }
  • if *ctx.rent_sponsor().key != compression_config.rent_sponsor
  •    || *ctx.compression_authority().key != compression_config.compression_authority
    
  • {
  •    return Err(crate::error::LightSdkError::ConstraintViolation.into());
    
  • }

(Import `crate::error::LightSdkError` if not already in scope.)

2. **Guard against length mismatches when slicing PDA accounts (Lines 56–71)**  
The slices

```rust
let cpi_accounts = CpiAccounts::new(
    ctx.fee_payer(),
    &remaining_accounts[system_accounts_offset as usize..],
    cpi_signer,
);

let system_accounts_start = cpi_accounts.system_accounts_end_offset();
let all_post_system = &cpi_accounts.to_account_infos()[system_accounts_start..];

let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
let solana_accounts = &all_post_system[pda_start_in_all_accounts..];

assume:

  • system_accounts_start <= to_account_infos().len(), and
  • compressed_accounts.len() <= all_post_system.len().

If either is violated (e.g., a malformed remaining_accounts array or an incorrect compressed_accounts count), this will panic with an out-of-bounds slice. Since these are ultimately driven by transaction input, it’s safer to add explicit bounds checks and return a constraint-style error instead of panicking.

For example:

-    let system_accounts_start = cpi_accounts.system_accounts_end_offset();
-    let all_post_system = &cpi_accounts.to_account_infos()[system_accounts_start..];
-
-    // PDAs are at the end of remaining_accounts, after all the merkle tree/queue accounts
-    let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
-    let solana_accounts = &all_post_system[pda_start_in_all_accounts..];
+    let system_accounts_start = cpi_accounts.system_accounts_end_offset();
+    let all_infos = cpi_accounts.to_account_infos();
+    if system_accounts_start > all_infos.len()
+        || compressed_accounts.len() > all_infos.len().saturating_sub(system_accounts_start)
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }
+    let all_post_system = &all_infos[system_accounts_start..];
+
+    // PDAs are at the end of the post-system accounts, after merkle tree/queue accounts
+    let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
+    let solana_accounts = &all_post_system[pda_start_in_all_accounts..];

This preserves the existing layout assumptions but fails cleanly instead of panicking if they’re ever violated.

sdk-libs/macros/src/compressible/seed_providers.rs (1)

81-275: Factor out “standard account field” detection to avoid drift and simplify maintenance

The logic that recognizes “standard” account fields (e.g., fee_payer, rent_sponsor, config, ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority, ctoken_config, compression_authority, ctoken_compression_authority) is duplicated in several places:

  • Token seeds path for ctx.accounts.* and ctx.* (Lines ~95–106, 137–148).
  • Authority seeds path for ctx.accounts.* and ctx.* (Lines ~223–229, 263–274).

Every time you add or rename a standard account field, you must update all of these matches!(field_name_str.as_str(), "...") lists in lockstep. That’s error‑prone in a macro this complex.

I’d strongly recommend centralizing this into a small helper in this module, e.g.:

fn is_standard_account_field(name: &str) -> bool {
    matches!(
        name,
        "fee_payer"
            | "rent_sponsor"
            | "config"
            | "ctoken_rent_sponsor"
            | "ctoken_program"
            | "ctoken_cpi_authority"
            | "ctoken_config"
            | "compression_authority"
            | "ctoken_compression_authority"
    )
}

and then replace all four inline matches!(field_name_str.as_str(), ...) occurrences with is_standard_account_field(field_name_str.as_str()).

This keeps all branches (token vs. authority, ctx.accounts vs. ctx) in sync automatically when the standard set changes and makes the intent clearer to future readers.

Optionally, you could also consider reusing or consolidating the is_pubkey_type helper with the one in compressible::utils to avoid divergent definitions over time.

sdk-libs/macros/src/lib.rs (1)

232-237: Align DataHasher derive implementation with its “Alias of LightHasher” documentation

The DataHasher derive is still wired to the SHA variant:

/// Alias of `LightHasher`.
#[proc_macro_derive(DataHasher, attributes(skip, hash))]
pub fn data_hasher(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemStruct);
    into_token_stream(derive_light_hasher_sha(input))
}

Doc comment says this is an alias of LightHasher (the Poseidon-based hasher), but the implementation uses derive_light_hasher_sha, i.e. the SHA256 variant. That’s confusing for users and can lead to mismatched hashes between expectations and actual on-chain/off-chain behavior.

You likely want one of:

  • True alias behavior: switch to the Poseidon implementation:

  • into_token_stream(derive_light_hasher_sha(input))

  • into_token_stream(derive_light_hasher(input))

- **Or** update the doc string to explicitly state that `DataHasher` is a convenience alias for the SHA256-based hasher, not the Poseidon one.

Given the current wording, I’d lean toward updating the implementation to call `derive_light_hasher` so the derive matches its documented semantics.

</blockquote></details>
<details>
<summary>sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)</summary><blockquote>

`179-196`: **Fail fast on PDA derivation mismatch instead of just logging**

In `handle_packed_pda_variant`, a derived PDA mismatch only emits a `msg!` but still proceeds with decompression:

```rust
if derived_pda != *solana_account.key {
  msg!(
      "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
      index,
      solana_account.key,
      derived_pda,
      seeds_vec
  );
}

If the account list or seeds are wrong, continuing past this point means you may create or modify an account whose pubkey does not match the intended PDA, which is both confusing and potentially unsafe.

Instead, treat this as a hard error so bad seeds/accounts abort the transaction:

-    if derived_pda != *solana_account.key {
-        msg!(/* ... */);
-    }
+    if derived_pda != *solana_account.key {
+        msg!(
+            "Derived PDA does not match account at index {}: expected {:?}, got {:?}, seeds: {:?}",
+            index,
+            solana_account.key,
+            derived_pda,
+            seeds_vec
+        );
+        return Err(ProgramError::InvalidArgument);
+        // or: return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

This keeps PDA derivation and account wiring in lockstep and surfaces misconfigurations early.


236-262: Add rent-sponsor validation and bounds checks to prevent inconsistent state and panics

Two robustness gaps in process_decompress_accounts_idempotent:

  1. Rent-sponsor invariants are not enforced (Lines 236–239)
    You load CompressibleConfig for address_space, but unlike the compression path you never verify that ctx.rent_sponsor() matches compression_config.rent_sponsor. That allows a caller to pass a different rent sponsor during decompression than the one configured on-chain.

    To keep compress/decompress semantics aligned and prevent misdirected rent, mirror the compress-side check:

      let compression_config =
          crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
      let address_space = compression_config.address_space[0];
  • if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
  •    return Err(crate::error::LightSdkError::ConstraintViolation.into());
    
  • }

(Import `crate::error::LightSdkError` if not already in scope.)

2. **Potential underflow/panic when deriving PDA account slice (Lines 260–261)**  
This code:

```rust
let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
let solana_accounts = &remaining_accounts[pda_accounts_start..];

will underflow and/or panic if compressed_accounts.len() > remaining_accounts.len(). Since both lengths ultimately come from transaction inputs, a malformed call can crash the program instead of returning a clean error.

Add an explicit guard before subtracting:

-    let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
-    let solana_accounts = &remaining_accounts[pda_accounts_start..];
+    if compressed_accounts.len() > remaining_accounts.len() {
+        msg!(
+            "compressed_accounts.len() ({}) exceeds remaining_accounts.len() ({})",
+            compressed_accounts.len(),
+            remaining_accounts.len()
+        );
+        return Err(ProgramError::NotEnoughAccountKeys);
+    }
+    let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
+    let solana_accounts = &remaining_accounts[pda_accounts_start..];

Together, these changes harden the decompression path, keep it consistent with the compression runtime, and avoid panics on malformed input.

sdk-libs/macros/src/compressible/instructions.rs (2)

424-461: Fix TokenSeedSpec lookup: comparing Ident to String still breaks compilation

In pda_seed_provider_impls you still perform:

let name_str = name.to_string();
let spec = if let Some(ref pda_seed_specs) = pda_seeds {
    pda_seed_specs
        .iter()
        .find(|s| s.variant == name_str)
        .ok_or_else(|| {
            macro_error!(name, "No seed specification for account type '{}'. All accounts must have seed specifications.", name_str)
        })?
} else { ... };

Here s.variant is a syn::Ident and name_str is a String; Ident does not implement PartialEq<String>, so this will not compile and the macros crate cannot build. The earlier review already flagged this, and the issue is still present.

Compare like with like instead, for example:

-        .find(|s| s.variant == name_str)
+        .find(|s| s.variant == *name)

or, if you prefer string comparison:

-        .find(|s| s.variant == name_str)
+        .find(|s| s.variant.to_string() == name_str)

Without this fix, add_compressible_instructions! cannot be used at all.


1182-1302: Generated PDA seed derivation still uses invalid account accessors and unchecked unwrap()

generate_pda_seed_derivation_for_trait (and its helper map_pda_expr_to_params) still has the issues called out in the previous review:

  • For ctx.accounts.<account> seeds, it generates:

    // ctx.accounts.account -> *accounts.account.as_ref().unwrap().key
    return syn::parse_quote! { *accounts.#field_name.as_ref().unwrap().key };
  • For ctx.<field> seeds, it generates:

    // ctx.field -> *accounts.field.as_ref().unwrap().key
    return syn::parse_quote! { *accounts.#field_name.as_ref().unwrap().key };
  • For ctx.accounts.account.key() it rewrites only the receiver, resulting in an expression roughly equivalent to:

    let seed_0 = *accounts.account.as_ref().unwrap().key;
    // later used as `seed_0.key()`

On Anchor account types like Signer<'info> and UncheckedAccount<'info>, the public API is via the Key trait (.key() method) rather than a .key field, and optional accounts are wrapped in Option. The generated code is therefore brittle:

  • It mixes field and method access and can end up calling .key() on a Pubkey.
  • It force-unwraps optional seed accounts (.as_ref().unwrap()), so a missing optional account will panic instead of returning CompressibleInstructionError::MissingSeedAccount, unlike the more defensive c-token seed provider.

To make PDA seed derivation safe and consistent, you should:

  1. Generate proper key access:

    // Non-optional "standard" fields (fee_payer, rent_sponsor, config, ctoken_*)
    return syn::parse_quote! { accounts.#field_name.key() };
    
    // Optional seed accounts
    return syn::parse_quote! {
        accounts.#field_name
            .as_ref()
            .ok_or(CompressibleInstructionError::MissingSeedAccount)?
            .key()
    };
  2. Mirror the token seed provider’s error behavior by avoiding unwrap() and using ok_or / ok_or_else instead.

  3. Consider making PdaSeedDerivation::derive_pda_seeds_with_accounts return a Result<(Vec<Vec<u8>>, solana_pubkey::Pubkey), solana_program_error::ProgramError> so that MissingSeedAccount can be propagated cleanly from handle_packed_pda_variant instead of panicking.

As it stands, any PDA seed that refers to optional accounts (or uses .key()) risks either compilation failures or on-chain panics.

Comment on lines +35 to +181
pub fn generate_decompress_context_trait_impl(
pda_type_idents: Vec<Ident>,
token_variant_ident: Ident,
lifetime: syn::Lifetime,
) -> Result<TokenStream> {
let pda_match_arms: Vec<_> = pda_type_idents
.iter()
.map(|pda_type| {
let packed_name = format_ident!("Packed{}", pda_type);
quote! {
CompressedAccountVariant::#packed_name(packed) => {
match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>(
&*self.rent_sponsor,
cpi_accounts,
address_space,
&solana_accounts[i],
i,
&packed,
&meta,
post_system_accounts,
&mut compressed_pda_infos,
&program_id,
self, // Pass the context itself as seed_accounts
std::option::Option::Some(seed_params_ref),
) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(e) => return std::result::Result::Err(e),
}
}
CompressedAccountVariant::#pda_type(_) => {
unreachable!("Unpacked variants should not be present during decompression");
}
}
})
.collect();

Ok(quote! {
impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> {
type CompressedData = CompressedAccountData;
type PackedTokenData = light_compressed_token_sdk::compat::PackedCTokenData<#token_variant_ident>;
type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress;
type SeedParams = SeedParams;

fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> {
&*self.fee_payer
}

fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> {
&self.config
}

fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> {
&self.rent_sponsor
}

fn ctoken_rent_sponsor(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> {
self.ctoken_rent_sponsor.as_ref()
}

fn ctoken_program(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> {
self.ctoken_program.as_ref().map(|a| &**a)
}

fn ctoken_cpi_authority(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> {
self.ctoken_cpi_authority.as_ref().map(|a| &**a)
}

fn ctoken_config(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> {
self.ctoken_config.as_ref().map(|a| &**a)
}

fn collect_pda_and_token<'b>(
&self,
cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>,
address_space: solana_pubkey::Pubkey,
compressed_accounts: Vec<Self::CompressedData>,
solana_accounts: &[solana_account_info::AccountInfo<#lifetime>],
seed_params: std::option::Option<&Self::SeedParams>,
) -> std::result::Result<(
Vec<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
Vec<(Self::PackedTokenData, Self::CompressedMeta)>,
), 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 mut compressed_pda_infos = Vec::with_capacity(compressed_accounts.len());
let mut compressed_token_accounts = Vec::with_capacity(compressed_accounts.len());

// Get seed_params or use default
let seed_params_ref = match seed_params {
std::option::Option::Some(params) => params,
std::option::Option::None => &SeedParams::default(),
};

for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
let meta = compressed_data.meta;
match compressed_data.data {
#(#pda_match_arms)*
CompressedAccountVariant::PackedCTokenData(mut data) => {
data.token_data.version = 3;
compressed_token_accounts.push((data, meta));
}
CompressedAccountVariant::CTokenData(_) => {
unreachable!();
}
}
}

std::result::Result::Ok((compressed_pda_infos, compressed_token_accounts))
}

#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn process_tokens<'b>(
&self,
remaining_accounts: &[solana_account_info::AccountInfo<#lifetime>],
fee_payer: &solana_account_info::AccountInfo<#lifetime>,
ctoken_program: &solana_account_info::AccountInfo<#lifetime>,
ctoken_rent_sponsor: &solana_account_info::AccountInfo<#lifetime>,
ctoken_cpi_authority: &solana_account_info::AccountInfo<#lifetime>,
ctoken_config: &solana_account_info::AccountInfo<#lifetime>,
config: &solana_account_info::AccountInfo<#lifetime>,
ctoken_accounts: Vec<(Self::PackedTokenData, Self::CompressedMeta)>,
proof: light_sdk::instruction::ValidityProof,
cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>,
post_system_accounts: &[solana_account_info::AccountInfo<#lifetime>],
has_pdas: bool,
) -> std::result::Result<(), solana_program_error::ProgramError> {
light_compressed_token_sdk::decompress_runtime::process_decompress_tokens_runtime(
self,
remaining_accounts,
fee_payer,
ctoken_program,
ctoken_rent_sponsor,
ctoken_cpi_authority,
ctoken_config,
config,
ctoken_accounts,
proof,
cpi_accounts,
post_system_accounts,
has_pdas,
&crate::ID,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

DecompressContext impl wiring matches runtime expectations; consider a defensive index check

The generated impl lines up with DecompressContext and handle_packed_pda_variant (PDA types, packed variants, SeedParams defaulting, and process_tokens delegation all look consistent). One small robustness improvement: in collect_pda_and_token, the loop indexes solana_accounts[i] assuming compressed_accounts.len() == solana_accounts.len(). If those ever diverge (macro misuse or future refactor), this will panic on-chain. A cheap if i >= solana_accounts.len() { return Err(ProgramError::InvalidAccountData); } before indexing would make that failure explicit instead of aborting.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/decompress_context.rs around lines 35 to
181, add a defensive bounds check inside the for-loop in collect_pda_and_token
before any access to solana_accounts[i]; if i >= solana_accounts.len() return
Err(solana_program_error::ProgramError::InvalidAccountData); this makes the
macro-generated code fail with a clear ProgramError instead of panicking when
compressed_accounts and solana_accounts lengths diverge.

Comment on lines +41 to +76
/// Determines if a type is a Copy type (primitives, Pubkey, and Options of Copy types).
///
/// This is used to decide whether to use `.clone()` or direct copy during field assignments.
#[inline(never)]
pub(crate) fn is_copy_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
matches!(
type_name.as_str(),
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "f32"
| "f64"
| "bool"
| "char"
| "Pubkey"
) || (type_name == "Option" && has_copy_inner_type(&segment.arguments))
} else {
false
}
}
Type::Array(_) => true,
_ => false,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Refine Copy detection for arrays and unify Pubkey helpers

Two small correctness/maintainability concerns:

  • is_copy_type treats every Type::Array(_) as Copy. In Rust, [T; N] is Copy only if T: Copy. For arrays of non‑Copy types this will misclassify the field and cause the generated compress_as impl to try to copy by value instead of cloning, resulting in confusing compile errors. Better to inspect the element type and recurse into is_copy_type rather than blanket‑marking arrays as Copy.

  • is_pubkey_type here only matches ident Pubkey, but there’s still a separate is_pubkey_type in seed_providers.rs that uses a looser contains("Pubkey") check (per the provided snippet). Having two subtly different implementations invites drift; it would be cleaner to route all Pubkey detection through this shared helper and delete the local copy.

Also applies to: 93-105

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/utils.rs around lines 41-76 (and similarly
lines 93-105), is_copy_type incorrectly marks every Type::Array(_) as Copy and
the Pubkey detection only matches an exact Ident "Pubkey"; change the
Type::Array branch to inspect the array element type and recursively call
is_copy_type on the element (use the elem field of Type::Array), and broaden the
Pubkey detection in this shared helper to look through path segments/arguments
for any segment matching "Pubkey" (so it catches qualified paths) then remove or
replace the separate looser is_pubkey_type in seed_providers.rs to call this
unified helper.

Comment on lines +47 to +97
pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream {
// Parse the input - just a program ID string literal
let program_id_lit = parse_macro_input!(input as LitStr);
let program_id_str = program_id_lit.value();

// Compute the PDA at compile time using solana-pubkey with "cpi_authority" seed
use std::str::FromStr;

// Parse program ID at compile time
let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) {
Ok(id) => id,
Err(_) => {
return syn::Error::new(
program_id_lit.span(),
"Invalid program ID format. Expected a base58 encoded public key",
)
.to_compile_error()
.into();
}
};

// Use fixed "cpi_authority" seed
let seeds = &[b"cpi_authority".as_slice()];

// Compute the PDA at compile time
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

// Generate the output code with precomputed CpiSigner struct
let program_id_bytes = program_id.to_bytes();
let pda_bytes = pda.to_bytes();

let program_id_literals = program_id_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));
let cpi_signer_literals = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));

let output = quote! {
{
// Use the CpiSigner type with absolute path to avoid import dependency
::light_sdk_types::CpiSigner {
program_id: [#(#program_id_literals),*],
cpi_signer: [#(#cpi_signer_literals),*],
bump: #bump,
}
}
};

output.into()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor duplication between PDA and CpiSigner helpers

derive_light_cpi_signer repeats the parsing/derivation logic from derive_light_cpi_signer_pda. Not a blocker, but factoring the common “parse program_id and derive PDA” into a small helper would reduce the chance of future divergence.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/cpi_signer.rs around lines 47 to 97, the macro
derive_light_cpi_signer duplicates the parse-and-derive-PDA logic found in
derive_light_cpi_signer_pda; extract a small shared helper (e.g.,
parse_program_and_find_pda) that accepts the input LitStr or &str and returns
Result<(solana_pubkey::Pubkey /*program_id*/, solana_pubkey::Pubkey /*pda*/, u8
/*bump*/), syn::Error> so both macros call it; move the FromStr parsing, error
construction using program_id_lit.span(), seeds setup, and find_program_address
call into that helper, keep existing error reporting and then update
derive_light_cpi_signer to call the helper and use its returned values when
building the quoted output.

Comment on lines +6 to +19
/// Converts a `syn::Result<proc_macro2::TokenStream>` to `proc_macro::TokenStream`.
///
/// ## Usage
/// ```ignore
/// #[proc_macro_derive(MyMacro)]
/// pub fn my_macro(input: TokenStream) -> TokenStream {
/// let input = parse_macro_input!(input as DeriveInput);
/// into_token_stream(some_function(input))
/// }
/// ```
#[inline]
pub(crate) fn into_token_stream(result: Result<proc_macro2::TokenStream>) -> TokenStream {
result.unwrap_or_else(|err| err.to_compile_error()).into()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Proc-macro error funnel looks good

into_token_stream correctly normalizes syn::Result<proc_macro2::TokenStream> into a proc_macro::TokenStream with to_compile_error() on failure; this is the right shape for sharing across macro entrypoints. Consider adding a brief note in the crate-level docs pointing macro authors at this helper so error handling stays consistent everywhere.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/utils.rs around lines 6 to 19, the helper
into_token_stream is correct but the crate lacks a brief mention directing macro
authors to use it; add a short note to the crate-level documentation (lib.rs or
README) that describes this helper’s purpose—normalizing
syn::Result<proc_macro2::TokenStream> into proc_macro::TokenStream using
to_compile_error()—and show the small usage snippet from the current doc comment
so authors consistently funnel errors through this utility.

Comment on lines 61 to 72
#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct CompressionInfo {
pub last_written_slot: u64,
/// Version of the compressible config used to initialize this account.
pub config_version: u16,
/// Lamports to top up on each write (from config, stored per-account to avoid passing config on every write)
pub lamports_per_write: u32,
/// Slot when rent was last claimed (epoch boundary accounting).
pub last_claimed_slot: u64,
/// Rent function parameters for determining compressibility/claims.
pub rent_config: RentConfig,
/// Account compression state.
pub state: CompressionState,
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

CompressionInfo rent accounting and INIT_SPACE look consistent; consider a small safety guard

The added fields and helpers (new_from_config, new_decompressed, last_claimed_slot accessors, calculate_top_up_lamports, top_up_rent, INIT_SPACE, and OPTION_COMPRESSION_INFO_SPACE) line up cleanly with the documented CToken-style rent behavior (emergency/compressible vs maintenance vs cruise-control). The layout comment for INIT_SPACE correctly accounts for the new fields.

One thing worth tightening is the division in calculate_top_up_lamports:

let rent_per_epoch = self.rent_config.rent_curve_per_epoch(num_bytes);
let epochs_funded_ahead = available_balance / rent_per_epoch;

If rent_curve_per_epoch were ever misconfigured to return 0, this would panic. If the API doesn’t already guarantee > 0, adding a debug/assert guard (or early-returning an error) would make this more robust in face of bad config.

Also applies to: 83-137, 139-227, 233-236, 243-245

🤖 Prompt for AI Agents
sdk-libs/sdk/src/compressible/compression_info.rs around lines 61-72 (and
similarly 83-137, 139-227, 233-236, 243-245): the division in
calculate_top_up_lamports currently does available_balance / rent_per_epoch and
will panic if rent_curve_per_epoch returns 0; add a guard to handle
rent_per_epoch == 0 by either returning 0 top-up or early error, and use a
debug_assert or explicit check to document the assumption; implement the check
before dividing, log or assert in debug builds, and ensure callers handle the
0-result appropriately so no panic can occur.

}

#[cfg(feature = "v2")]
#[cfg(feature = "devenv")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
#[cfg(feature = "devenv")]
#[cfg(feature = "v2")]

Comment on lines +872 to +875
) -> Result<Response<Items<[u8; 32]>>, IndexerError> {
#[cfg(not(feature = "v2"))]
#[cfg(not(feature = "devenv"))]
unimplemented!("get_subtrees");
#[cfg(feature = "v2")]
#[cfg(feature = "devenv")]
Copy link
Contributor

Choose a reason for hiding this comment

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

pls revert

}
};

let seeds = &[b"rent_sponsor".as_slice(), &version_u16.to_le_bytes()[..]];
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use a method for the derivation?

Comment on lines +98 to +99
let seeds = &[b"rent_sponsor".as_slice(), &version_u16.to_le_bytes()[..]];
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use a method for the derivation?

Comment on lines +101 to +104
let pda_bytes = pda.to_bytes();
let bytes = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));
Copy link
Contributor

@ananas-block ananas-block Nov 18, 2025

Choose a reason for hiding this comment

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

we could add a helper function that returns [#(#program_id_literals),*] this is used in multiple places

/// #[derive(Compressible)]
/// #[compress_as(start_time = 0, end_time = None, score = 0)]
/// pub struct GameSession {
/// #[skip]
Copy link
Contributor

Choose a reason for hiding this comment

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

#[skip] is not applied if we use sha256::LightAccount (the default LightAccount)

Comment on lines +530 to +532
pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream {
cpi_signer::derive_light_cpi_signer(input)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

pls remove I moved this to program-libs/macros since it is used in the ctoken program

///
/// ## Example
///
/// ```ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// ```ignore
/// ```rust

///
/// ## Example
///
/// ```ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// ```ignore
/// ```rust

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

♻️ Duplicate comments (1)
sdk-libs/macros/src/lib.rs (1)

242-247: DataHasher derive still doesn’t match its documentation.

This entry point is documented as an alias of LightHasher, yet it forwards to derive_light_hasher_sha. Either the implementation must call derive_light_hasher or the docs need to say it aliases the SHA variant. Right now users get SHA semantics while the docs promise Poseidon, which is misleading.

Consider the following fix:

 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9911ff and 05408c4.

⛔ Files ignored due to path filters (3)
  • scripts/lint.sh is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/lib.rs is excluded by none and included by none
📒 Files selected for processing (1)
  • sdk-libs/macros/src/lib.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk-libs/macros/src/lib.rs (9)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (3)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
  • process_light_accounts_derive (446-596)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/cpi_signer.rs (1)
  • derive_light_cpi_signer (47-97)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (76-110)
  • derive_light_rent_sponsor (119-166)
🪛 GitHub Actions: examples-tests
sdk-libs/macros/src/lib.rs

[error] 292-292: failed to resolve: use of unresolved module or unlinked crate light_sdk (doc-test compile failure in sdk-libs/macros/src/lib.rs:292)

🪛 GitHub Check: system-programs (sdk-libs, light-sdk-macros light-sdk light-program-test light-client light-compr...
sdk-libs/macros/src/lib.rs

[failure] 399-399:
failed to resolve: use of unresolved module or unlinked crate light_sdk


[failure] 401-401:
cannot find attribute skip in this scope


[failure] 396-396:
failed to resolve: use of unresolved module or unlinked crate light_sdk


[failure] 333-333:
cannot find macro declare_id in this scope


[failure] 340-340:
cannot find attribute program in this scope


[failure] 335-335:
cannot find attribute add_compressible_instructions in this scope


[failure] 332-332:
failed to resolve: use of unresolved module or unlinked crate anchor_lang


[failure] 296-296:
failed to resolve: use of unresolved module or unlinked crate light_sdk


[failure] 303-303:
cannot find attribute skip in this scope


[failure] 292-292:
failed to resolve: use of unresolved module or unlinked crate light_sdk

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Test program-libs-slow
  • GitHub Check: Test program-libs-fast
  • GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...

Comment on lines +276 to +280
#[proc_macro_derive(HasCompressionInfo)]
pub fn has_compression_info(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemStruct);
into_token_stream(compressible::traits::derive_has_compression_info(input))
}
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

Allow #[skip] on HasCompressionInfo just like the docs describe.

The derive is advertised to work with a #[skip]d compression_info field, but the proc-macro declaration never whitelists that attribute. As soon as a user follows the documented pattern, the compiler raises “cannot find attribute skip.” Please extend the derive declaration to accept the attribute.

Use this patch:

-#[proc_macro_derive(HasCompressionInfo)]
+#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
 pub fn has_compression_info(input: TokenStream) -> TokenStream {
📝 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
#[proc_macro_derive(HasCompressionInfo)]
pub fn has_compression_info(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemStruct);
into_token_stream(compressible::traits::derive_has_compression_info(input))
}
#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
pub fn has_compression_info(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemStruct);
into_token_stream(compressible::traits::derive_has_compression_info(input))
}
🤖 Prompt for AI Agents
In sdk-libs/macros/src/lib.rs around lines 276 to 280, the proc-macro
declaration for HasCompressionInfo does not whitelist the #[skip] attribute so
users get “cannot find attribute `skip`”; update the proc-macro attribute to
accept the skip attribute by changing the declaration to include
attributes(skip) (i.e. #[proc_macro_derive(HasCompressionInfo,
attributes(skip))]) so the derive will allow #[skip] on the compression_info
field and then keep the rest of the function body unchanged.

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

♻️ Duplicate comments (3)
sdk-libs/macros/src/lib.rs (3)

153-179: Remove commented-out code.

This large block of commented-out code for LightDiscriminatorSha has been flagged previously and should be removed if the feature is deprecated. If you plan to re-enable it later, replace this block with a single-line TODO referencing a tracking issue.

Apply this diff if the feature is permanently removed:

-// /// SHA256 variant of the LightDiscriminator derive macro.
-// ///
-// /// This derive macro provides the same discriminator functionality as LightDiscriminator
-// /// but is designed to be used with SHA256-based hashing for consistency.
-...
-// }

Or if you plan to re-enable it:

-// /// SHA256 variant of the LightDiscriminator derive macro.
-...
-// }
+// TODO(#ISSUE_NUMBER): Re-enable LightDiscriminatorSha when SHA256 discriminator support is complete

Based on learnings


291-295: Add attributes(skip) to allow #[skip] attribute as documented.

The documentation shows using #[skip] on the compression_info field (line 278), but the proc_macro_derive declaration doesn't whitelist this attribute. Users following the documentation will encounter "cannot find attribute skip" compiler errors.

Apply this fix:

-#[proc_macro_derive(HasCompressionInfo)]
+#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
 pub fn has_compression_info(input: TokenStream) -> TokenStream {

374-397: Remove commented-out code as requested by reviewer.

Reviewer ananas-block requested removal of this block, noting it was moved to program-libs/macros. Please remove this commented-out code.

Apply this diff:

-// /// Adds native compressible instructions for the specified account types
-// ///
-// /// This macro generates thin wrapper processor functions that you dispatch manually.
-// ///
-...
-// }

Based on learnings

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05408c4 and 2f30f02.

📒 Files selected for processing (1)
  • sdk-libs/macros/src/lib.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk-libs/macros/src/lib.rs (11)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/traits.rs (1)
  • process_light_traits (5-23)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (2)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (187-226)
sdk-libs/macros/src/cpi_signer.rs (1)
  • derive_light_cpi_signer (47-97)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (76-110)
  • derive_light_rent_sponsor (119-166)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test batched-merkle-tree-simulate
🔇 Additional comments (3)
sdk-libs/macros/src/lib.rs (3)

256-261: Verify DataHasher implementation matches documentation.

The documentation states this is an "Alias of LightHasher", but the implementation calls derive_light_hasher_sha (SHA256 variant) instead of derive_light_hasher (Poseidon variant). A previous review flagged this inconsistency and marked it as addressed, yet the code still shows the SHA variant.

If the SHA variant is intentional, update the documentation to clarify that DataHasher aliases the SHA-based hasher, not the Poseidon-based LightHasher.

-/// Alias of `LightHasher`.
+/// Alias of `LightHasherSha` (SHA256-based hashing).

Or if it should truly alias LightHasher:

 #[proc_macro_derive(DataHasher, attributes(skip, hash))]
 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }

1-19: Well-organized module structure.

The new module declarations (compressible, cpi_signer, rent_sponsor, utils) are logically organized and properly integrated. The consistent use of the into_token_stream helper throughout the file provides uniform error handling across all macro entry points.


291-295: Excellent macro implementation consistency.

The new proc macros and derives follow a consistent, maintainable pattern with comprehensive documentation. The uniform use of into_token_stream for error handling and the well-structured examples (appropriately marked with ignore) demonstrate attention to lessons from previous reviews. The only critical issue is the missing attributes(skip) declaration for HasCompressionInfo (flagged separately).

Also applies to: 332-335, 366-372, 447-450, 474-477, 564-569, 584-586, 600-602, 617-619

///
/// ## Example
///
/// ```ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use rust language identifier for code fence.

Specify the language identifier for better syntax highlighting in the generated documentation.

-/// ```
+/// ```rust

Based on learnings

🤖 Prompt for AI Agents
In sdk-libs/macros/src/lib.rs around line 593, the doc-code fence uses "ignore"
and should use the "rust" language identifier for proper syntax highlighting in
generated docs; update the triple-backtick fence label from ignore to rust
(i.e., change ```ignore to ```rust) so the example is highlighted correctly.

///
/// ## Example
///
/// ```ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use rust language identifier for code fence.

Specify the language identifier for better syntax highlighting in the generated documentation.

-/// ```
+/// ```rust

Based on learnings

🤖 Prompt for AI Agents
In sdk-libs/macros/src/lib.rs around line 610, the doc code fence uses the
"ignore" language tag; change the opening fence to use "rust" instead (i.e.,
replace ```ignore with ```rust) so the example gets Rust syntax highlighting in
generated docs, and verify any matching closing fence remains unchanged.

@SwenSchaeferjohann
Copy link
Contributor Author

@coderabbitai reset

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

@SwenSchaeferjohann: I'll reset and perform a fresh review of the entire PR.

✅ Actions performed

Full review triggered.

@SwenSchaeferjohann
Copy link
Contributor Author

@coderabbitai pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

✅ Actions performed

Reviews paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
sdk-libs/program-test/Cargo.toml (1)

23-23: Critical dependency issues confirmed — both optional markers and feature entries are missing.

The review comment's assessment is accurate. I've verified the current state:

Problems identified:

  1. Line 23: light-compressed-token-sdk is unconditional but only used behind #[cfg(feature = "devenv")] throughout the codebase
  2. Line 33: light-compressible-client is unconditional but only used behind #[cfg(feature = "devenv")] throughout the codebase
  3. Line 10: The devenv feature lacks activation entries for both dependencies

The codebase uses #[cfg(feature = "devenv")] extensively (compressible.rs, config.rs, light_program_test.rs, test_rpc.rs, and more), meaning both crates are purely optional. Publishing will fail with these unconditional dependencies, and builds without the devenv feature will break if they try to use this crate.

The suggested diffs are correct and complete. Apply them to unblock publishing and resolve CI test failures.

sdk-libs/program-test/src/program_test/test_rpc.rs (1)

151-164: Align warp_slot_forward trait docs with the new PDA auto-compression behavior

The LightProgramTest::warp_slot_forward implementation now clearly auto-compresses PDAs for all auto_mine_cold_state_programs in addition to ctoken accounts (lines 155–162), but the TestRpc trait‑level doc comment at Lines 107–110 still only mentions “compressible ctoken accounts.” To avoid confusion for other TestRpc implementors and callers, update the trait doc to describe the PDA auto‑compression behavior (or at least mention that implementations may also auto‑compress program PDAs) so the contract matches what the primary implementation actually does.

sdk-libs/program-test/src/program_test/light_program_test.rs (1)

37-84: Auto‑mine program tracking field and initialization look appropriate

The new auto_mine_cold_state_programs: Vec<Pubkey> field (Line 37) and its Vec::new() initialization in new() (Line 83) are consistent with how warp_slot_forward uses this list to drive PDA auto‑compression. Given the expected small number of programs in tests, a Vec is a reasonable choice; if this ever grows large or becomes highly dynamic, consider a HashSet to make membership checks cheaper, but there’s no need to change that now.

sdk-libs/program-test/src/program_test/config.rs (1)

122-165: Behavior change: auto_register_custom_programs_for_pda_compression now defaults to true (devenv)

This will auto‑register custom programs for PDA compression in all ProgramTestConfig::default()‑based flows under devenv. That’s consistent with the new compressible runtime, but it does change existing test behavior: any tests that previously relied on not auto‑registering will now need to flip this flag back to false explicitly.

If that “no auto‑registration” mode is still important in your test matrix, consider adding a small helper constructor (or a without_auto_register_for_pda_compression() builder) to make that intent obvious at call sites.

sdk-libs/program-test/src/program_test/compressible_setup.rs (2)

110-141: Add missing discriminator parameter to doc comment

The discriminator: &[u8] parameter at line 132 is documented nowhere in the function's doc comment (lines 110–122). Add it to the # Arguments section to clearly explain its purpose—specifically, that it's the discriminator forwarded to the underlying compressible_instruction::update_compression_config call.


61-99: Update doc comments to document discriminator and config_bump parameters

Confirmed: initialize_compression_config (lines 64–72) accepts discriminator: &[u8] and config_bump: Option<u8>, but the doc comment (lines 54–63) stops at address_space. Similarly, update_compression_config (lines 103–114) accepts discriminator but the doc comment (lines 94–102) omits it.

Both functions correctly forward these parameters into their respective compressible_instruction calls, so the implementation is sound. Please update both doc comments to include descriptions of:

  • discriminator – its purpose and expected shape
  • config_bump – its purpose and when to use None vs Some(_)

This keeps the module self-documenting for callers.

sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

342-369: Remove dead if let Some(sponsor) branch and clarify packed_accounts expectations

The outer if let Some(sponsor) = rent_sponsor_pubkey at line 343 is unreachable—rent_sponsor_pubkey is initialized to None at line 307 and never assigned before that check. This dead code path obscures the actual fallback logic.

Simplify by removing the outer if-let and moving the else branch logic directly into actual_rent_sponsor:

let actual_rent_sponsor = {
    // Check if there's a rent recipient in the compressible extension
    if let Some(extensions) = &compressed_token.extensions {
        for extension in extensions {
            if let ZExtensionStruct::Compressible(ext) = extension {
                if ext.rent_sponsor != [0u8; 32] {
                    return Ok(Pubkey::from(ext.rent_sponsor));
                }
                break;
            }
        }
    }
    
    // Fall back to first signer in packed_accounts
    for account in packed_accounts.iter() {
        if account.is_signer {
            return Ok(*account.key);
        }
    }
    
    Err(TokenSdkError::InvalidAccountData)
}?;

Also document near the call site or in function docs that packed_accounts order matters: the first signer encountered becomes the fallback rent sponsor if the extension lacks one. This implicit dependency deserves visibility.

sdk-libs/sdk/src/compressible/mod.rs (1)

5-26: v2 gating and re‑exports look coherent; verify downstream expectations for close / runtime traits

The new compress_runtime / decompress_idempotent / decompress_runtime modules and their re-exports are consistently gated under feature = "v2", which keeps the expanded runtime surface opt‑in. One small behavioral change is that close::close is only re-exported under v2, even though the module itself is always compiled; non-v2 consumers now need to import crate::compressible::close::close instead of crate::compressible::close.

If that shift is intentional, the module wiring here looks fine. It’s worth double-checking downstream crates (and any public docs) to ensure their imports line up with the new gating, especially for CTokenSeedProvider / DecompressContext which are now available via this module.

Also applies to: 32-40

sdk-libs/macros/src/lib.rs (1)

181-207: Update docstring to reference the current ToByteArray trait, not the removed AsByteVec

The light-hasher crate now uses the ToByteArray trait for byte conversion. The docstring at lines 181-207 incorrectly states that fields must implement AsByteVec, which is not present in the current code.

Update the comment to reference ToByteArray or provide guidance on the actual trait(s) that light-hasher expects, so users understand the real requirements.

♻️ Duplicate comments (39)
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

409-418: Remove the unused lifetime 'b instead of suppressing Clippy

compress_and_close_ctoken_accounts_signed<'b, 'info>(...) doesn’t use 'b in its signature or body, so adding #[allow(clippy::extra_unused_lifetimes)] just hides a genuine redundancy.

Prefer to:

  • Drop the unused lifetime parameter and keep only 'info, and
  • Remove the extra_unused_lifetimes allow.

This keeps the signature simpler and avoids training callers to ignore Clippy here.

sdk-libs/macros/src/cpi_signer.rs (1)

5-97: Factor out shared “parse program ID + derive PDA” logic between the two macros

Both derive_light_cpi_signer_pda and derive_light_cpi_signer:

  • Parse the same LitStr program ID,
  • Validate it via Pubkey::from_str, and
  • Derive the "cpi_authority" PDA with find_program_address.

The logic is currently duplicated almost line‑for‑line. Extracting a small helper (e.g., returning (program_id, pda, bump) or (pda_bytes, bump)) would:

  • Keep error handling and seed usage in one place, and
  • Reduce the chance of future divergence between the PDA‑only and full CpiSigner variants.

The generated code shapes can then stay as they are, just wired to the shared helper.

sdk-libs/macros/src/compressible/variant_enum.rs (2)

40-56: Document that the first account type controls CompressedAccountVariant’s Default

The Default impl always picks the first account type passed into compressed_account_variant and calls its Default. That’s a reasonable convention, but it’s implicit: reordering the macro arguments silently changes the default.

Consider either:

  • Emitting a doc comment on CompressedAccountVariant explaining that the first account type in the macro input defines the default variant (with a short example), or
  • Extending the macro input to allow an explicit “default” account type to be specified and using that instead of positional ordering.

This will make the behavior clear to downstream users of the generated type.


58-219: Make trait behavior and unreachable!() invariants on variants explicit

Across DataHasher, HasCompressionInfo, Size, Pack, and Unpack, most Packed* and the CToken‑related variants are handled via unreachable!() (except PackedCTokenData::unpack, which clones self). That’s consistent with a “perf over defensive checks” trade‑off, but it’s not obvious to callers which variants are valid inputs for which traits. Based on prior work in this repo, these invariants are relied on in tightly‑controlled flows rather than enforced at the type level. (Based on learnings)

To reduce surprises without adding runtime overhead:

  • Generate a doc comment on CompressedAccountVariant that clearly documents which variants are valid for each trait (hashing, compression info, size, pack/unpack), and that invoking them on other variants will panic, and
  • Consider making the PackedCTokenData Unpack impl consistent with the rest (either also unreachable!() or an explicit panic message), so all “invalid” paths fail in the same, well‑documented way.

That keeps the current performance profile but makes the contract much clearer.

sdk-libs/sdk/src/compressible/config.rs (1)

504-513: Duplicate check still compares references, not values

Despite the past comment claiming this was addressed, validate_address_space_no_duplicates still builds a HashSet that infers HashSet<&Pubkey> from the context. When iterating for pubkey in address_space, pubkey is &Pubkey, so seen.insert(pubkey) stores the reference, not the value.

With the current MAX_ADDRESS_TREES_PER_SPACE == 1 this doesn't bite, but it will silently fail when increased.

Apply this diff:

 fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
-    let mut seen = HashSet::new();
+    let mut seen: HashSet<Pubkey> = HashSet::new();
     for pubkey in address_space {
-        if !seen.insert(pubkey) {
+        if !seen.insert(*pubkey) {
             msg!("Duplicate pubkey found in address_space: {}", pubkey);
             return Err(LightSdkError::ConstraintViolation);
         }
     }
     Ok(())
 }
sdk-libs/compressible-client/src/lib.rs (3)

80-164: Config PDA derivation mismatch between initialize and update

initialize_compression_config allows a configurable config_bump (line 89), but update_compression_config always hardcodes 0 at line 138. If a config is initialized with a non-zero bump, updates will target the wrong PDA and silently fail.

Either add a config_bump parameter to update_compression_config or remove the Option<u8> from initialize_compression_config and hardcode 0 in both places.


206-207: Avoid panicking on missing CPI context when mixing tokens and PDAs

Line 207 calls .unwrap() on compressed_accounts[0].0.tree_info.cpi_context, which will panic if the first account is a token with cpi_context = None. Even if a later element has context, this will crash the client.

Apply this diff:

     if has_pdas && has_tokens {
-        let cpi_context_of_first_input =
-            compressed_accounts[0].0.tree_info.cpi_context.unwrap();
+        let cpi_context = compressed_accounts
+            .iter()
+            .find_map(|(acc, _)| acc.tree_info.cpi_context)
+            .ok_or("Expected CPI context for PDA inputs but found none")?;
         let system_config = SystemAccountMetaConfig::new_with_cpi_context(
             *program_id,
-            cpi_context_of_first_input,
+            cpi_context,
         );

286-358: Clarify or remove the unused accounts_to_compress parameter

The accounts_to_compress parameter is checked for length equality (line 296) but never actually used. This either indicates incomplete validation or a vestigial parameter.

Either remove the parameter and length check if not needed, or add meaningful validation (e.g., verify account ownership/lamports match expectations).

sdk-libs/sdk/src/compressible/compress_runtime.rs (2)

49-54: Replace hard-coded ProgramError::Custom(0) with LightSdkError::ConstraintViolation

The validation checks return opaque Custom(0) errors, making debugging difficult and potentially colliding with other uses of error code 0. The codebase already has LightSdkError::ConstraintViolation for exactly this purpose (mapped to error code 16001), and config.rs uses this pattern consistently for identical validation checks.

Apply this diff:

-    if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
-        return Err(ProgramError::Custom(0));
-    }
-    if *ctx.compression_authority().key != compression_config.compression_authority {
-        return Err(ProgramError::Custom(0));
-    }
+    if *ctx.rent_sponsor().key != compression_config.rent_sponsor
+        || *ctx.compression_authority().key != compression_config.compression_authority
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

66-71: Add bounds checks to prevent panics from malformed account arrays

Two unguarded slice operations will panic with attacker-controlled inputs:

  • Line 67: &cpi_accounts.to_account_infos()[system_accounts_start..] panics if system_accounts_start exceeds the list length
  • Lines 70-71: all_post_system.len() - compressed_accounts.len() underflows if compressed_accounts is longer, then the slice panics

Since remaining_accounts is external CPI input, these invariants aren't guaranteed. Add bounds checks that return LightSdkError::ConstraintViolation before slicing.

Apply this diff:

     let system_accounts_start = cpi_accounts.system_accounts_end_offset();
     let all_post_system = &cpi_accounts.to_account_infos()[system_accounts_start..];
 
+    if compressed_accounts.len() > all_post_system.len() {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }
     let pda_start_in_all_accounts = all_post_system.len() - compressed_accounts.len();
     let solana_accounts = &all_post_system[pda_start_in_all_accounts..];
sdk-libs/sdk/src/compressible/compression_info.rs (3)

330-362: System Program ID is still hardcoded as zeros—CPI will fail

transfer_lamports_cpi uses a zeroed array as the System Program ID (lines 340-343), which doesn't match the real System Program ID (11111111111111111111111111111111). Every call to top_up_rent will fail when attempting the CPI.

You already receive system_program: &AccountInfo as a parameter—use its key directly.

Apply this diff:

-    // System Program ID
-    const SYSTEM_PROGRAM_ID: [u8; 32] = [
-        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-        0, 0,
-    ];
-
     // System Program Transfer instruction discriminator: 2 (u32 little-endian)
     let mut instruction_data = vec![2, 0, 0, 0];
     instruction_data.extend_from_slice(&lamports.to_le_bytes());
 
     let transfer_instruction = Instruction {
-        program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
+        program_id: *system_program.key,
         accounts: vec![
             AccountMeta::new(*from.key, true),
             AccountMeta::new(*to.key, false),
         ],
         data: instruction_data,
     };

254-320: Return semantics don't match the documentation

The doc comment promises None for "nothing to claim," but line 319 returns Ok(Some(0)) in that case. This makes it impossible to distinguish "no claim performed" from "successful zero-amount claim."

Apply this diff:

     if let Some(amount) = claimable {
         if amount > 0 {
             // ... transfer logic ...
             return Ok(Some(amount));
         }
     }
-    Ok(Some(0))
+    Ok(None)

174-175: Guard against division by zero in rent calculation

If rent_curve_per_epoch returns 0 (due to misconfiguration), line 175 will panic. Add a guard to handle this gracefully.

Apply this diff:

     let rent_per_epoch = self.rent_config.rent_curve_per_epoch(num_bytes);
+    if rent_per_epoch == 0 {
+        return 0;
+    }
     let epochs_funded_ahead = available_balance / rent_per_epoch;
sdk-libs/sdk/Cargo.toml (1)

58-59: Remove unused light-ctoken-types dependency

light-compressible is actively used (imports in compress_account.rs, compression_info.rs, and config.rs), but light-ctoken-types has no direct imports in the SDK source. The program-test crate gates it behind a devenv feature, which is the more appropriate pattern.

Remove the unconditional light-ctoken-types dependency or make it optional and feature-gated like in program-test/Cargo.toml.

sdk-libs/sdk-types/src/lib.rs (1)

22-28: Consider adding Default for ergonomic placeholder construction

If RentSponsor is ever constructed with zeroed values as a placeholder, adding #[derive(Default)] would enable RentSponsor::default() for cleaner initialization.

Apply this diff if useful:

-#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
 pub struct RentSponsor {
sdk-libs/macros/src/compressible/GUIDE.md (1)

1-1: Fix markdown heading level to satisfy markdownlint

The file starts with a level-2 heading (##), but the configured style expects a top-level heading. Static analysis flags this as MD041.

Apply this diff:

-## Compressible macros — caller program usage (first draft)
+# Compressible macros — caller program usage (first draft)
sdk-libs/sdk/src/compressible/compress_account.rs (1)

54-60: Rent gating looks sound, but address_space and owner invariants still need hardening

The new rent-based gating via AccountRentState and Rent::get() is a good replacement for the old slot-delay check and the error logging is nicely contextual.

Two invariants remain fragile here:

  1. Potential panic on empty address_space

    let derived_c_pda = derive_address(
        &account_info.key.to_bytes(),
        &address_space[0].to_bytes(),
        &program_id.to_bytes(),
    );

    If address_space is ever empty (e.g., misconfigured macro use or a future call site outside the current flow), this will panic instead of returning a ProgramError. For an on-chain SDK helper, it’s safer to guard explicitly and fail gracefully.

  2. Owner program ID vs derivation program ID

    let owner_program_id = cpi_accounts.self_program_id();
    let mut compressed_account =
        LightAccount::<A::Output>::new_empty(&owner_program_id, &meta_with_address)?;

    You derive the PDA with program_id but use owner_program_id to own the compressed account. If these ever differ (misconfigured CpiAccounts), you end up with an account whose owner doesn’t match the program that derived its address.

Consider tightening both points along these lines:

-    let derived_c_pda = derive_address(
-        &account_info.key.to_bytes(),
-        &address_space[0].to_bytes(),
-        &program_id.to_bytes(),
-    );
+    let address_tree = address_space.first().ok_or_else(|| {
+        msg!("prepare_account_for_compression requires a non-empty address_space");
+        LightSdkError::ConstraintViolation
+    })?;
+
+    let derived_c_pda = derive_address(
+        &account_info.key.to_bytes(),
+        &address_tree.to_bytes(),
+        &program_id.to_bytes(),
+    );
@@
-    let owner_program_id = cpi_accounts.self_program_id();
+    let owner_program_id = cpi_accounts.self_program_id();
+    if owner_program_id != *program_id {
+        msg!(
+            "prepare_account_for_compression: owner_program_id mismatch (expected {}, got {})",
+            program_id,
+            owner_program_id
+        );
+        return Err(LightSdkError::ConstraintViolation.into());
+    }

to enforce the intended invariants and avoid panics.

Also applies to: 68-100, 114-118

sdk-libs/macros/src/rent_sponsor.rs (1)

5-64: Harden macro argument handling and version parsing for rent-sponsor PDAs

For macros that derive PDAs controlling rent flows, the current argument handling is too forgiving:

  1. Extra arguments silently ignored

    Args::parse stores only elems[0] and elems[1]; any additional arguments are dropped. That makes call-site mistakes hard to diagnose. Add an explicit length check:

    let elems = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
    if elems.is_empty() {
        return Err(syn::Error::new(
            input.span(),
            "Expected at least a program id string literal",
        ));
    }
+   if elems.len() > 2 {
+       return Err(syn::Error::new(
+           input.span(),
+           "Expected at most 2 arguments: program id string and optional u16 version",
+       ));
+   }
  1. Invalid version literals quietly fall back to 1

    Both macros currently do:

let version_u16: u16 = match args.version.as_ref() {
    Some(lit) => lit.base10_parse::<u16>().unwrap_or(1u16),
    None => 1u16,
};

If the literal is out of range or malformed, parsing fails and you silently default to version 1, deriving the wrong PDA with no signal. For something as sensitive as rent sponsorship, that should be a compile error instead, e.g.:

-    let version_u16: u16 = match args.version.as_ref() {
-        Some(lit) => lit.base10_parse::<u16>().unwrap_or(1u16),
-        None => 1u16,
-    };
+    let version_u16: u16 = match args.version.as_ref() {
+        Some(lit) => match lit.base10_parse::<u16>() {
+            Ok(v) => v,
+            Err(e) => return e.to_compile_error().into(),
+        },
+        None => 1u16,
+    };
  1. Shared PDA derivation logic

    derive_light_rent_sponsor_pda and derive_light_rent_sponsor duplicate the program-id parse, seed construction, PDA derivation, and byte-literal conversion. Extracting a small internal helper that returns (program_id_bytes, pda_bytes, bump, version_u16) would keep the derivation logic single-sourced and easier to evolve.

These changes will make the macros fail fast and loudly on misconfiguration instead of silently deriving incorrect rent-sponsor addresses.

Also applies to: 76-110, 120-166

sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

108-111: Map Rent::get() failures to a ProgramError-backed variant instead of LightSdkError::Borsh

Rent::get() returns a ProgramError for sysvar access issues, but this code wraps failures as LightSdkError::Borsh, which is semantically misleading and inconsistent with other sysvar / CPI error handling paths:

let rent = Rent::get().map_err(|err| {
    msg!("Failed to get rent: {:?}", err);
    LightSdkError::Borsh
})?;

Prefer mapping to the ProgramError-backed variant (or using an existing From<ProgramError> impl), e.g.:

-    let rent = Rent::get().map_err(|err| {
-        msg!("Failed to get rent: {:?}", err);
-        LightSdkError::Borsh
-    })?;
+    let rent = Rent::get().map_err(|err| {
+        msg!("Failed to get rent: {:?}", err);
+        LightSdkError::ProgramError(err)
+    })?;
+    // or, if LightSdkError: From<ProgramError> is implemented:
+    // let rent = Rent::get().map_err(LightSdkError::from)?;

This keeps sysvar failures clearly distinguished from Borsh serialization issues.

sdk-libs/macros/src/compressible/decompress_context.rs (2)

40-52: Add defensive bounds check between compressed_accounts and solana_accounts to avoid on-chain panics

collect_pda_and_token iterates compressed_accounts and uses the loop index i to index solana_accounts[i] in the generated match arms, but there is no guarantee these slices stay in lockstep. If they ever diverge (macro misuse, future refactor), the generated code will panic on-chain with an out-of-bounds access instead of returning a ProgramError.

Consider adding an explicit bounds check in the generated impl before indexing, e.g.:

-                for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
+                if compressed_accounts.len() != solana_accounts.len() {
+                    return std::result::Result::Err(
+                        solana_program_error::ProgramError::InvalidAccountData,
+                    );
+                }
+
+                for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
                     let meta = compressed_data.meta;
                     match compressed_data.data {
                         #(#pda_match_arms)*

This keeps the macro-generated code robust against mismatched inputs and avoids panicking in the PDA handler path.

Also applies to: 106-147


187-225: Tighten lifetime and attribute parsing for more predictable macro failures (optional)

The derive currently grabs input.generics.lifetimes().next() and uses bare Idents for #[pda_types(..)] / #[token_variant(..)]. This works today but can fail in surprising ways if additional lifetimes or qualified paths are introduced.

As a non-blocking improvement, consider:

  • Checking that there is exactly one lifetime and erroring if more are present.
  • Future-proofing attributes by parsing syn::Path instead of Ident for PDA types / token variants, so qualified paths (e.g., my_mod::MyPda) are accepted without redesign later.

These changes would make misuse fail earlier with clearer compiler errors.

sdk-libs/program-test/src/compressible.rs (1)

211-235: Use cfg.compression_authority instead of forcing compression_authority = payer

auto_compress_program_pdas deserializes CpdaCompressibleConfig but then ignores the on-chain compression_authority and hard-codes it to payer.pubkey():

let rent_sponsor = cfg.rent_sponsor;
// TODO: add coverage for external compression_authority
let compression_authority = payer.pubkey();

If a program sets a real external compression authority, this helper will construct compress instructions with the wrong authority account; authorization checks will fail and, because try_compress_chunk swallows errors, the whole function silently becomes a no-op.

Prefer honoring the config:

-    let rent_sponsor = cfg.rent_sponsor;
-    // TODO: add coverage for external compression_authority
-    let compression_authority = payer.pubkey();
+    let rent_sponsor = cfg.rent_sponsor;
+    let compression_authority = cfg.compression_authority;

Tests can still choose to configure compression_authority == payer when convenient, but the helper will also work correctly once you add coverage for distinct authorities.

sdk-libs/macros/src/compressible/utils.rs (1)

41-76: Fix is_copy_type for arrays: only mark [T; N] as Copy when T is Copy

is_copy_type currently treats all arrays as Copy:

match ty {
    Type::Path(type_path) => { /* ... */ }
    Type::Array(_) => true,
    _ => false,
}

In Rust, [T; N] is Copy only when T: Copy. For arrays of non-Copy types (e.g., [String; 4]), this will misclassify the field and cause generated code to attempt by-value copies where a .clone() is required, leading to confusing compile errors at macro expansion sites.

Reuse your existing heuristic recursively on the element type:

-        Type::Array(_) => true,
+        Type::Array(array) => {
+            // Arrays are Copy if their element type is Copy.
+            is_copy_type(&array.elem)
+        }

This keeps the check cheap but aligned with Rust’s Copy semantics.

sdk-libs/macros/src/compressible/traits.rs (1)

86-129: Treat compression_info as a reserved field: avoid duplicate initialization and double‑counting in generated impls

Right now the macro both:

  • Hardcodes compression_info: None in the CompressAs struct literal, and
  • Includes compression_info in the generic per-field loops:
fn generate_compress_as_impl(...) -> TokenStream {
    quote! {
        impl light_sdk::compressible::CompressAs for #struct_name {
            type Output = Self;

            fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> {
                std::borrow::Cow::Owned(Self {
                    compression_info: None,
                    #(#field_assignments)*
                })
            }
        }
    }
}

generate_compress_as_field_assignments and generate_size_fields only skip fields with #[skip], so for any struct that does have a compression_info field (which validate_compression_info_field requires for derive_compressible), the macro will:

  • Emit two initializers for compression_info (compile error), and
  • Count its serialized length again in size(), on top of compression_info_size.

You can fix this by treating compression_info as a reserved field handled solely by the dedicated logic:

fn generate_compress_as_field_assignments(
    fields: &Punctuated<Field, Token![,]>,
    compress_as_fields: &Option<CompressAsFields>,
) -> Vec<TokenStream> {
    let mut field_assignments = Vec::new();

    for field in fields {
        let field_name = field.ident.as_ref().unwrap();
        let field_type = &field.ty;

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is handled explicitly in `generate_compress_as_impl`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
        // ...
    }
}
fn generate_size_fields(fields: &Punctuated<Field, Token![,]>) -> Vec<TokenStream> {
    let mut size_fields = Vec::new();

    for field in fields.iter() {
        let field_name = field.ident.as_ref().unwrap();

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is already accounted for via `compression_info_size`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }

        size_fields.push(quote! {
            + self.#field_name.try_to_vec().expect("Failed to serialize").len()
        });
    }

    size_fields
}

Optionally, you can also have derive_compress_as call validate_compression_info_field so standalone #[derive(CompressAs)] gets the same clear diagnostics as derive_compressible when the field is missing.

As-is, any struct using derive_compressible will fail to compile once this macro expands.

Also applies to: 150-181, 192-209, 219-255

sdk-libs/macros/src/lib.rs (3)

226-261: DataHasher derive is documented as alias of LightHasher but still calls SHA variant

The docs for DataHasher say:

/// Alias of `LightHasher`.
#[proc_macro_derive(DataHasher, attributes(skip, hash))]

but the implementation delegates to derive_light_hasher_sha:

pub fn data_hasher(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemStruct);
    into_token_stream(derive_light_hasher_sha(input))
}

This makes DataHasher effectively an alias of LightHasherSha, not LightHasher. That contradicts the documentation and can be surprising for callers relying on the default (Poseidon vs SHA) behavior.

Either:

  • Change the implementation to use derive_light_hasher(input) so it truly aliases LightHasher, or
  • Update the docstring to clearly state that DataHasher is an alias of the SHA variant and document the intended use case.
#!/bin/bash
# Show the LightHasher vs LightHasherSha implementations for clarity.
rg -n "derive_light_hasher" -S sdk-libs/macros/src/hasher.rs

263-295: HasCompressionInfo derive does not whitelist #[skip] attribute as docs require

The new HasCompressionInfo derive is declared as:

#[proc_macro_derive(HasCompressionInfo)]
pub fn has_compression_info(input: TokenStream) -> TokenStream { ... }

but the example immediately above shows:

#[derive(HasCompressionInfo)]
pub struct UserRecord {
    #[skip]
    pub compression_info: Option<CompressionInfo>,
    // ...
}

Without attributes(skip) on the macro declaration, #[skip] will cause a compile error (“cannot find attribute skip in this scope”) even though it’s part of the documented pattern.

To align behavior and docs, change the declaration to:

#[proc_macro_derive(HasCompressionInfo, attributes(skip))]

so that user code can mark compression_info as skipped without breaking compilation.


374-397: Large commented-out add_native_compressible_instructions block should be removed or tracked

There is still a substantial commented‑out implementation of add_native_compressible_instructions and its docs. As with the earlier LightDiscriminatorSha block, this dead code:

  • Adds noise and confusion about feature status.
  • Risks becoming stale if APIs drift.

If this API is truly deprecated, it would be cleaner to delete the commented block. If you plan to reintroduce it, replace the large comment with a concise // TODO(#issue-id): Re‑enable add_native_compressible_instructions and track the work in an issue.

sdk-libs/macros/src/compressible/seed_providers.rs (2)

193-348: Authority seed handling duplicates the same “standard field” logic and shares the same Option bug

The authority‑seed path mirrors the token‑seed expression handling, including the same is_standard_field list and .key() vs .as_ref() split. That means the ctoken_* Option accounts are again treated as non‑optional, leading to invalid .key() access and inconsistent behavior between token and authority derivation.

Refactor the authority branch to reuse a single helper that correctly distinguishes:

  • Non‑optional standard accounts (fee_payer, rent_sponsor, config, …) → direct .key()
  • Optional seed accounts (ctoken_*, dynamically generated seed accounts) → as_ref().ok_or(...)? .key()

This will both fix the compile‑time issue and prevent the two branches from drifting.


516-897: Avoid local is_pubkey_type duplication; reuse the shared helper

This file defines its own is_pubkey_type(ty: &syn::Type) again, but compressible::utils already exposes a canonical helper for this. Keeping two subtly different implementations makes it harder to reason about how data.* fields are mapped into client parameters and increases drift risk.

Prefer:

  • Exporting a single is_pubkey_type from compressible::utils, and
  • Importing and using it here in both analyze_seed_spec_for_client and analyze_seed_spec_for_client_expr.

That keeps type inference consistent across macro entrypoints and reduces maintenance overhead.

#!/bin/bash
# Confirm there is an existing utility helper and identify callers.
rg -n "fn is_pubkey_type" sdk-libs/macros/src/compressible -C3
sdk-libs/macros/src/compressible/instructions.rs (6)

53-141: Parser still mishandles seeds starting with ctx.* / data.* and similar identifier expressions

The TokenSeedSpec parser continues to special‑case the first identifier inside (...):

if content.peek(Ident) {
    let first_ident: Ident = content.parse()?;
    match first_ident.to_string().as_str() {
        "is_token" => { ... }
        "true" => { ... }
        "is_pda" | "false" => { ... }
        _ => {
            // treat first_ident as a SeedElement::Expression, then optionally parse more
        }
    }
}

For seed specs like:

MyPda = (ctx.accounts.user, data.bump)
MyPda = (data.index)

this consumes only the leading ctx / data identifier, dropping .accounts.user / .index. The resulting seed becomes an invalid SeedElement::Expression(Box::new(syn::Expr::Path(ctx))), and the rest of the expression is silently ignored.

The earlier suggestion to only special‑case control keywords and otherwise delegate to parse_seeds_with_authority is still applicable:

  • Use a fork/peek to check whether the first ident is is_token, is_ata, true, is_pda, or false.
  • If it’s not a control keyword, do not consume it; instead hand the entire content to parse_seeds_with_authority.

Without this change, legitimate seeds starting with identifier expressions will generate broken code that either fails to compile or references non‑existent symbols.


402-417: InstructionVariant allows PDA‑only / token‑only but downstream code still assumes they’re unreachable

instruction_variant currently permits all three combinations:

let instruction_variant = match (has_pda_seeds, has_token_seeds) {
    (true, true) => InstructionVariant::Mixed,
    (true, false) => InstructionVariant::PdaOnly,
    (false, true) => InstructionVariant::TokenOnly,
    (false, false) => { /* error */ }
};

However, multiple downstream generators still treat PdaOnly and TokenOnly as impossible via unreachable!():

  • generate_decompress_accounts_struct (Lines 1441–1488)
  • generate_error_codes (Lines 1550–1553)
  • compress_accounts struct creation (Lines 710–728)

This means that if a user legitimately supplies only PDA seeds or only token seeds, macro expansion will panic at compile time with an unreachable!() backtrace instead of a targeted error message.

Either:

  • Reject PDA‑only / token‑only upfront in this match with a clear macro_error! explaining the constraint, or
  • Fully implement those variants and remove the unreachable!()s.

In its current form, the API surface advertises support for PdaOnly/TokenOnly, but the implementation does not.


424-461: Compile‑time bug: comparing syn::Ident to String in pda_seed_provider_impls

In the seed provider implementation generation, the lookup still does:

let name_str = name.to_string();
let spec = if let Some(ref pda_seed_specs) = pda_seeds {
    pda_seed_specs
        .iter()
        .find(|s| s.variant == name_str)
        .ok_or_else(|| macro_error!(...))?
} else { ... };

But s.variant is a syn::Ident, and name_str is a String. Ident does not implement PartialEq<String>, so this code does not compile.

You likely want:

.find(|s| s.variant == *name)

or, if you need string normalization:

.find(|s| s.variant.to_string() == name_str)

Until this is fixed, add_compressible_instructions! cannot be used at all because the macros crate won’t build.


985-1071: Rent distribution logic is sound, but overflow maps to an opaque custom(0) error

The rent close distribution logic:

  • Loads Clock and Rent sysvars,
  • Computes AccountRentState and calls calculate_close_distribution,
  • Uses checked_sub and checked_add to move lamports to the fee payer and rent sponsor.

The structure looks correct and defensive, but both checked_add failures map to ProgramError::Custom(0):

**dst = dst.checked_add(dist.to_user)
    .ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;
...
**dst = dst.checked_add(dist.to_rent_sponsor)
    .ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;

This collapses a rare but distinct overflow condition into a generic, possibly conflicting error code. Consider adding a dedicated variant to CompressibleInstructionError (e.g., OverflowDuringRentDistribution) and using that instead, so callers and logs can disambiguate this from other failures.

#!/bin/bash
# Show the CompressibleInstructionError enum to confirm an overflow variant doesn't already exist.
rg -n "enum CompressibleInstructionError" -n sdk-libs/macros/src/compressible/instructions.rs -C5

1182-1302: Generated PDA seed derivation still uses .key field and unwrap() on optional accounts

generate_pda_seed_derivation_for_trait’s map_pda_expr_to_params still rewrites:

  • ctx.accounts.foo as *accounts.foo.as_ref().unwrap().key
  • ctx.foo as *accounts.foo.as_ref().unwrap().key

Issues:

  1. Invalid accessor for most account wrapper types

    • Signer<'info> and UncheckedAccount<'info> don’t expose a public .key field; they implement the Key trait and provide a .key() method (or deref into AccountInfo).
    • The generated *accounts.foo.as_ref().unwrap().key will not compile for these types.

    For non‑optional accounts, you likely want:

    let k = accounts.foo.key();

    and for optional seed accounts:

    let k = accounts.foo
        .as_ref()
        .ok_or(/* CompressibleInstructionError::MissingSeedAccount */)?
        .key();

    and then use k.as_ref() when building seeds.

  2. Unchecked unwrap() on optional accounts

    Everywhere else (e.g., in generate_ctoken_seed_provider_implementation) you return a structured CompressibleInstructionError::MissingSeedAccount instead of panicking. Here, a missing optional seed account will cause a hard panic in the program.

    This should be aligned with the rest of the code by replacing .as_ref().unwrap() with .as_ref().ok_or_else(/* MissingSeedAccount */)?.

  3. Comment/code mismatch

    The comment says “(unwrap if optional)” but the correct behavior for this macro family has moved toward returning a specific error, not panicking.

I strongly recommend mirroring the error‑handling and accessor patterns you already use in the CToken seed provider generator so that PDA seed derivation is both safe and consistent.

#!/bin/bash
# Quickly list account wrapper types to confirm their APIs (key()/Key trait).
rg -n "struct UncheckedAccount" -S ~/.cargo/registry 2>/dev/null | head -5 || true

1425-1519: Decompress accounts struct doc and variant handling are out of sync with runtime behavior

In generate_decompress_accounts_struct:

  • InstructionVariant::PdaOnly and TokenOnly arms still call unreachable!(), even though these variants are constructed earlier, so a PDA‑only or token‑only configuration will cause a macro panic instead of a friendly error. (Same concern as the earlier instruction_variant match.)

  • The rent_sponsor field is documented as:

    /// CHECK: anyone can pay
    #[account(mut)]
    pub rent_sponsor: UncheckedAccount<'info>

    but the runtime path in compress_runtime and decompress_runtime treats rent_sponsor as a specific PDA configured in CompressibleConfig, not an arbitrary payer. This comment is now misleading.

Suggestions:

  • Either disallow PdaOnly/TokenOnly at argument‑parsing time, or implement them and remove the unreachable!() calls.
  • Update the rent_sponsor doc to describe it as “the program’s configured rent sponsor PDA; checked against CompressibleConfig at runtime” (or similar).
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)

147-218: PDA derivation mismatch logs but does not abort the operation

handle_packed_pda_variant checks derived_pda vs the on‑chain account key:

let (seeds_vec, derived_pda) = /* ... */;
if derived_pda != *solana_account.key {
    msg!(/* mismatch diagnostics */);
}
// then continues into prepare_account_for_decompression_idempotent

For safety and diagnosability, this should be a hard error, not just a log:

  • If the caller wires the wrong account for the given seeds, continuing to “prepare and decompress” risks mutating an unintended account or at least hides a serious configuration bug.
  • The seeds and PDA relationship is a core integrity check; failing it should abort the instruction.

Consider returning Err(ProgramError::InvalidArgument) (or a dedicated constraint error) immediately after logging, rather than proceeding.

#!/bin/bash
# Search for other PDA mismatch handling to ensure consistency (e.g. compress side).
rg -n "Derived PDA does not match" -S

223-263: Missing bounds check on account slicing can underflow and panic

In process_decompress_accounts_idempotent:

let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
let solana_accounts = &remaining_accounts[pda_accounts_start..];

If compressed_accounts.len() > remaining_accounts.len(), this subtraction underflows and the slice will panic (or exhibit undefined behavior in optimized builds).

Even if macro wiring “should” keep these in sync, this is user‑facing runtime logic and should defend itself. Suggested fix:

if compressed_accounts.len() > remaining_accounts.len() {
    return Err(ProgramError::NotEnoughAccountKeys);
}
let pda_accounts_start = remaining_accounts.len() - compressed_accounts.len();
let solana_accounts = &remaining_accounts[pda_accounts_start..];

This makes mis‑wired calls fail cleanly instead of panicking.


236-245: Decompress path still does not validate rent_sponsor against CompressibleConfig

After loading the config:

let compression_config =
    crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
let address_space = compression_config.address_space[0];

the compress side enforces that ctx.rent_sponsor().key equals compression_config.rent_sponsor, but the decompress side does not. That means a program could decompress using a different rent_sponsor than the one configured on-chain, violating the intended invariant.

To align behavior and tighten safety, add:

if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
    return Err(ProgramError::Custom(/* appropriate code */));
}

(possibly mapping to a dedicated InvalidRentSponsor or similar error code).

#!/bin/bash
# Show compress_runtime rent_sponsor check for reference.
rg -n "InvalidRentSponsor" sdk-libs -C3

308-314: Track or resolve the // TODO: fix this in non‑CPI context branch

The non‑cpi-context has_pdas path is still guarded by a bare // TODO: fix this:

// TODO: fix this
#[cfg(not(feature = "cpi-context"))]
if has_pdas {
    LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof)
        .with_account_infos(&compressed_pda_infos)
        .invoke(cpi_accounts.clone())?;
}

Given this is core PDA decompression logic, it would be good to either:

  • Clarify what’s wrong and how it differs from the cpi-context implementation, or
  • Open a tracking issue and reference it in the comment.

Otherwise it’s hard for readers to know whether this path is safe to rely on.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3274f97 and 2f30f02.

⛔ Files ignored due to path filters (48)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • scripts/lint.sh is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/processor.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/variant.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/user_record_tests.rs is excluded by none and included by none
📒 Files selected for processing (35)
  • sdk-libs/compressed-token-sdk/src/decompress_runtime.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressible-client/src/lib.rs (9 hunks)
  • sdk-libs/macros/src/compressible/GUIDE.md (1 hunks)
  • sdk-libs/macros/src/compressible/README.md (1 hunks)
  • sdk-libs/macros/src/compressible/decompress_context.rs (1 hunks)
  • sdk-libs/macros/src/compressible/instructions.rs (1 hunks)
  • sdk-libs/macros/src/compressible/mod.rs (1 hunks)
  • sdk-libs/macros/src/compressible/pack_unpack.rs (1 hunks)
  • sdk-libs/macros/src/compressible/seed_providers.rs (1 hunks)
  • sdk-libs/macros/src/compressible/traits.rs (1 hunks)
  • sdk-libs/macros/src/compressible/utils.rs (1 hunks)
  • sdk-libs/macros/src/compressible/variant_enum.rs (1 hunks)
  • sdk-libs/macros/src/cpi_signer.rs (1 hunks)
  • sdk-libs/macros/src/lib.rs (7 hunks)
  • sdk-libs/macros/src/rent_sponsor.rs (1 hunks)
  • sdk-libs/macros/src/utils.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (1 hunks)
  • sdk-libs/program-test/src/compressible.rs (5 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (3 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
  • sdk-libs/sdk-types/src/lib.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (4 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (12 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (7 hunks)
  • sdk-libs/sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (2 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-14T00:36:53.191Z
Learnt from: ananas-block
Repo: Lightprotocol/light-protocol PR: 1909
File: program-libs/zero-copy/src/init_mut.rs:241-249
Timestamp: 2025-08-14T00:36:53.191Z
Learning: In the light-protocol zero-copy crate, performance is prioritized over safety checks for edge cases like Vec lengths exceeding u32::MAX, even when there might be wire format inconsistencies.

Applied to files:

  • sdk-libs/compressed-token-sdk/src/decompress_runtime.rs
  • sdk-libs/macros/src/compressible/variant_enum.rs
🪛 markdownlint-cli2 (0.18.1)
sdk-libs/macros/src/compressible/GUIDE.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


31-31: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


43-43: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


53-53: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


75-75: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


80-80: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


138-138: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


157-157: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


173-173: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)

}
```

2. Generate compress/decompress instructions with auto seeds
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 | 🟡 Minor

Normalize ordered list numbering to "1/1/1" style

The ordered lists use sequential numbering (2., 3., 4.), but the repo's markdownlint configuration expects all items to be prefixed with 1.. This is flagged as MD029 by static analysis.

Change all list items to use 1. while preserving content:

-2. Generate compress/decompress instructions with auto seeds
+1. Generate compress/decompress instructions with auto seeds

-3. Initialize your compression config (one-time)
+1. Initialize your compression config (one-time)

Apply similar changes to all affected lines.

Also applies to: 43-43, 53-53, 75-75, 80-80, 138-138, 157-157, 173-173

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

31-31: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/GUIDE.md around line 31 (and also apply to
lines 43, 53, 75, 80, 138, 157, 173), update the ordered list numbering from
sequential numbers (e.g., 2., 3., 4.) to the normalized "1." style required by
markdownlint MD029; leave the item text unchanged and only replace the numeric
prefixes so every ordered list item begins with "1." to comply with the repo
linting rules.

Comment on lines +48 to +60
let pack_field_assignments = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;

if *field_name == "compression_info" {
quote! { #field_name: None }
} else if is_pubkey_type(field_type) {
quote! { #field_name: remaining_accounts.insert_or_get(self.#field_name) }
} else if is_copy_type(field_type) {
quote! { #field_name: self.#field_name }
} else {
quote! { #field_name: self.#field_name.clone() }
}
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

Guard against out-of-bounds indexing on remaining_accounts in generated Unpack

In the packed path, the generated Unpack impl reconstructs Pubkey fields via:

#field_name: *remaining_accounts[self.#field_name as usize].key

If self.#field_name ever points past remaining_accounts.len(), this will panic rather than returning a ProgramError. Since the accounts slice is ultimately instruction-controlled, this is a fragile assumption for on-chain code.

Consider generating a bounds-checked version instead, e.g.:

-        } else if is_pubkey_type(field_type) {
-            quote! {
-                #field_name: *remaining_accounts[self.#field_name as usize].key
-            }
+        } else if is_pubkey_type(field_type) {
+            quote! {
+                #field_name: {
+                    let idx = self.#field_name as usize;
+                    let account_info = remaining_accounts
+                        .get(idx)
+                        .ok_or(anchor_lang::prelude::ProgramError::NotEnoughAccountKeys)?;
+                    *account_info.key
+                }
+            }
         }

so malformed inputs fail gracefully with a clear error instead of aborting the program.

Also applies to: 101-131


I'll investigate the actual implementation to verify the bounds-checking concern. Let me examine the file and understand the context.


🏁 Script executed:

#!/bin/bash
# First, check if the file exists and examine the specific line ranges
echo "=== Examining lines 40-70 (context for lines 48-60) ==="
sed -n '40,70p' sdk-libs/macros/src/compressible/pack_unpack.rs

echo -e "\n=== Examining lines 93-140 (context for lines 101-131) ==="
sed -n '93,140p' sdk-libs/macros/src/compressible/pack_unpack.rs

Length of output: 2762


🏁 Script executed:

#!/bin/bash
# Search for how remaining_accounts indexing is handled in the generated code
echo "=== Searching for remaining_accounts indexing patterns ==="
rg -n 'remaining_accounts\[' sdk-libs/macros/src/compressible/pack_unpack.rs -A2 -B2

echo -e "\n=== Searching for bounds checks or safety patterns ==="
rg -n '\.get\(|ok_or|bounds|checked' sdk-libs/macros/src/compressible/pack_unpack.rs -C2

Length of output: 341


🏁 Script executed:

#!/bin/bash
# Check for tests or documentation about error handling
echo "=== Looking for test files ==="
fd 'test|spec' sdk-libs/macros/src/compressible/ --type f

echo -e "\n=== Checking for error handling patterns in compressible module ==="
rg -n 'ProgramError|Error|error' sdk-libs/macros/src/compressible/pack_unpack.rs | head -20

Length of output: 383


I found the vulnerability. Let me examine the insert_or_get method and broader context to understand if there's upstream validation:


🏁 Script executed:

#!/bin/bash
# Find insert_or_get to understand Pack path
echo "=== Searching for insert_or_get definition ==="
rg -n 'insert_or_get|PackedAccounts' sdk-libs/macros/src/compressible/ -A5

echo -e "\n=== Check light_sdk module for PackedAccounts ==="
fd 'light_sdk|instruction' --type f | head -20

Length of output: 4188


🏁 Script executed:

#!/bin/bash
# Check how the Unpack trait is used in the codebase
echo "=== Searching for Unpack trait usage ==="
rg -n 'trait Unpack|\.unpack\(' --type rs -C3 | head -40

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Look for validation patterns before calling unpack
echo "=== Searching for account validation patterns ==="
rg -n 'len\(|validate|check.*account' sdk-libs/macros/src/compressible/ -B2 -A2

Length of output: 8693


Guard against out-of-bounds indexing on remaining_accounts in generated Unpack

The generated Unpack impl at line 109 reconstructs Pubkey fields via direct array indexing:

#field_name: *remaining_accounts[self.#field_name as usize].key

If self.#field_name indexes past the bounds of remaining_accounts, this panics instead of returning a ProgramError. Since the function signature is Result<Self::Unpacked, anchor_lang::prelude::ProgramError>, callers expect graceful error handling, not program abort.

On-chain, this is a critical vulnerability—a malformed or truncated account slice passed by an attacker will cause the instruction to abort rather than return an error.

Implement bounds checking using .get() and ok_or():

         } else if is_pubkey_type(field_type) {
             quote! {
-                #field_name: *remaining_accounts[self.#field_name as usize].key
+                #field_name: {
+                    let idx = self.#field_name as usize;
+                    let account_info = remaining_accounts
+                        .get(idx)
+                        .ok_or(anchor_lang::prelude::ProgramError::NotEnoughAccountKeys)?;
+                    *account_info.key
+                }
             }
         }
🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/pack_unpack.rs around lines 48 to 60, the
generator currently emits direct indexing into remaining_accounts for Pubkey
fields which can panic if the index is out of bounds; change the emitted code so
it uses remaining_accounts.get(index) and converts a None into an Err returned
from Unpack (use .get(...).map(...).ok_or(...) to produce a ProgramError
indicating a missing/out-of-range account), thereby avoiding panics and
returning a proper ProgramError on malformed/truncated account slices.

Comment on lines +181 to +187
let (seeds_vec, derived_pda) = if let Some(params) = seed_params {
data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)
} else {
// For implementations without seed params, create a default one
let default_params = S::default();
data.derive_pda_seeds_with_accounts(program_id, seed_accounts, &default_params)
};
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 | 🟡 Minor

Defaulting SeedParams on None can mask missing seed data

When seed_params is None, handle_packed_pda_variant does:

let (seeds_vec, derived_pda) = if let Some(params) = seed_params {
    data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)
} else {
    let default_params = S::default();
    data.derive_pda_seeds_with_accounts(program_id, seed_accounts, &default_params)
};

This makes it impossible for implementations to distinguish “no seed data provided” from “all seed fields are default values”, which could lead to subtle bugs if a PDA truly requires non‑default data.* fields.

If the intent is to support PDAs that don’t use any SeedParams, consider:

  • Documenting that S: Default is for “no seed params used”, and/or
  • Returning an error when seed_params is None but S actually has required fields (e.g., via a marker trait or an explicit flag).

Not urgent, but worth clarifying to avoid surprises.

wip

add borsh_compat compressed_proof

add new_address_owner to instructiondata trait

add derive_compressed_address

remaining new_address_owner impl

add csdk-anchor-test program

lint add address_owner trait impl

add sdk libs - wip

add transfer_interface, transfer_ctoken, transfer_spl_to_ctoken, transfer_ctoken_to_spl, signed, instructions

rename consistently transfer_x

rename file transfer_decompressed to transfer_ctoken

add todos

add create_ctoken_account_signed, rename create_compressible_token_account to create_compressible_token_account_instruction

add create_associated_ctoken_account

add inline comment to copyLocalProgramBinaries.sh

remove new_address_owner

add pack and unpack for tokendata

first pass, compressible helpers for light-sdk

wip

compiles

lint

compressAccountsIdempotent csdk works, adjust test asserts to account is_none

ctoken add signer flags for decompress_full_ctoken_accounts_with_indices

wip

stash: removing ctoken from compression

all tests working

add auto-comp, clean up tests

rm dependency on patch

fmt

lint

lint

refactor

rm

wip

clean

fmt

clean

clean

clean

rm macro

clean

clean

dedupe derivation methods

clean

fmt

revert copyLocalProgramBinaries.sh diff

add csdk_anchor_test binary to ci

fix indexer

fix doctest

fix cli ci build target

fix cli build

clean

address nits

fix cli cache

fix cache

clean

fix csdk anchor test program build

add pkg.json csdk

rebuild

fix syntax

fix nx

rm panics

fix ci

fix build sdk-anchor-test

try fix

bmt keccak

spawn_prover fix

fix lint

fix clock sysvar

add test feature to account-compression

revert profiler

refactor csdk-anchor-test program lib.rs

split tests

fmt

revert cli script

reset close_for_compress_and_close to main

fmt

try revert

build account-compression with test flag

fmt

fix workflow to ensure we build account-compression with test feature

fix sdk test nav

try sdk-tests.yml with hyphen

rm idl build

csdk anchor test artifact

wip

reuse ctoken_types

move ctoken to light-compressed-token-sdk

clean

move pack to compressed-token-sdk

clean

clean

clean

clean

clean

clean

wip - add macro

lint

clean

clean

fmt clean, rename to sdk-compressible-test

cargo lock

default auto compress false

wip

patch to compile

wip

add borsh_compat compressed_proof

add new_address_owner to instructiondata trait

add derive_compressed_address

remaining new_address_owner impl

add csdk-anchor-test program

lint add address_owner trait impl

add sdk libs - wip

add transfer_interface, transfer_ctoken, transfer_spl_to_ctoken, transfer_ctoken_to_spl, signed, instructions

rename consistently transfer_x

rename file transfer_decompressed to transfer_ctoken

add todos

add create_ctoken_account_signed, rename create_compressible_token_account to create_compressible_token_account_instruction

add create_associated_ctoken_account

add inline comment to copyLocalProgramBinaries.sh

remove new_address_owner

add pack and unpack for tokendata

first pass, compressible helpers for light-sdk

wip

compiles

lint

compressAccountsIdempotent csdk works, adjust test asserts to account is_none

ctoken add signer flags for decompress_full_ctoken_accounts_with_indices

wip

stash: removing ctoken from compression

all tests working

add auto-comp, clean up tests

rm dependency on patch

fmt

lint

lint

refactor

rm

wip

clean

fmt

clean

clean

clean

rm macro

clean

clean

dedupe derivation methods

clean

fmt

revert copyLocalProgramBinaries.sh diff

add csdk_anchor_test binary to ci

fix indexer

fix doctest

fix cli ci build target

fix cli build

clean

address nits

fix cli cache

fix cache

clean

fix csdk anchor test program build

add pkg.json csdk

rebuild

fix syntax

fix nx

rm panics

fix ci

fix build sdk-anchor-test

try fix

bmt keccak

spawn_prover fix

fix lint

fix clock sysvar

add test feature to account-compression

revert profiler

refactor csdk-anchor-test program lib.rs

split tests

fmt

revert cli script

reset close_for_compress_and_close to main

fmt

try revert

build account-compression with test flag

fmt

fix workflow to ensure we build account-compression with test feature

fix sdk test nav

try sdk-tests.yml with hyphen

rm idl build

csdk anchor test artifact

wip

reuse ctoken_types

move ctoken to light-compressed-token-sdk

clean

move pack to compressed-token-sdk

clean

clean

clean

clean

clean

clean

wip - add macro

push macros

refactor compressible_instructions macro

split into compressible_instructions_decompress

modularized decompressaccountsidempotent

add decompresscontext derive macro

clean

macros done

compress runtime and clean

fmt

use small derive macros

wip

csdk anchor derived test using derive macros

lint

wip

clean

rm dead code

clean

lint

fmt

clean

fmt

dry

clean

lint

clean

rent cpi

wip

fmt and lint

clean

avoid reallocs in decompress_accounts_idempotent ixn builder

remove rent sponsor and compression authority

optional ctoken keys for decompress_accounts_idempotent

wip

auto compress pda

auto compress test

derive_rent_sponsor macro

add disable cold state mining flag

wip

add complex seed test

wip

clean

clean

ignore doctest

wip

revert to devnenv for lightprogramtest until we can remove it

clean

wip

address comments fixes

apply suggestion in decompress_runtime.rs

lint

wip

fix lint

fix macros lint

fix macro lint

add standard derive_rent_sponsor helper

remove unused ctoken-types dep from sdk

patch

rm unwrap from nested field access

address final comments
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
sdk-libs/compressible-client/src/lib.rs (1)

318-331: Potential panic on missing state trees.

Line 321 uses .unwrap() to access state_trees, which will panic if the validity proof doesn't contain state tree information. For consistency with the graceful error handling elsewhere (e.g., lines 300-302), convert this to an explicit error:

 for packed_tree_info in packed_tree_infos
     .state_trees
     .as_ref()
-    .unwrap()
+    .ok_or("Missing state trees in validity proof")?
     .packed_tree_infos
     .iter()
sdk-libs/sdk/src/utils.rs (1)

4-30: Runtime rent-sponsor PDA derivation looks correct

Using RENT_SPONSOR_SEED plus version.to_le_bytes() matches the documented seed scheme and the proc-macro implementation; Pubkey::find_program_address(seeds, program_id) is used correctly. If you later need signer seeds for invoke_signed, consider a sibling helper that also returns the &[&[u8]] seeds array (similar to get_light_cpi_signer_seeds) for ergonomics, but this function itself is solid.

sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

50-132: Sign the rent sponsor PDA when funding create_account.

invoke_create_account_with_heap debits rent_sponsor but never provides signer seeds for it. Because the rent sponsor is a PDA, create_account will immediately fail with MissingRequiredSignature—the helper signs only for the destination PDA. Thread the rent-sponsor seeds (or derive them here) and pass both signer sets to invoke_signed; otherwise decompression can never create the destination account when rent is supposed to come from the sponsor PDA. citeturn0files3

♻️ Duplicate comments (27)
sdk-libs/compressible-client/src/lib.rs (4)

80-164: Unresolved: Config PDA derivation mismatch between initialize and update.

A past review flagged that initialize_compression_config allows callers to pass a custom config_bump (defaulting to 0 at line 89), but update_compression_config always hardcodes 0 at line 138. If a non-zero bump is used during initialization, updates will target the wrong PDA.

Either add a config_bump parameter to update_compression_config or remove the Option<u8> from initialize_compression_config and document that only bump 0 is supported.


205-216: Critical: Potential panic when extracting CPI context from mixed inputs.

When both PDAs and tokens are present, line 207 unconditionally unwraps compressed_accounts[0].0.tree_info.cpi_context. If the first account happens to be a token (which typically has None for cpi_context), this will panic on the client side.

This is an unresolved issue from a previous review. Scan the accounts to find the first one with Some(cpi_context):

 if has_pdas && has_tokens {
-    let cpi_context_of_first_input =
-        compressed_accounts[0].0.tree_info.cpi_context.unwrap();
+    let cpi_context_of_first_input = compressed_accounts
+        .iter()
+        .find_map(|(acc, _)| acc.tree_info.cpi_context)
+        .ok_or("Expected CPI context for PDA inputs but found none")?;
     let system_config = SystemAccountMetaConfig::new_with_cpi_context(
         *program_id,
         cpi_context_of_first_input,
     );

288-298: Unused parameter creates API confusion.

The accounts_to_compress parameter is validated for length (line 296-297) but never actually used beyond that. A past review noted this makes the API misleading—callers might expect the function to validate account data or lamports, but it doesn't.

Consider either:

  • Removing the parameter and length check entirely if the Account data isn't needed, or
  • Using it meaningfully (e.g., validating owner/lamports match expectations).

229-259: Potential panic on malformed proof structure.

Line 232 uses .unwrap() on state_trees.as_ref(), which will panic if the proof doesn't contain state tree information. While this might be a structural invariant in well-formed proofs, failing gracefully would be safer:

-let packed_tree_infos_slice = &packed_tree_infos
-    .state_trees
-    .as_ref()
-    .unwrap()
-    .packed_tree_infos;
+let packed_tree_infos_slice = &packed_tree_infos
+    .state_trees
+    .as_ref()
+    .ok_or("Missing state trees in validity proof")?
+    .packed_tree_infos;

The explicit error handling in the loop (lines 240-249) is good, showing that graceful failures are preferred elsewhere in this function.

sdk-libs/sdk/src/compressible/compress_account.rs (1)

54-60: Rent-based gating looks good, but address_space[0] can still panic on empty slices

The new rent-function gating via AccountRentState::is_compressible is a nice improvement: it enforces rent‑based eligibility and logs detailed diagnostics (slot, lamports, bytes, rent exemption, last_claimed_slot, rent_config) before returning ConstraintViolation.

However, this function still directly indexes address_space[0]:

let derived_c_pda = derive_address(
    &account_info.key.to_bytes(),
    &address_space[0].to_bytes(),
    &program_id.to_bytes(),
);

If address_space is ever empty (e.g., a misconfigured caller outside the standard macro flow), this will panic rather than returning a ProgramError, which is undesirable for on‑chain code.

Please add a guard that fails gracefully, for example:

 use light_compressed_account::address::derive_address;

-    let derived_c_pda = derive_address(
-        &account_info.key.to_bytes(),
-        &address_space[0].to_bytes(),
-        &program_id.to_bytes(),
-    );
+    let address_tree = address_space.first().ok_or_else(|| {
+        msg!("prepare_account_for_compression requires a non-empty address_space");
+        LightSdkError::ConstraintViolation
+    })?;
+
+    let derived_c_pda = derive_address(
+        &account_info.key.to_bytes(),
+        &address_tree.to_bytes(),
+        &program_id.to_bytes(),
+    );

so misconfigured callers get a clear error instead of a hard abort.

(If you also want to enforce that cpi_accounts.self_program_id() matches program_id, this is a good place to assert that invariant as well.)

Also applies to: 68-85

sdk-libs/macros/src/compressible/GUIDE.md (1)

1-1: Fix markdown lint issues and re-check rent-flow description

Two small doc/consistency items here:

  1. Heading level (line 1)
    markdownlint expects the first line to be a top‑level heading. Change to:

    # Compressible macros — caller program usage (first draft)
  2. Ordered-list numbering (lines 15, 31, 43, 53, 75, 80, 138, 157, 173)
    The repo’s style is “1/1/1” for ordered lists. Update each list item to start with 1. instead of 2., 3., etc. (the rendered numbering will still be sequential).

  3. Rent semantics text (lines 118–121)
    The sentence:

    Funds on close/compress go to rent_sponsor (completed epochs) and refund fee payer for partial epochs automatically.

    should be double‑checked against the current rent model (especially now that program PDAs/cTokens route rent via a rent_sponsor PDA). If rent is now always captured by the sponsor for program PDAs, or if the refund behavior only applies in specific flows, please clarify that here so integrators don’t rely on outdated semantics.

Based on static_analysis_hints.

Also applies to: 15-15, 31-31, 43-43, 53-53, 75-75, 80-80, 118-121, 138-138, 157-157, 173-173

sdk-libs/macros/src/compressible/pack_unpack.rs (1)

101-131: Bounds-check remaining_accounts in generated Unpack to avoid panics

For Pubkey fields, the generated Unpack impl currently does:

#field_name: *remaining_accounts[self.#field_name as usize].key

If self.#field_name points past remaining_accounts.len(), this will panic instead of returning a ProgramError, which is a critical footgun for on‑chain code.

Please generate a bounds‑checked version, e.g.:

-        } else if is_pubkey_type(field_type) {
-            quote! {
-                #field_name: *remaining_accounts[self.#field_name as usize].key
-            }
+        } else if is_pubkey_type(field_type) {
+            quote! {
+                #field_name: {
+                    let idx = self.#field_name as usize;
+                    let account_info = remaining_accounts
+                        .get(idx)
+                        .ok_or(anchor_lang::prelude::ProgramError::NotEnoughAccountKeys)?;
+                    *account_info.key
+                }
+            }
         }

so malformed/truncated account slices fail with a proper ProgramError instead of aborting the program.

sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (1)

405-408: Prefer removing unused lifetime 'b over silencing Clippy

compress_and_close_ctoken_accounts_signed<'b, 'info>(...) doesn’t currently use 'b in its parameters or body, so #[allow(clippy::extra_unused_lifetimes)] is just papering over an unused generic.

Two options:

  • If 'b isn’t needed, simplify the signature to <'info> and drop the allow.
  • If a second lifetime is intentionally reserved for future use (e.g., to tie signer_seeds lifetimes), keep it but add a brief comment explaining why the allow is intentional so future maintainers don’t “clean it up” blindly.

Also applies to: 408-418

sdk-libs/compressed-token-sdk/src/lib.rs (1)

4-8: Clarify intent of crate-root re-export of decompress_runtime helpers

You now expose process_decompress_tokens_runtime and CTokenSeedProvider both via:

  • compressed_token_sdk::compressible::process_decompress_tokens_runtime, and
  • compressed_token_sdk::process_decompress_tokens_runtime (this new re-export).

That’s fine ergonomically, but it does duplicate the surface. If the goal is to make these part of the top-level public API, consider adding a short module- or crate-level doc comment stating that these runtime helpers are intended to be used directly from the crate root, so future readers don’t wonder why they’re re-exported twice.

Also applies to: 15-15

sdk-libs/macros/src/compressible/variant_enum.rs (2)

49-56: Make Default semantics of CompressedAccountVariant explicit or configurable

Default implicitly picks the first account type as the default variant. That couples behavior to macro argument ordering, which is easy to miss and brittle if someone later reorders/extends the list.

Consider either:

  • Allowing the macro input to specify an explicit default variant, or
  • Emitting a doc comment on CompressedAccountVariant that clearly states: “Default uses the first account type passed to the macro” (with an example).

That makes the contract obvious to downstream users and safer to evolve.


58-76: Document and/or tighten invariants around unreachable!() variants and PackedCTokenData::unpack

Across the generated impls (DataHasher, HasCompressionInfo, Size, Pack, Unpack), most packed variants and the CToken variants are handled via bare unreachable!(), except PackedCTokenData in Unpack, which returns Ok(self.clone()). That leaves a few rough edges:

  • Calling these traits on the “wrong” variant panics at runtime with no context.
  • PackedCTokenData::unpack returning a cloned PackedCTokenData variant is surprising given the trait name and the behavior of other variants.

If these invariants are intentional, I’d strongly recommend:

  • Generating a doc comment on CompressedAccountVariant (or macro docs) that clearly spells out which variants are valid for each trait/method, and that other variants will panic at runtime.
  • Making the Unpack behavior for PackedCTokenData consistent with the rest (either unreachable!() with a clear message, or a more explicit panic message stating that CToken unpacking is handled elsewhere).

That keeps the macro powerful while making misuse easier to diagnose.

Also applies to: 85-151, 153-171, 173-195, 197-219

sdk-libs/sdk-types/src/lib.rs (1)

22-28: Consider deriving Default for RentSponsor

If you often need a “zeroed” or placeholder RentSponsor (e.g., in configs/tests), adding #[derive(Default)] would avoid manual [0u8; 32] boilerplate. If such placeholders are rare, the current shape is fine as-is.

sdk-libs/macros/src/compressible/decompress_context.rs (1)

132-137: Add a bounds check before indexing solana_accounts.

collect_pda_and_token still indexes solana_accounts[i] without verifying i < solana_accounts.len(). A mismatched slice (macro misuse, future refactor, adversarial input) will panic on-chain instead of returning a clear ProgramError. Please guard the index and return ProgramError::InvalidAccountData when the slices diverge. citeturn0files2

sdk-libs/macros/src/compressible/utils.rs (1)

41-75: Treat arrays as Copy only when their element type is Copy.

is_copy_type still returns true for every Type::Array, so a field like [String; 4] is marked Copy and the generated code will try to move it by value, causing compile-time errors. Recurse into array.elem and only return true when the element type is Copy (matching Rust’s rules). citeturn0files4

sdk-libs/sdk/src/compressible/config.rs (1)

338-353: address_space updates are still effectively locked and duplicate-check compares references

Two related issues around address_space:

  1. Updates are effectively disabled

With MAX_ADDRESS_TREES_PER_SPACE == 1 and:

if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE {}
validate_address_space_no_duplicates(&new_address_space)?;
validate_address_space_only_adds(&config.address_space, &new_address_space)?;

the only valid new_address_space is one that is exactly equal to the existing config.address_space (same single Pubkey, no duplicates, and “only adds” applied to a length‑1 vector). Callers can’t meaningfully change the address space.

If this is intentional (lock address_space for now), it would be clearer to either:

  • Reject new_address_space.is_some() with a dedicated error, or
  • Document explicitly that address_space is immutable in v1.

If you do want to allow future extensions, consider relaxing the invariants once MAX_ADDRESS_TREES_PER_SPACE is increased.

  1. Duplicate-check compares pointer identity, not Pubkey values

validate_address_space_no_duplicates currently does:

let mut seen = HashSet::new();
for pubkey in address_space {
    if !seen.insert(pubkey) {}
}

By inference this is a HashSet<&Pubkey>, so it only detects duplicate references, not duplicate values. With today’s len() == 1 this doesn’t bite, but it will silently miss value duplicates once multiple trees are allowed.

You can fix this now without behavior change:

fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
-    let mut seen = HashSet::new();
-    for pubkey in address_space {
-        if !seen.insert(pubkey) {
+    let mut seen: HashSet<Pubkey> = HashSet::new();
+    for pubkey in address_space {
+        if !seen.insert(*pubkey) {
             msg!("Duplicate pubkey found in address_space: {}", pubkey);
             return Err(LightSdkError::ConstraintViolation);
         }
     }
     Ok(())
}

Also applies to: 504-514, 516-532

sdk-libs/macros/src/compressible/traits.rs (1)

86-129: Treat compression_info as a reserved field in CompressAs and Size generators

Right now compression_info is handled twice:

  • generate_compress_as_impl always sets compression_info: None, and
  • generate_compress_as_field_assignments will also emit an assignment for the compression_info field (clone/copy), producing a duplicate field in the struct literal and a compile error for any struct that actually has compression_info.

Similarly, generate_size_impl adds a fixed compression_info_size, but generate_size_fields will also include compression_info in the try_to_vec().len() sum, double‑counting its bytes.

You should exclude compression_info from the generic per‑field loops and let the dedicated logic own it. For example:

fn generate_compress_as_field_assignments(
    fields: &Punctuated<Field, Token![,]>,
    compress_as_fields: &Option<CompressAsFields>,
) -> Vec<TokenStream> {
    let mut field_assignments = Vec::new();

    for field in fields {
        let field_name = field.ident.as_ref().unwrap();
        let field_type = &field.ty;

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is handled explicitly in `generate_compress_as_impl`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
        …
    }
}
fn generate_size_fields(fields: &Punctuated<Field, Token![,]>) -> Vec<TokenStream> {
    let mut size_fields = Vec::new();

    for field in fields.iter() {
        let field_name = field.ident.as_ref().unwrap();

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is already accounted for via `compression_info_size`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }

        size_fields.push(quote! {
            + self.#field_name.try_to_vec().expect("Failed to serialize").len()
        });
    }

    size_fields
}

Optionally, also call validate_compression_info_field from derive_compress_as so a bare #[derive(CompressAs)] fails fast when the field is missing.

Also applies to: 150-181, 192-209

sdk-libs/sdk/src/compressible/compress_runtime.rs (1)

46-55: Use a structured SDK error instead of ProgramError::Custom(0) for config mismatches

These checks:

if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
    return Err(ProgramError::Custom(0));
}
if *ctx.compression_authority().key != compression_config.compression_authority {
    return Err(ProgramError::Custom(0));
}

silently collapse distinct “config mismatch” cases into an opaque Custom(0) code and diverge from the rest of the compressible module, which already routes such violations through LightSdkError.

It would be more consistent and debuggable to surface a proper SDK error, e.g.:

-    if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
-        return Err(ProgramError::Custom(0));
-    }
-    if *ctx.compression_authority().key != compression_config.compression_authority {
-        return Err(ProgramError::Custom(0));
-    }
+    if *ctx.rent_sponsor().key != compression_config.rent_sponsor
+        || *ctx.compression_authority().key != compression_config.compression_authority
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

This keeps on‑chain error codes aligned with the rest of the SDK and makes mismatches much easier to diagnose.

#!/bin/bash
# Verify LightSdkError::ConstraintViolation exists and maps into ProgramError.
rg -n "enum\s+LightSdkError" sdk-libs/sdk/src -A10
rg -n "ConstraintViolation" sdk-libs/sdk/src -A3 -B3
rg -n "impl From<LightSdkError> for solana_program_error::ProgramError" sdk-libs/sdk/src -A5
sdk-libs/macros/src/compressible/seed_providers.rs (1)

57-161: Optional ctoken_* accounts are handled correctly now; consider deduplicating helper logic

The seed-provider generation now:

  • Treats fee_payer / rent_sponsor / config / compression_authority as always-present and calls .key() on them.
  • Routes everything else (including ctoken_* and extra seed accounts) through the optional path with .as_ref().ok_or(...)? .key(), fixing the earlier compile-time issue when referencing optional ctoken_* accounts in seeds.

That behavior looks correct.

Non-blocking cleanup opportunities:

  • The "fee_payer" | "rent_sponsor" | "config" | "compression_authority" “standard field” set is repeated in several places (token vs authority seeds). A small helper like fn is_standard_account_field(&str) -> bool would reduce drift.
  • The local is_pubkey_type at the bottom duplicates compressible::utils::is_pubkey_type. Prefer importing the shared helper so client parameter inference stays consistent across macros.

Also applies to: 183-260, 871-882

sdk-libs/macros/src/compressible/instructions.rs (4)

61-131: TokenSeedSpec::parse still mishandles seeds that start with identifier expressions (ctx.accounts.*, data.*, etc.)

The parser still consumes the first identifier eagerly:

if content.peek(Ident) {
    let first_ident: Ident = content.parse()?;
    match first_ident.to_string().as_str() {
        "is_token" => {}
        "true" => {}
        "is_pda" | "false" => {}
        _ => {
            // builds a SeedElement::Expression from just `first_ident`}
    }
}

For common seeds like:

MyPda = (ctx.accounts.user, data.bump)
MyPda = (data.index)

this parses only ctx / data as an expression, drops the rest (.accounts.user, …), and leaves trailing tokens unconsumed, which will cause a confusing parse error.

The intent from earlier discussions was to only special‑case control keywords (is_token, is_ata, true, is_pda, false) and otherwise delegate the entire parenthesized content to parse_seeds_with_authority.

Concretely, you can switch to lookahead and avoid consuming the ident unless it’s a control keyword:

-        let (is_token, is_ata, seeds, authority) = if content.peek(Ident) {
-            let first_ident: Ident = content.parse()?;
-
-            match first_ident.to_string().as_str() {
+        let (is_token, is_ata, seeds, authority) = if content.peek(Ident) {
+            // Look ahead, but don't consume unless it's a control keyword
+            let first_ident: Ident = content.fork().parse()?;
+
+            match first_ident.to_string().as_str() {
                 "is_token" => {
+                    let _kw: Ident = content.parse()?; // is_token
                     let _comma: Token![,] = content.parse()?;
                     …
                 }
                 "true" => {
+                    let _kw: Ident = content.parse()?; // true
                     let _comma: Token![,] = content.parse()?;
                     let (seeds, authority) = parse_seeds_with_authority(&content)?;
                     (Some(true), false, seeds, authority)
                 }
                 "is_pda" | "false" => {
+                    let _kw: Ident = content.parse()?; // is_pda / false
                     let _comma: Token![,] = content.parse()?;
                     let (seeds, authority) = parse_seeds_with_authority(&content)?;
                     (Some(false), false, seeds, authority)
                 }
-                _ => {
-                    // custom handling that only sees `first_ident` and can drop the rest
-
-                }
+                _ => {
+                    // No control keyword: treat the whole content as seeds (+ optional authority)
+                    let (seeds, authority) = parse_seeds_with_authority(&content)?;
+                    (None, false, seeds, authority)
+                }
             }
         } else {
             let (seeds, authority) = parse_seeds_with_authority(&content)?;
             (None, false, seeds, authority)
         };

That lets arbitrary Rust expressions (including ctx.accounts.user, data.bump, function calls, etc.) pass through the existing SeedElement machinery without being truncated.


424-461: Fix TokenSeedSpec lookup: comparing Ident to String does not compile

In the PDA seed provider impls you look up the seed spec with:

let name_str = name.to_string();
let spec = if let Some(ref pda_seed_specs) = pda_seeds {
    pda_seed_specs
        .iter()
        .find(|s| s.variant == name_str)
        .ok_or_else(|| macro_error!(… name_str))?
} else {};

But TokenSeedSpec::variant is a syn::Ident, and Ident does not implement PartialEq<String>, so s.variant == name_str does not compile.

You want to compare like‑typed values here. Either compare idents directly:

-        .find(|s| s.variant == name_str)
+        .find(|s| s.variant == *name)

or compare their string forms:

-        .find(|s| s.variant == name_str)
+        .find(|s| s.variant.to_string() == name_str)

The first option is cheaper and keeps the type information; you can still use name_str purely for the error message.


985-1070: Use a dedicated overflow error instead of ProgramError::Custom(0) in rent distribution

The rent-based close distribution inside the generated CompressContext::compress_pda_account impl correctly:

  • Computes AccountRentState,
  • Uses calculate_close_distribution,
  • Moves lamports with checked_sub / checked_add.

However, on checked_add overflow you currently return ProgramError::Custom(0):

**dst = dst
    .checked_add(dist.to_user)
    .ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;**dst = dst
    .checked_add(dist.to_rent_sponsor)
    .ok_or(anchor_lang::prelude::ProgramError::Custom(0))?;

Overflow here is rare but represents a distinct, serious condition; collapsing it into Custom(0) makes debugging and monitoring harder and may collide with other uses of code 0.

Consider adding a specific CompressibleInstructionError variant (e.g. OverflowDuringRentDistribution) and mapping to that instead, so on-chain failures are self-describing and consistent with the rest of your error surface.

#!/bin/bash
# Locate CompressibleInstructionError and check how it's converted to ProgramError.
rg -n "enum\s+CompressibleInstructionError" sdk-libs/macros/src/compressible -A10
rg -n "From<CompressibleInstructionError" -S

405-415: Avoid unreachable!() for PdaOnly / TokenOnly instruction variants or reject them explicitly

InstructionVariant supports PdaOnly, TokenOnly, and Mixed, and add_compressible_instructions happily constructs PdaOnly/TokenOnly when only PDA or only token seeds are provided:

let instruction_variant = match (has_pda_seeds, has_token_seeds) {
    (true, true) => InstructionVariant::Mixed,
    (true, false) => InstructionVariant::PdaOnly,
    (false, true) => InstructionVariant::TokenOnly,};

But downstream code treats those variants as impossible and uses unreachable!():

  • generate_decompress_accounts_struct
  • the generated CompressAccountsIdempotent definition (compress side)
  • generate_error_codes variant-specific errors

If a user invokes the macro with only PDA seeds or only token seeds, macro expansion will panic with a confusing unreachable!() rather than emitting a clear compile-time error.

Either:

  • Reject unsupported variants up front in the match:
-    let instruction_variant = match (has_pda_seeds, has_token_seeds) {
-        (true, true) => InstructionVariant::Mixed,
-        (true, false) => InstructionVariant::PdaOnly,
-        (false, true) => InstructionVariant::TokenOnly,
-        (false, false) => { … }
-    };
+    let instruction_variant = match (has_pda_seeds, has_token_seeds) {
+        (true, true) => InstructionVariant::Mixed,
+        (true, false) | (false, true) => {
+            return Err(macro_error!(
+                &module,
+                "PDA-only or token-only compression is not supported yet. \
+                 Provide both PDA and token seed specifications."
+            ));
+        }
+        (false, false) => { … }
+    };
  • Or fully implement PdaOnly / TokenOnly support and remove the unreachable!() calls.

The first option is much nicer UX today and avoids panicking inside the macro.

Also applies to: 710-728, 1556-1569

sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)

239-244: Stop decompressing when rent sponsor mismatches config

We load compression_config but never assert that ctx.rent_sponsor() matches compression_config.rent_sponsor. If a caller wires the wrong account, we’ll happily try to create PDAs with an unexpected payer, breaking the rent-flow invariants that the compress path already enforces. Please mirror the compress runtime: compare the keys immediately after load_checked and return LightSdkError::ConstraintViolation (log if you wish) when they differ so the transaction fails fast.

sdk-libs/sdk/src/compressible/compression_info.rs (2)

290-320: Return None when nothing was claimed

Per the docstring, callers expect None when no rent was actually claimed. The current fallback returns Ok(Some(0)), forcing them to pattern-match a value that should have been None and making it impossible to distinguish “not claimable” from “claim succeeded with value.” Please return Ok(None) (after the loop) so the behavior matches the documented contract.

-    if let Some(amount) = claimable {
-        if amount > 0 {
+    if let Some(amount) = claimable {
+        if amount > 0 {
             ...
             return Ok(Some(amount));
         }
     }
-    Ok(Some(0))
+    Ok(None)

339-356: Use the real system program ID in CPI transfers

transfer_lamports_cpi hard-codes an all-zero Pubkey as the System Program, so every invocation fails with IncorrectProgramId. Leverage the account we already receive: assert it equals solana_program::system_program::ID (to catch wiring bugs) and set the instruction’s program_id to *system_program.key. That restores the CPI path and prevents silent rent top-up failures.

-    // System Program ID
-    const SYSTEM_PROGRAM_ID: [u8; 32] = [
-        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-        0, 0,
-    ];
-
+    if system_program.key != &solana_program::system_program::ID {
+        return Err(ProgramError::IncorrectProgramId);
+    }
     // System Program Transfer instruction discriminator: 2 (u32 little-endian)
     let mut instruction_data = vec![2, 0, 0, 0];
     instruction_data.extend_from_slice(&lamports.to_le_bytes());
 
     let transfer_instruction = Instruction {
-        program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
+        program_id: *system_program.key,
         accounts: vec![
             AccountMeta::new(*from.key, true),
             AccountMeta::new(*to.key, false),
         ],
         data: instruction_data,
     };
sdk-libs/macros/src/lib.rs (2)

256-261: Make DataHasher actually alias LightHasher

The docs say “Alias of LightHasher”, but we’re still delegating to the SHA variant. That silently swaps hashing algorithms and breaks expectations for downstream derivations. Please call derive_light_hasher(input) here so the alias is real (or update the docs, but aligning behavior is the safer fix).

 #[proc_macro_derive(DataHasher, attributes(skip, hash))]
 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }

292-295: Allow #[skip] on HasCompressionInfo as documented

The derive example requires #[skip] on the compression_info field, but the macro attribute list doesn’t whitelist it. As soon as users follow the docs, compilation fails with “cannot find attribute skip.” Add attributes(skip) to the proc-macro declaration so the derive accepts the attribute and matches the documented contract.

-#[proc_macro_derive(HasCompressionInfo)]
+#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
 pub fn has_compression_info(input: TokenStream) -> TokenStream {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f30f02 and a125346.

⛔ Files ignored due to path filters (49)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • scripts/lint.sh is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/processor.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/variant.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/user_record_tests.rs is excluded by none and included by none
📒 Files selected for processing (41)
  • sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/compressible/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressible-client/src/lib.rs (9 hunks)
  • sdk-libs/macros/Cargo.toml (1 hunks)
  • sdk-libs/macros/src/compressible/GUIDE.md (1 hunks)
  • sdk-libs/macros/src/compressible/README.md (1 hunks)
  • sdk-libs/macros/src/compressible/decompress_context.rs (1 hunks)
  • sdk-libs/macros/src/compressible/instructions.rs (1 hunks)
  • sdk-libs/macros/src/compressible/mod.rs (1 hunks)
  • sdk-libs/macros/src/compressible/pack_unpack.rs (1 hunks)
  • sdk-libs/macros/src/compressible/seed_providers.rs (1 hunks)
  • sdk-libs/macros/src/compressible/traits.rs (1 hunks)
  • sdk-libs/macros/src/compressible/utils.rs (1 hunks)
  • sdk-libs/macros/src/compressible/variant_enum.rs (1 hunks)
  • sdk-libs/macros/src/cpi_signer.rs (1 hunks)
  • sdk-libs/macros/src/lib.rs (7 hunks)
  • sdk-libs/macros/src/rent_sponsor.rs (1 hunks)
  • sdk-libs/macros/src/utils.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (1 hunks)
  • sdk-libs/program-test/src/compressible.rs (5 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (3 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
  • sdk-libs/sdk-types/src/constants.rs (1 hunks)
  • sdk-libs/sdk-types/src/lib.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (2 hunks)
  • sdk-libs/sdk/src/compressible/close.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (4 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (12 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (7 hunks)
  • sdk-libs/sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (2 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
  • sdk-libs/sdk/src/utils.rs (2 hunks)
  • sdk-libs/token-client/Cargo.toml (0 hunks)
💤 Files with no reviewable changes (1)
  • sdk-libs/token-client/Cargo.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-14T00:36:53.191Z
Learnt from: ananas-block
Repo: Lightprotocol/light-protocol PR: 1909
File: program-libs/zero-copy/src/init_mut.rs:241-249
Timestamp: 2025-08-14T00:36:53.191Z
Learning: In the light-protocol zero-copy crate, performance is prioritized over safety checks for edge cases like Vec lengths exceeding u32::MAX, even when there might be wire format inconsistencies.

Applied to files:

  • sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs
  • sdk-libs/macros/src/compressible/variant_enum.rs
  • sdk-libs/macros/src/compressible/pack_unpack.rs
🧬 Code graph analysis (27)
sdk-libs/program-test/src/program_test/test_rpc.rs (2)
sdk-libs/program-test/src/compressible.rs (2)
  • new (53-73)
  • claim_and_compress (81-199)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
  • new (68-382)
sdk-libs/macros/src/utils.rs (2)
sdk-libs/macros/src/compressible/instructions.rs (1)
  • syn (303-303)
sdk-libs/macros/src/compressible/variant_enum.rs (1)
  • syn (22-22)
sdk-libs/compressed-token-sdk/src/compressible/mod.rs (1)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/rent_sponsor.rs (1)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_rent_sponsor_pda (615-617)
  • derive_light_rent_sponsor (632-634)
sdk-libs/program-test/src/program_test/compressible_setup.rs (1)
sdk-libs/compressible-client/src/lib.rs (2)
  • initialize_compression_config (80-127)
  • update_compression_config (130-164)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • compressible (204-204)
  • rent_sponsor (67-67)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_decompressed (104-112)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/macros/src/compressible/decompress_context.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (11)
  • compressible (204-204)
  • handle_packed_pda_variant (150-221)
  • rent_sponsor (67-67)
  • fee_payer (65-65)
  • config (66-66)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
  • collect_pda_and_token (78-88)
  • process_tokens (94-108)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/lib.rs (1)
  • derive_decompress_context (562-567)
sdk-libs/macros/src/compressible/utils.rs (2)
sdk-libs/macros/src/accounts.rs (1)
  • type_path (494-502)
sdk-libs/macros/src/compressible/seed_providers.rs (1)
  • is_pubkey_type (871-882)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • fee_payer (65-65)
  • config (66-66)
  • rent_sponsor (67-67)
  • compressible (204-204)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-43)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (2)
sdk-libs/compressed-token-sdk/src/ctoken/create_token_account/instruction.rs (2)
  • create_token_account (92-118)
  • create_ctoken_account_signed (122-171)
sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs (1)
  • decompress_full_ctoken_accounts_with_indices (43-125)
sdk-libs/sdk/src/lib.rs (2)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_rent_sponsor (632-634)
  • derive_light_rent_sponsor_pda (615-617)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor (133-187)
  • derive_light_rent_sponsor_pda (83-124)
sdk-libs/macros/src/compressible/variant_enum.rs (2)
sdk-libs/macros/src/compressible/decompress_context.rs (2)
  • parse (16-20)
  • parse (28-32)
sdk-libs/sdk/src/compressible/compression_info.rs (6)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
  • pack (17-17)
  • unpack (23-26)
sdk-libs/macros/src/cpi_signer.rs (2)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_cpi_signer_pda (581-583)
  • derive_light_cpi_signer (598-600)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • last_claimed_slot (126-128)
sdk-libs/program-test/src/compressible.rs (4)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • config (12-12)
  • rent_sponsor (13-13)
  • compression_authority (14-14)
program-libs/compressible/src/config.rs (1)
  • new_ctoken (163-182)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (70-77)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (288-358)
sdk-libs/sdk/src/compressible/mod.rs (3)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • process_compress_pda_accounts_idempotent (28-117)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • compression_info (37-37)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • check_account_types (132-145)
  • handle_packed_pda_variant (150-221)
  • process_decompress_accounts_idempotent (226-358)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
sdk-libs/program-test/src/program_test/config.rs (1)
  • new (60-69)
sdk-libs/compressed-token-sdk/src/lib.rs (1)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/compressible/traits.rs (2)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • extract_fields_from_item_struct (11-21)
  • is_copy_type (45-76)
sdk-libs/sdk/src/compressible/compression_info.rs (4)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • compressible (204-204)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/macros/src/compressible/utils.rs (1)
  • is_pubkey_type (95-105)
sdk-libs/macros/src/compressible/pack_unpack.rs (3)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • is_copy_type (45-76)
  • is_pubkey_type (95-105)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • insert_or_get (301-303)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • pack (17-17)
  • unpack (23-26)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_from_config (89-100)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • prepare_account_for_decompression_idempotent (82-148)
  • into_compressed_meta_with_address (25-44)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/config.rs (2)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • compression_authority (14-14)
  • rent_sponsor (13-13)
  • config (12-12)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • rent_sponsor (67-67)
  • config (66-66)
sdk-libs/macros/src/compressible/instructions.rs (5)
sdk-libs/macros/src/compressible/variant_enum.rs (3)
  • syn (22-22)
  • parse (14-18)
  • compressed_account_variant (21-247)
sdk-libs/macros/src/compressible/decompress_context.rs (3)
  • parse (16-20)
  • parse (28-32)
  • generate_decompress_context_trait_impl (35-185)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (14)
  • compressible (204-204)
  • derive_pda_seeds_with_accounts (122-127)
  • handle_packed_pda_variant (150-221)
  • rent_sponsor (67-67)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
  • collect_pda_and_token (78-88)
  • fee_payer (65-65)
  • config (66-66)
  • process_decompress_accounts_idempotent (226-358)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
  • generate_ctoken_account_variant_enum (9-25)
  • generate_ctoken_seed_provider_implementation (27-363)
  • generate_client_seed_functions (366-499)
sdk-libs/macros/src/compressible/utils.rs (1)
  • generate_empty_ctoken_enum (110-116)
sdk-libs/sdk/src/compressible/compression_info.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • compressible (204-204)
  • rent_sponsor (67-67)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/program-test/src/compressible.rs (1)
  • new (53-73)
sdk-libs/macros/src/lib.rs (10)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (2)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (187-226)
sdk-libs/macros/src/cpi_signer.rs (2)
  • derive_light_cpi_signer_pda (7-42)
  • derive_light_cpi_signer (44-91)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (83-124)
  • derive_light_rent_sponsor (133-187)
🪛 markdownlint-cli2 (0.18.1)
sdk-libs/macros/src/compressible/GUIDE.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


31-31: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


43-43: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


53-53: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)


75-75: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


80-80: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


138-138: Ordered list item prefix
Expected: 1; Actual: 2; Style: 1/1/1

(MD029, ol-prefix)


157-157: Ordered list item prefix
Expected: 1; Actual: 3; Style: 1/1/1

(MD029, ol-prefix)


173-173: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1

(MD029, ol-prefix)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: programs (light-system-program-address, ["cargo-test-sbf -p system-test -- test_with_address", "c...
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Test program-libs-slow

Comment on lines +12 to +32
/// Trait for getting token account seeds.
pub trait CTokenSeedProvider: Copy {
/// Type of accounts struct needed for seed derivation.
type Accounts<'info>;

/// Get seeds for the token account PDA (used for decompression).
fn get_seeds<'a, 'info>(
&self,
accounts: &'a Self::Accounts<'info>,
remaining_accounts: &'a [AccountInfo<'info>],
) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;

/// Get authority seeds for signing during compression.
///
/// TODO: consider removing.
fn get_authority_seeds<'a, 'info>(
&self,
accounts: &'a Self::Accounts<'info>,
remaining_accounts: &'a [AccountInfo<'info>],
) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Safety fixes in decompression runtime look solid; consider clarifying get_authority_seeds

The runtime now:

  • Validates owner_index / mint_index against packed_accounts.len() before indexing.
  • Only derives a CPI context pubkey when has_pdas is true.
  • Uses plain Vec for token_decompress_indices.

This resolves the earlier panic/logic risks in this path.

The only remaining nit is the CTokenSeedProvider::get_authority_seeds method: it’s still required by the trait but unused in this runtime and marked with a TODO. Either document it as reserved for future use or move it to a separate trait so implementors don’t have to provide dead code today.

Also applies to: 60-79, 81-163

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs around
lines 12 to 32, the trait CTokenSeedProvider still requires get_authority_seeds
even though the decompression runtime never uses it; either extract
get_authority_seeds into a new separate trait (e.g.,
CTokenAuthoritySeedProvider) so implementors of CTokenSeedProvider aren’t forced
to implement unused methods, or add a default implementation that returns an
Err/unused-ok value and update docs to mark it reserved for future use; update
trait docs and any implementations accordingly so no dead/unnecessary
implementations are required.

Comment on lines +667 to +676
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 estimated_capacity = compressed_accounts.len();
let mut compressed_pda_infos = Vec::with_capacity(estimated_capacity);
let mut compressed_token_accounts: Vec<(
light_compressed_token_sdk::compat::PackedCTokenData<CTokenAccountVariant>,
light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress,
)> = Vec::with_capacity(estimated_capacity);

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 out-of-bounds slicing when deriving post_system_accounts

In __macro_helpers::collect_pda_and_token you have:

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..];

If system_accounts_end_offset() ever exceeds all_infos.len() (e.g., due to a mismatch between the passed remaining accounts and the CPI layout), this will panic with an out‑of‑bounds slice.

Elsewhere (e.g., in light_sdk::compressible::compress_runtime and the new decompression runtime) you’ve started using safe .get(..) plus a ConstraintViolation error instead of panicking. It would be good to mirror that pattern here:

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 post_system_accounts = all_infos
+    .get(post_system_offset..)
+    .ok_or_else(|| solana_program_error::ProgramError::from(
+        light_sdk::error::LightSdkError::ConstraintViolation,
+    ))?;

and adjust the call sites inside this helper to work with the resulting slice reference.

Comment on lines +1182 to +1308
#[inline(never)]
fn generate_pda_seed_derivation_for_trait(
spec: &TokenSeedSpec,
_instruction_data: &[InstructionDataSpec],
) -> Result<TokenStream> {
let mut bindings: Vec<TokenStream> = Vec::new();
let mut seed_refs = Vec::new();

// Recursively rewrite expressions:
// - `data.<field>` -> `seed_params.<field>` (from instruction params, not struct fields!)
// - `ctx.accounts.<account>` -> `accounts.<account>`
// - `ctx.<field>` -> `accounts.<field>`
// While preserving function/method calls and references.
fn map_pda_expr_to_params(expr: &syn::Expr) -> syn::Expr {
match expr {
syn::Expr::Field(field_expr) => {
if let syn::Member::Named(field_name) = &field_expr.member {
// Handle nested field access: ctx.accounts.field_name -> accounts.field_name.as_ref().ok_or(...)?.key()
if let syn::Expr::Field(nested_field) = &*field_expr.base {
if let syn::Member::Named(base_name) = &nested_field.member {
if base_name == "accounts" {
if let syn::Expr::Path(path) = &*nested_field.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "ctx" {
return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError {
let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into();
err.into()
})?.key() };
}
}
}
}
}
}
// Handle direct field access
if let syn::Expr::Path(path) = &*field_expr.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "data" {
// data.field -> seed_params.field (from instruction params!)
return syn::parse_quote! { seed_params.#field_name };
} else if segment.ident == "ctx" {
// ctx.field -> accounts.field.as_ref().ok_or(...)?.key() (error if optional account is missing)
return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError {
let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into();
err.into()
})?.key() };
}
}
}
}
expr.clone()
}
syn::Expr::MethodCall(method_call) => {
// Special case: ctx.accounts.account_name.key() -> accounts.account_name.key()
// This is already handled by the Field case transforming ctx.accounts.X -> accounts.X
let mut new_method_call = method_call.clone();
new_method_call.receiver = Box::new(map_pda_expr_to_params(&method_call.receiver));
new_method_call.args = method_call
.args
.iter()
.map(map_pda_expr_to_params)
.collect();
syn::Expr::MethodCall(new_method_call)
}
syn::Expr::Call(call_expr) => {
// Map function args recursively. We do not transform the function path.
let mut new_call_expr = call_expr.clone();
new_call_expr.args = call_expr.args.iter().map(map_pda_expr_to_params).collect();
syn::Expr::Call(new_call_expr)
}
syn::Expr::Reference(ref_expr) => {
let mut new_ref_expr = ref_expr.clone();
new_ref_expr.expr = Box::new(map_pda_expr_to_params(&ref_expr.expr));
syn::Expr::Reference(new_ref_expr)
}
_ => {
// For other expressions (constants, literals, paths), leave as-is
expr.clone()
}
}
}

for (i, seed) in spec.seeds.iter().enumerate() {
match seed {
SeedElement::Literal(lit) => {
let value = lit.value();
seed_refs.push(quote! { #value.as_bytes() });
}
SeedElement::Expression(expr) => {
if let syn::Expr::Path(path_expr) = &**expr {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
seed_refs.push(quote! { #ident.as_bytes() });
continue;
}
}
}

// Generic solution: rewrite any `data.*` occurrences recursively to `self.*`,
// then bind the result to a local to ensure lifetimes are valid,
// and use `.as_ref()` to convert into a seed `&[u8]`.
let binding_name =
syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site());
let mapped_expr = map_pda_expr_to_params(expr);
bindings.push(quote! {
let #binding_name = #mapped_expr;
});
seed_refs.push(quote! { (#binding_name).as_ref() });
}
}
}

let indices: Vec<usize> = (0..seed_refs.len()).collect();

Ok(quote! {
#(#bindings)*
let seeds: &[&[u8]] = &[#(#seed_refs,)*];
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id);
let mut seeds_vec = Vec::with_capacity(seeds.len() + 1);
#(
seeds_vec.push(seeds[#indices].to_vec());
)*
seeds_vec.push(vec![bump]);
Ok((seeds_vec, pda))
})
}
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

Fix PDA seed derivation for ctx.accounts.* / ctx.* to respect optional vs non-optional accounts

generate_pda_seed_derivation_for_trait now rewrites expressions used in PDA seeds via map_pda_expr_to_params. For account-based seeds it generates expressions like:

// ctx.accounts.field -> accounts.field.as_ref().ok_or(...)? .key()
return syn::parse_quote! {
    accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError {
        let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into();
        err.into()
    })?.key()
};

This unconditionally assumes accounts.#field_name is an Option<_>, but in the generated DecompressAccountsIdempotent struct:

  • fee_payer: Signer<'info>
  • config: AccountInfo<'info>
  • rent_sponsor: UncheckedAccount<'info>

are not optional, while only the extra seed accounts and ctoken_* fields are Option<_>.

As a result, for seeds like ctx.accounts.rent_sponsor or ctx.accounts.fee_payer, the generated code will try to call .as_ref().ok_or_else()? on a non-optional account type and fail to compile. The same problem applies to the ctx.field branch.

You need to mirror the distinction you already use in seed_providers.rs: treat always-present “standard” accounts differently from optional ones. For example:

fn is_standard_account_field(name: &str) -> bool {
    matches!(name, "fee_payer" | "rent_sponsor" | "config" | "compression_authority")
}

fn map_pda_expr_to_params(expr: &syn::Expr) -> syn::Expr {
    match expr {
        syn::Expr::Field(field_expr) => {
            if let syn::Member::Named(field_name) = &field_expr.member {
                if let syn::Expr::Field(nested_field) = &*field_expr.base {
                    if let syn::Member::Named(base_name) = &nested_field.member {
                        if base_name == "accounts" {
                            if let syn::Expr::Path(path) = &*nested_field.base {
                                if let Some(segment) = path.path.segments.first() {
                                    if segment.ident == "ctx" {
                                        let field_str = field_name.to_string();
                                        if is_standard_account_field(&field_str) {
                                            // Non-optional account: just key()
                                            return syn::parse_quote! { accounts.#field_name.key() };
                                        } else {
                                            // Optional seed account: as_ref().ok_or(...)?.key()
                                            return syn::parse_quote! {
                                                accounts.#field_name
                                                    .as_ref()
                                                    .ok_or_else(|| -> solana_program_error::ProgramError {
                                                        let err: anchor_lang::error::Error =
                                                            CompressibleInstructionError::MissingSeedAccount.into();
                                                        err.into()
                                                    })?
                                                    .key()
                                            };
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                // … existing data/ctx handling for data.field and ctx.field …
            }
            expr.clone()
        }
        // other arms unchanged …
        _ => expr.clone(),
    }
}

This keeps non-optional accounts simple (.key()) while still returning a structured error when an optional seed account is missing, matching what you already do in the ctoken seed provider generator.

Without this distinction, valid seed specs that reference always-present accounts will make the macro output fail to compile.

🤖 Prompt for AI Agents
sdk-libs/macros/src/compressible/instructions.rs around lines 1182-1308: the PDA
seed expression rewriting unconditionally treats ctx.accounts.<name> and
ctx.<name> as Option<_> and generates .as_ref().ok_or_else()? calls, which
breaks for always-present account types (e.g. fee_payer, config, rent_sponsor,
compression_authority). Add a helper like is_standard_account_field(name: &str)
-> bool listing those non-optional fields and update map_pda_expr_to_params so
that when encountering ctx.accounts.<field> or ctx.<field> it emits
accounts.<field>.key() for standard (non-optional) fields and only emits the
.as_ref().ok_or_else(...)? .key() form for optional seed accounts; keep all
other branches (data.* -> seed_params.*, method calls, calls, references)
unchanged.

Comment on lines +1 to +91
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};

const CPI_AUTHORITY_SEED: &[u8] = b"cpi_authority";

pub fn derive_light_cpi_signer_pda(input: TokenStream) -> TokenStream {
let program_id_lit = parse_macro_input!(input as LitStr);
let program_id_str = program_id_lit.value();

// Parse program ID at compile time
use std::str::FromStr;
let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) {
Ok(id) => id,
Err(_) => {
return syn::Error::new(
program_id_lit.span(),
"Invalid program ID format. Expected a base58 encoded public key",
)
.to_compile_error()
.into();
}
};

// Use fixed "cpi_authority" seed
let seeds = &[CPI_AUTHORITY_SEED];

// Compute the PDA at compile time
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

// Generate the output code with precomputed byte array and bump
let pda_bytes = pda.to_bytes();
let bytes = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));

let output = quote! {
([#(#bytes),*], #bump)
};

output.into()
}

pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream {
let program_id_lit = parse_macro_input!(input as LitStr);
let program_id_str = program_id_lit.value();

// Parse program ID at compile time
use std::str::FromStr;
let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) {
Ok(id) => id,
Err(_) => {
return syn::Error::new(
program_id_lit.span(),
"Invalid program ID format. Expected a base58 encoded public key",
)
.to_compile_error()
.into();
}
};

// Use fixed "cpi_authority" seed
let seeds = &[CPI_AUTHORITY_SEED];

// Compute the PDA at compile time
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

// Generate the output code with precomputed CpiSigner struct
let program_id_bytes = program_id.to_bytes();
let pda_bytes = pda.to_bytes();

let program_id_literals = program_id_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));
let cpi_signer_literals = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));

let output = quote! {
{
// Use the CpiSigner type with absolute path to avoid import dependency
::light_sdk_types::CpiSigner {
program_id: [#(#program_id_literals),*],
cpi_signer: [#(#cpi_signer_literals),*],
bump: #bump,
}
}
};

output.into()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

CPI signer macros look correct; consider sharing the seed constant

The macros correctly validate the program ID at compile time, use a fixed "cpi_authority" seed, and either return the PDA bytes + bump or a fully-populated ::light_sdk_types::CpiSigner. This matches the runtime helper behavior.

If you already expose a shared seed constant for the CPI authority PDA (e.g., CPI_AUTHORITY_PDA_SEED in a types/constants crate), consider reusing it here instead of hardcoding b"cpi_authority" to avoid accidental divergence between macro- and runtime-derived PDAs.

Comment on lines +1 to +187
use light_sdk_types::constants::RENT_SPONSOR_SEED;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse_macro_input, punctuated::Punctuated, Expr, LitInt, LitStr, Token};

struct Args {
program_id: LitStr,
version: Option<LitInt>,
}
impl Parse for Args {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let elems = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
if elems.is_empty() {
return Err(syn::Error::new(
input.span(),
"Expected at least a program id string literal",
));
}
if elems.len() > 2 {
return Err(syn::Error::new_spanned(
&elems[2],
"Too many arguments: expected at most 2 (program_id, version)",
));
}
// First argument must be a string literal
let program_id = match &elems[0] {
Expr::Lit(expr_lit) => {
if let syn::Lit::Str(ls) = &expr_lit.lit {
ls.clone()
} else {
return Err(syn::Error::new_spanned(
&elems[0],
"First argument must be a string literal program id",
));
}
}
_ => {
return Err(syn::Error::new_spanned(
&elems[0],
"First argument must be a string literal program id",
))
}
};
// Optional second argument: version as integer literal (u16)
let version = if elems.len() > 1 {
match &elems[1] {
Expr::Lit(expr_lit) => {
if let syn::Lit::Int(li) = &expr_lit.lit {
Some(li.clone())
} else {
return Err(syn::Error::new_spanned(
&elems[1],
"Second argument must be an integer literal (u16 version)",
));
}
}
_ => {
return Err(syn::Error::new_spanned(
&elems[1],
"Second argument must be an integer literal (u16 version)",
))
}
}
} else {
None
};
Ok(Args {
program_id,
version,
})
}
}

/// Derives a Rent Sponsor PDA for a program at compile time.
///
/// Seeds: ["rent_sponsor", <u16 version little-endian>]
///
/// Usage:
/// - With default version=1:
/// const DATA: ([u8; 32], u8) = derive_light_rent_sponsor_pda!("Program1111111111111111111111111111111111");
/// - With explicit version:
/// const DATA: ([u8; 32], u8) = derive_light_rent_sponsor_pda!("Program1111111111111111111111111111111111", 2);
pub fn derive_light_rent_sponsor_pda(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as Args);
let program_id_str = args.program_id.value();
let version_u16: u16 = match args.version.as_ref() {
Some(lit) => match lit.base10_parse::<u16>() {
Ok(v) => v,
Err(e) => {
return syn::Error::new_spanned(lit, format!("Invalid version number: {}", e))
.to_compile_error()
.into();
}
},
None => 1u16,
};

// Parse program ID at compile time
use std::str::FromStr;
let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) {
Ok(id) => id,
Err(_) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"Invalid program ID format. Expected a base58 encoded public key",
)
.to_compile_error()
.into();
}
};

let seeds = &[RENT_SPONSOR_SEED, &version_u16.to_le_bytes()[..]];
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

let pda_bytes = pda.to_bytes();
let bytes = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));

let output = quote! {
([#(#bytes),*], #bump)
};
output.into()
}

/// Derives a Rent Sponsor configuration struct at compile time.
///
/// Returns `::light_sdk_types::RentSponsor { program_id, rent_sponsor, bump, version }`.
///
/// Usage:
/// const RENT_SPONSOR: ::light_sdk_types::RentSponsor =
/// derive_light_rent_sponsor!("Program1111111111111111111111111111111111", 1);
pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as Args);
let program_id_str = args.program_id.value();
let version_u16: u16 = match args.version.as_ref() {
Some(lit) => match lit.base10_parse::<u16>() {
Ok(v) => v,
Err(e) => {
return syn::Error::new_spanned(lit, format!("Invalid version number: {}", e))
.to_compile_error()
.into();
}
},
None => 1u16,
};

// Parse program ID at compile time
use std::str::FromStr;
let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) {
Ok(id) => id,
Err(_) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"Invalid program ID format. Expected a base58 encoded public key",
)
.to_compile_error()
.into();
}
};

let seeds = &[RENT_SPONSOR_SEED, &version_u16.to_le_bytes()[..]];
let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id);

let program_id_bytes = program_id.to_bytes();
let pda_bytes = pda.to_bytes();

let program_id_literals = program_id_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));
let pda_literals = pda_bytes
.iter()
.map(|b| proc_macro2::Literal::u8_unsuffixed(*b));

let version_lit = proc_macro2::Literal::u16_unsuffixed(version_u16);
let output = quote! {
{
::light_sdk_types::RentSponsor {
program_id: [#(#program_id_literals),*],
rent_sponsor: [#(#pda_literals),*],
bump: #bump,
version: #version_lit,
}
}
};
output.into()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Rent-sponsor macros are robust; consider tightening error span for program ID

Argument parsing, version handling, and PDA derivation all look solid now: you enforce arg count/types, propagate u16 parse errors, and use RENT_SPONSOR_SEED + LE version bytes consistently across both macros.

One small UX improvement: in the from_str failure case you currently use proc_macro2::Span::call_site(). Switching to args.program_id.span() would point the compiler error directly at the offending literal instead of the whole macro invocation, which tends to make diagnostics clearer at call sites.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/rent_sponsor.rs lines 1-187, the macros currently create
an error on invalid program-id using proc_macro2::Span::call_site(); change both
occurrences to use args.program_id.span() so the compile error points at the
offending string literal; update the two Err(_) branches in
derive_light_rent_sponsor_pda and derive_light_rent_sponsor to construct
syn::Error::new(args.program_id.span(), "...") (keeping the same message) and
return its to_compile_error().into().

Comment on lines 219 to 235
let rent_sponsor = cfg.rent_sponsor;
// TODO: add coverage for external compression_authority
let compression_authority = payer.pubkey();
let address_tree = cfg.address_space[0];

let program_accounts = rpc.context.get_program_accounts(&program_id);

if program_accounts.is_empty() {
return Ok(());
}

let output_state_tree_info = rpc
.get_random_state_tree_info()
.map_err(|e| RpcError::CustomError(format!("no state tree: {e:?}")))?;

let program_metas = vec![
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(config_pda, false),
AccountMeta::new(rent_sponsor, false),
AccountMeta::new_readonly(compression_authority, false),
];
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

Respect the configured compression authority when building metas.

auto_compress_program_pdas is hard-coding compression_authority to the local payer and pushes it as a non-signer meta. As soon as the on-chain config stores a different authority (the normal case for production programs), the helper will build instructions that fail authority checks, silently turning the whole routine into a no-op. Please deserialize cfg.compression_authority, mark it as a signer in program_metas, and ensure the corresponding keypair signs the transaction; otherwise every compress attempt will fail with MissingRequiredSignature / authorization errors. citeturn0files0

🤖 Prompt for AI Agents
In sdk-libs/program-test/src/compressible.rs around lines 219 to 235, the code
currently hardcodes compression_authority to the local payer and adds it as a
non-signer meta; instead deserialize cfg.compression_authority, replace the
hardcoded payer pubkey with that deserialized pubkey in program_metas, mark that
AccountMeta as a signer, and ensure the transaction is signed with the
corresponding keypair (load or fetch the keypair for cfg.compression_authority
and include it in the signers list) so compression authority checks won't fail
with MissingRequiredSignature.

Comment on lines 175 to 178
pub use light_sdk_macros::{
light_system_accounts, LightDiscriminator, LightHasher, LightHasherSha, LightTraits,
derive_light_rent_sponsor, derive_light_rent_sponsor_pda, light_system_accounts,
LightDiscriminator, LightHasher, LightHasherSha, LightTraits,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Rent-sponsor macro re-exports are consistent with the SDK surface

Re-exporting derive_light_rent_sponsor and derive_light_rent_sponsor_pda alongside the other light_sdk_macros keeps the macro surface coherent. As a follow-up, consider mentioning these in the top-of-file docs near derive_light_cpi_signer so users discover rent-sponsor support from the main README-style section.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/lib.rs around lines 175 to 178, ensure the rent-sponsor
macros are re-exported with the other light_sdk_macros and add a short mention
in the top-of-file documentation near the existing derive_light_cpi_signer
reference so users can discover rent-sponsor support; update the pub use line to
include derive_light_rent_sponsor and derive_light_rent_sponsor_pda if missing,
and add one sentence in the file-level docs pointing to these macros.

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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
sdk-libs/compressible-client/src/lib.rs (3)

80-127: Fix config PDA derivation mismatch between initialize and update.

initialize_compression_config allows callers to specify an arbitrary config_bump via Option<u8> (line 87, defaulting to 0 at line 89), but update_compression_config always hardcodes 0 when deriving the PDA at line 138:

let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0);

If anyone initializes with a non-zero bump, update_compression_config will target a different PDA than the one created during initialization, causing updates to fail or hit the wrong account.

Fix by either:

  1. Add a config_bump: u8 parameter to update_compression_config and pass it through to derive_pda, ensuring callers use the same bump, or
  2. Remove Option<u8> from initialize_compression_config and hardcode 0 there as well, documenting that only bump=0 is supported.

Apply this diff for option 2 (simpler if only one config instance is intended):

 pub fn initialize_compression_config(
     program_id: &Pubkey,
     discriminator: &[u8],
     payer: &Pubkey,
     authority: &Pubkey,
     rent_sponsor: Pubkey,
     address_space: Vec<Pubkey>,
-    config_bump: Option<u8>,
 ) -> Instruction {
-    let config_bump = config_bump.unwrap_or(0);
-    let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump);
+    let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0);

And update all call sites to remove the config_bump argument.

Also applies to: 129-164


166-284: Replace panicking unwraps with graceful error handling.

Two unwrap sites remain that can panic on malformed input:

  1. Line 207: compressed_accounts[0].0.tree_info.cpi_context.unwrap() assumes the first element has a CPI context, but it might be a token account with None. If the first account is a token and a later one is a PDA with context, this will panic.

  2. Lines 230-233: packed_tree_infos.state_trees.as_ref().unwrap() will panic if state_trees is None.

Apply this diff to fix both:

 if has_pdas && has_tokens {
-    let cpi_context_of_first_input =
-        compressed_accounts[0].0.tree_info.cpi_context.unwrap();
+    let cpi_context = compressed_accounts
+        .iter()
+        .find_map(|(acc, _)| acc.tree_info.cpi_context)
+        .ok_or("Expected CPI context for PDA inputs but found none")?;
     let system_config = SystemAccountMetaConfig::new_with_cpi_context(
         *program_id,
-        cpi_context_of_first_input,
+        cpi_context,
     );
     remaining_accounts.add_system_accounts_v2(system_config)?;
 } else {

 // pack account data
 let packed_tree_infos_slice = &packed_tree_infos
     .state_trees
     .as_ref()
-    .unwrap()
+    .ok_or("PackedTreeInfos missing state_trees")?
     .packed_tree_infos;

286-358: Remove unused accounts_to_compress parameter and replace remaining unwrap.

The accounts_to_compress parameter (line 292) is accepted but never meaningfully used—only its length is checked against account_pubkeys.len() at lines 296-298. Beyond that validation, no lamports, owner, or other Account data is read or encoded.

Additionally, line 321 unwraps state_trees, which can panic if it's None.

Fix 1: Remove the unused parameter if no validation of Account data is needed:

 pub fn compress_accounts_idempotent(
     program_id: &Pubkey,
     discriminator: &[u8],
     account_pubkeys: &[Pubkey],
-    accounts_to_compress: &[Account],
     program_account_metas: &[AccountMeta],
     validity_proof_with_context: ValidityProofWithContext,
 ) -> Result<Instruction, Box<dyn std::error::Error>> {
-    if account_pubkeys.len() != accounts_to_compress.len() {
-        return Err("Accounts pubkeys length must match accounts length".into());
-    }

Fix 2: Replace the unwrap with a graceful error:

 for packed_tree_info in packed_tree_infos
     .state_trees
     .as_ref()
-    .unwrap()
+    .ok_or("PackedTreeInfos missing state_trees")?
     .packed_tree_infos
     .iter()
sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (1)

338-365: Rent sponsor selection is safer now, but fallback to “first signer” is a bit implicit.

The new actual_rent_sponsor logic is a clear improvement over unwrapping: you prefer the compressible extension’s rent_sponsor, and you now fail with InvalidAccountData if nothing suitable is found.

Two follow‑ups to consider:

  • Using the first signer in packed_accounts as the fallback “fee payer” couples rent semantics to account ordering. Since this function already takes an explicit fee_payer: Pubkey, it might be less error‑prone to use that as the fallback rent sponsor (and rely on find_account_indices to fail if the fee payer account isn’t actually present in packed_accounts), or at least document the assumption that “first signer == intended fee payer”.
  • Right now rent_sponsor_pubkey is always None at the top of the loop, so the if let Some(sponsor) branch is effectively dead. Either set rent_sponsor_pubkey earlier when you do want to override it, or simplify the code to a single path to avoid wasted branching.

Neither is a functional bug today, but tightening this up would make the rent path easier to reason about and harder to misuse if the packed account order changes.

sdk-libs/sdk/src/compressible/compress_account.rs (1)

56-60: Defend against empty address_space

This still indexes address_space[0] without checking the slice length. If a misconfigured caller passes an empty list, we panic before we can emit a constraint error. Let’s fail gracefully by validating the slice before deriving the PDA.

-    let derived_c_pda = derive_address(
-        &account_info.key.to_bytes(),
-        &address_space[0].to_bytes(),
-        &program_id.to_bytes(),
-    );
+    let address_tree = address_space.first().ok_or_else(|| {
+        msg!("prepare_account_for_compression requires a non-empty address_space");
+        LightSdkError::ConstraintViolation
+    })?;
+
+    let derived_c_pda = derive_address(
+        &account_info.key.to_bytes(),
+        &address_tree.to_bytes(),
+        &program_id.to_bytes(),
+    );
sdk-libs/sdk/src/compressible/config.rs (2)

21-41: Be explicit about on-chain compatibility after expanding CompressibleConfig layout

Adding write_top_up, compression_authority, and rent_config (and updating LEN/size_for_address_space) changes the serialized layout of CompressibleConfig. Any accounts created with the previous layout will no longer deserialize correctly with the new struct definition.

If there are already deployed configs, you’ll need either:

  • A version bump + backward-compatible loader that can read both layouts, or
  • A documented migration path that recreates configs with the new layout.

If no configs exist on-chain yet, it’s worth documenting that assumption in a comment to avoid surprises later.

Also applies to: 43-67


281-337: Update path correctly wires new fields; consider whether additional validation is needed

The update routine now allows changing compression_authority, rent_config, and write_top_up in place. The wiring looks correct, but it currently applies no extra validation on these fields beyond what RentConfig itself enforces.

If there are invariants you rely on (e.g., write_top_up nonzero, or bounded within some range), consider enforcing them here or in CompressibleConfig::validate so misconfigured updates can’t brick compression behavior.

♻️ Duplicate comments (24)
sdk-libs/macros/src/compressible/pack_unpack.rs (2)

101-131: Guard against out-of-bounds remaining_accounts indexing in generated Unpack

The generated Unpack for the packed struct still uses direct indexing:

#field_name: *remaining_accounts[self.#field_name as usize].key

If self.#field_name points past remaining_accounts.len(), this will panic instead of returning a ProgramError. Since the generated unpack signature is Result<_, anchor_lang::prelude::ProgramError>, callers expect graceful error handling, especially on-chain.

You can keep the same shape but add a bounds check:

-        } else if is_pubkey_type(field_type) {
-            quote! {
-                #field_name: *remaining_accounts[self.#field_name as usize].key
-            }
+        } else if is_pubkey_type(field_type) {
+            quote! {
+                #field_name: {
+                    let idx = self.#field_name as usize;
+                    let account_info = remaining_accounts
+                        .get(idx)
+                        .ok_or(anchor_lang::prelude::ProgramError::NotEnoughAccountKeys)?;
+                    *account_info.key
+                }
+            }
         }

That converts malformed/truncated account slices into a proper ProgramError instead of aborting the program.

Please re-run your macro-driven programs’ tests (especially decompress paths) after this change to ensure no call sites rely on the old panic behavior, and confirm NotEnoughAccountKeys is the desired error variant here.


7-61: Make derive constraints on generics and Clone explicit (or enforce them)

This derive still:

  • Ignores input.generics, emitting non-generic impl Pack/Unpack for #struct_name and Packed#struct_name even if the source struct is generic.
  • Assumes non-Copy fields implement Clone, emitting self.#field_name.clone().
  • Treats a field literally named compression_info as transient metadata (always set to None on pack/unpack).

All of these are reasonable design choices, but they’re implicit and can lead to confusing macro errors (e.g., applying the derive to struct Foo<T> or to a type with non-Clone fields).

I’d recommend either:

  • Rejecting unsupported shapes up front with a clear syn::Error::new_spanned message (e.g., “derive_compressible_pack does not support generics yet” / “non-Copy fields must implement Clone”), or
  • Properly threading generics and bounds through the generated Packed type and impls.

If you keep the current behavior, please at least document these constraints in the derive’s docs so downstream users know generics aren’t supported and non-Copy fields must be Clone.

Also applies to: 145-186

sdk-libs/program-test/src/compressible.rs (1)

211-235: auto_compress_program_pdas still ignores cfg.compression_authority

Here:

let cfg = CpdaCompressibleConfig::try_from_slice(&cfg_acc.data)?;
let rent_sponsor = cfg.rent_sponsor;
// TODO: add coverage for external compression_authority
let compression_authority = payer.pubkey();

you derive rent_sponsor from the on-chain config but hard-code compression_authority to the payer. That conflicts with the runtime check in process_compress_pda_accounts_idempotent, which compares ctx.compression_authority().key against compression_config.compression_authority. As soon as a program sets a real external compression authority, this helper will silently no-op (transactions will fail and you ignore the error in try_compress_chunk).

Instead, you almost certainly want:

-    let rent_sponsor = cfg.rent_sponsor;
-    // TODO: add coverage for external compression_authority
-    let compression_authority = payer.pubkey();
+    let rent_sponsor = cfg.rent_sponsor;
+    let compression_authority = cfg.compression_authority;

and leave the payer as just the transaction signer.

Once you switch to cfg.compression_authority, please add or run a test where compression_authority != payer.pubkey() to ensure the helper still works and correctly honors the configured authority.

sdk-libs/macros/src/compressible/utils.rs (1)

41-76: Tighten is_copy_type for arrays and centralize Pubkey detection

Two small but non-trivial points:

  1. is_copy_type still treats every Type::Array(_) as Copy:
Type::Array(_) => true,

In Rust, [T; N] is Copy only if T: Copy. This can cause the macros to generate direct copies for arrays of non-Copy types, leading to confusing compile errors instead of falling back to .clone(). A more accurate heuristic is:

-        Type::Array(_) => true,
+        Type::Array(array) => {
+            // Arrays are Copy iff their element type is Copy.
+            is_copy_type(&array.elem)
+        }
  1. You now have a shared is_pubkey_type here, but seed_providers.rs still defines its own is_pubkey_type with slightly different logic. Keeping two implementations invites drift. Making this the canonical helper (and using it from seed_providers.rs) would ensure consistent behavior across all compressible macros.

After changing Type::Array handling, please re-run your macro-using crates to confirm that any arrays-of-non-Copy types now fail with clearer errors (or compile successfully when you expect them to be cloned).

Also applies to: 93-105

sdk-libs/macros/src/compressible/instructions.rs (3)

61-131: TokenSeedSpec still mishandles seeds starting with identifier expressions (ctx.*, data.*, etc.)

The parser now special-cases the first identifier inside (...) and, in the default arm, synthesizes a SeedElement::Expression from just that ident:

let first_ident: Ident = content.parse()?;
match first_ident.to_string().as_str() {
    "is_token" | "true" | "is_pda" | "false" => { /* … */ }
    _ => {
        // builds an ExprPath from `first_ident` only
        seeds.push(SeedElement::Expression(Box::new(syn::Expr::Path(...))));
        // may or may not parse the rest
    }
}

For specs like:

MyPda = (ctx.accounts.user)
MyPda = (data.bump)

this drops everything after the first ident (.accounts.user, .bump), so the seed expression becomes just ctx or data. That’s exactly the class of bugs you wanted to fix: seed expressions that start with an identifier but are not control keywords.

The safer approach is to peek for control keywords and otherwise delegate the entire parenthesized content to parse_seeds_with_authority, which already handles arbitrary SeedElement::Expression correctly. Conceptually:

-        let (is_token, is_ata, seeds, authority) = if content.peek(Ident) {
-            let first_ident: Ident = content.parse()?;
-
-            match first_ident.to_string().as_str() {
+        let (is_token, is_ata, seeds, authority) = if content.peek(Ident) {
+            let lookahead: Ident = content.fork().parse()?;
+
+            match lookahead.to_string().as_str() {
                 "is_token" => { /* consume ident, comma, then parse seeds */ }
                 "true" => { /* … */ }
                 "is_pda" | "false" => { /* … */ }
-                _ => {
-                    // custom logic that only sees the first ident and can drop the rest
-                }
+                _ => {
+                    // No control keyword: treat the *whole* content as seeds (+ optional authority).
+                    let (seeds, authority) = parse_seeds_with_authority(&content)?;
+                    (None, false, seeds, authority)
+                }
             }
         } else {
             let (seeds, authority) = parse_seeds_with_authority(&content)?;
             (None, false, seeds, authority)
         };

That way, expressions like ctx.accounts.user, data.bump, or max_key(ctx.accounts.user, data.bump) are preserved intact and later remapped by generate_pda_seed_derivation_for_trait.

After adjusting this logic, please add or run a macro test that uses seeds like (ctx.accounts.user, data.bump) and confirm the generated seed derivation code matches the intended expressions.


424-461: PDA seed lookup compares Ident to String and is likely to fail to compile

Here:

let name_str = name.to_string();
let spec = if let Some(ref pda_seed_specs) = pda_seeds {
    pda_seed_specs
        .iter()
        .find(|s| s.variant == name_str)
        .ok_or_else(|| macro_error!(...))?
} else { ... };

s.variant is a syn::Ident, and name_str is a String. syn::Ident doesn’t implement PartialEq<String> in general, so this comparison is very likely a compile error. Even if it compiled by accident, you’re comparing mismatched types.

You only need to match by ident; using name directly is simpler and type-safe:

-            let name_str = name.to_string();
+            let name_str = name.to_string();
             let spec = if let Some(ref pda_seed_specs) = pda_seeds {
                 pda_seed_specs
                     .iter()
-                    .find(|s| s.variant == name_str)
+                    .find(|s| s.variant == *name)
                     .ok_or_else(|| {
                         macro_error!(
                             name,
                             "No seed specification for account type '{}'. All accounts must have seed specifications.",
                             name_str
                         )
                     })?

This keeps the nice error message while comparing Ident to Ident.

Please run cargo check -p sdk-macros (or equivalent) after this change to confirm the macros crate compiles cleanly and that account types without matching seed specs still produce the intended error.


405-415: Reject unsupported InstructionVariants instead of panicking with unreachable!(); update rent sponsor doc

You still construct InstructionVariant::PdaOnly and TokenOnly:

let instruction_variant = match (has_pda_seeds, has_token_seeds) {
    (true, true) => InstructionVariant::Mixed,
    (true, false) => InstructionVariant::PdaOnly,
    (false, true) => InstructionVariant::TokenOnly,
    (false, false) => { /* error */ }
};

but downstream helpers like generate_decompress_accounts_struct and generate_error_codes treat those variants as unreachable!(). If a user invokes the macro with only PDA or only token seed specs, macro expansion will panic with an opaque backtrace instead of a clear compile error.

If Mixed is the only supported mode today, reject the others up front:

-    let instruction_variant = match (has_pda_seeds, has_token_seeds) {
-        (true, true) => InstructionVariant::Mixed,
-        (true, false) => InstructionVariant::PdaOnly,
-        (false, true) => InstructionVariant::TokenOnly,
-        (false, false) => {
-            return Err(macro_error!(...));
-        }
-    };
+    let instruction_variant = match (has_pda_seeds, has_token_seeds) {
+        (true, true) => InstructionVariant::Mixed,
+        (true, false) | (false, true) => {
+            return Err(macro_error!(
+                &module,
+                "PDA-only or token-only compression is not implemented yet. \
+                 Provide both PDA and token seed specifications (Mixed variant)."
+            ));
+        }
+        (false, false) => {
+            return Err(macro_error!(
+                &module,
+                "At least one PDA or token seed specification must be provided"
+            ));
+        }
+    };

and leave generate_decompress_accounts_struct / generate_error_codes handling only the Mixed case.

Separately, in the generated DecompressAccountsIdempotent struct you still document:

/// CHECK: anyone can pay
#[account(mut)]
pub rent_sponsor: UncheckedAccount<'info>

but the runtime checks in compress_runtime::process_compress_pda_accounts_idempotent enforce that ctx.rent_sponsor().key == compression_config.rent_sponsor, i.e., this is the program’s configured rent sponsor PDA, not an arbitrary payer. The doc comment should reflect that constraint to avoid misleading downstream users.

After tightening the variant handling, please add a small macro test that uses only PDA seeds or only token seeds to verify you now get a clear compile-time error instead of an unreachable!() panic.

Also applies to: 1431-1491, 1545-1576

sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (1)

407-418: Drop the unused 'b lifetime instead of suppressing Clippy.

compress_and_close_ctoken_accounts_signed<'b, 'info> doesn’t use 'b anywhere in its parameters or body, so clippy::extra_unused_lifetimes is rightly complaining. Rather than allowing the lint, you can simplify the signature:

-#[allow(clippy::extra_unused_lifetimes)]
-pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>(
+pub fn compress_and_close_ctoken_accounts_signed<'info>(

This keeps the API cleaner and removes the need for the allowance.

sdk-libs/macros/src/compressible/README.md (1)

1-45: Docs are clear; an end‑to‑end example would still be a nice follow‑up.

This overview maps the modules well. As a later improvement, a tiny end‑to‑end snippet (derive Compressible on a PDA + add_compressible_instructions usage + expected seeds/accounts) would help new consumers connect the pieces without hunting through source.

sdk-libs/macros/src/compressible/GUIDE.md (1)

116-126: Clarify PDA rent flow vs fee payer to match current implementation semantics.

In the PDA rent section you say:

Funds on close/compress go to rent_sponsor (completed epochs) and refund fee payer for partial epochs automatically.

The PR objectives describe c‑PDA rent as always flowing to the program’s rent_sponsor PDA, even when a different fee payer funded rent exemption, with that fee effectively subsidizing the next user’s PDA rent. If that’s the current behavior, this paragraph should be updated to:

  • Explicitly state that rent for program PDAs is captured by the program’s rent sponsor (and whether any part is ever refunded), and
  • Clearly distinguish PDA behavior from cToken account behavior, so integrators don’t rely on refunds that never occur on‑chain.

Can you confirm the intended semantics and align this section accordingly?

sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)

12-32: Token decompression runtime now guards indices and optional CPI context correctly

The updated process_decompress_tokens_runtime addresses the earlier critical concerns:

  • Validates mint_index and owner_index against packed_accounts.len() and returns ProgramError::InvalidAccountData instead of panicking.
  • Computes cpi_context_pubkey only when has_pdas is true, so a missing CPI context no longer fails the pure‑token path.
  • Uses plain Vec for token_decompress_indices and signer seeds, avoiding unnecessary boxing while keeping the API straightforward.

The remaining get_authority_seeds method on CTokenSeedProvider is only an API‑surface TODO and doesn’t affect correctness of this runtime path.

Also applies to: 37-187

sdk-libs/macros/src/compressible/decompress_context.rs (1)

106-116: Add bounds check before indexing solana_accounts[i] to avoid panics in generated code

Inside the generated collect_pda_and_token, the loop assumes compressed_accounts.len() == solana_accounts.len() and directly indexes:

for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
    let meta = compressed_data.meta;
    match compressed_data.data {
        CompressedAccountVariant::#packed_name(packed) => {
            light_sdk::compressible::handle_packed_pda_variant::<...>(
                &*self.rent_sponsor,
                cpi_accounts,
                address_space,
                &solana_accounts[i],
                i,
                ...

If these slices ever diverge (mis‑wired accounts, future refactors, or non‑macro callers of DecompressContext::collect_pda_and_token), this will panic on‑chain. The non‑macro runtime already uses .get(..).ok_or_else(|| ConstraintViolation) in similar situations; the macro‑generated path should be equally defensive.

You can cheaply guard before indexing:

-                for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
-                    let meta = compressed_data.meta;
+                for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
+                    if i >= solana_accounts.len() {
+                        return std::result::Result::Err(
+                            solana_program_error::ProgramError::InvalidAccountData,
+                        );
+                    }
+                    let meta = compressed_data.meta;
                     match compressed_data.data {

This keeps behavior unchanged in the happy path while ensuring bad inputs produce a clear ProgramError instead of an abort.

Also applies to: 132-143

sdk-libs/macros/src/compressible/traits.rs (1)

86-129: Treat compression_info as a reserved field in generated impls to avoid duplicate init and size double‑counting

For structs with a compression_info field, the current generators have two coupled problems:

  1. Duplicate field initialization in CompressAs
    generate_compress_as_impl always emits:

    Self {
        compression_info: None,
        #(#field_assignments)*
    }

    but generate_compress_as_field_assignments iterates all fields except those with #[skip], so it will also emit an assignment for compression_info. The result is a struct literal with compression_info specified twice, which fails to compile.

  2. Double‑counting in Size
    generate_size_impl adds an explicit compression_info_size constant:

    let compression_info_size =
        1 + <light_sdk::compressible::CompressionInfo as light_sdk::compressible::Space>::INIT_SPACE;
    compression_info_size #(#size_fields)*

    but generate_size_fields also includes compression_info in the try_to_vec().len() sum, so its bytes are counted twice.

To fix both issues, reserve compression_info for the dedicated logic and skip it in the generic field loops, and ensure derive_compress_as validates its presence:

fn generate_compress_as_field_assignments(
    fields: &Punctuated<Field, Token![,]>,
    compress_as_fields: &Option<CompressAsFields>,
) -> Vec<TokenStream> {
    let mut field_assignments = Vec::new();

    for field in fields {
        let field_name = field.ident.as_ref().unwrap();
        let field_type = &field.ty;

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is handled explicitly in `generate_compress_as_impl`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
@@
fn generate_size_fields(fields: &Punctuated<Field, Token![,]>) -> Vec<TokenStream> {
    let mut size_fields = Vec::new();

    for field in fields.iter() {
        let field_name = field.ident.as_ref().unwrap();

-        if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) {
+        // `compression_info` is already accounted for via `compression_info_size`
+        if field_name == "compression_info"
+            || field.attrs.iter().any(|attr| attr.path().is_ident("skip"))
+        {
             continue;
         }
@@
pub fn derive_compress_as(input: ItemStruct) -> Result<TokenStream> {
    let struct_name = &input.ident;
    let fields = extract_fields_from_item_struct(&input)?;

+    // Ensure the struct actually has the expected `compression_info` field
+    validate_compression_info_field(fields, struct_name)?;
@@
-    let field_assignments = generate_compress_as_field_assignments(fields, &compress_as_fields);
-    Ok(generate_compress_as_impl(struct_name, &field_assignments))
+    let field_assignments = generate_compress_as_field_assignments(fields, &compress_as_fields);
+    Ok(generate_compress_as_impl(struct_name, &field_assignments))
}

This keeps compression_info under explicit control (both for initialization and for sizing), prevents compile‑time field duplication, and brings the Size behavior in line with the comments about always allocating space for a Some(CompressionInfo).

Also applies to: 150-181, 192-209

sdk-libs/sdk/src/compressible/compress_runtime.rs (1)

46-54: Replace opaque ProgramError::Custom(0) with a structured constraint violation error

Config validation currently does:

if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
    return Err(ProgramError::Custom(0));
}
if *ctx.compression_authority().key != compression_config.compression_authority {
    return Err(ProgramError::Custom(0));
}

Using Custom(0) makes it hard to distinguish this failure from other custom‑0 usages and is inconsistent with the rest of the compressible module, where configuration/constraint mismatches map to LightSdkError::ConstraintViolation and then into a well‑defined ProgramError code (as in config::load_checked).

Recommend aligning these branches with that convention:

-    if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
-        return Err(ProgramError::Custom(0));
-    }
-    if *ctx.compression_authority().key != compression_config.compression_authority {
-        return Err(ProgramError::Custom(0));
-    }
+    if *ctx.rent_sponsor().key != compression_config.rent_sponsor
+        || *ctx.compression_authority().key != compression_config.compression_authority
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

This keeps the on‑chain ABI consistent with other compression checks and gives clients a meaningful, documented error code.

sdk-libs/macros/src/compressible/seed_providers.rs (2)

81-161: Centralize “standard account field” detection to avoid drift between branches

Both the token seed and authority seed handling duplicate the same is_standard_field match on "fee_payer" | "rent_sponsor" | "config" | "compression_authority" in multiple places (Lines 95–101, 132–138, 213–219). This repetition makes it easy to forget a branch when adding/removing a standard field.

Consider factoring this into a small helper (e.g. fn is_standard_account_field(name: &str) -> bool) and reusing it in all branches (ctx.accounts., ctx. and authority) so behavior stays in sync when the standard set evolves.

Also applies to: 183-283


871-882: Reuse shared is_pubkey_type helper instead of redefining it here

This file defines its own is_pubkey_type that differs from compressible::utils::is_pubkey_type. Maintaining two implementations increases the chance of subtle divergence in how “pubkey-like” types are treated when inferring client parameters from instruction_data.

Recommend deleting this local function and importing the canonical helper from compressible::utils so all macro entrypoints share one definition of what counts as a pubkey type.

sdk-libs/sdk/src/compressible/compression_info.rs (3)

139-184: Guard against rent_per_epoch == 0 to avoid on-chain panics

calculate_top_up_lamports divides by rent_per_epoch from rent_config.rent_curve_per_epoch(num_bytes) (Line 175). If a misconfigured RentConfig ever yields 0 here, this will panic and abort the transaction instead of returning a clear error.

Given RentConfig is on-chain data, I’d add an explicit guard before the division:

let rent_per_epoch = self.rent_config.rent_curve_per_epoch(num_bytes);
if rent_per_epoch == 0 {
    // Either treat as no top-up or return a ProgramError / custom LightSdkError
    return 0;
}
let epochs_funded_ahead = available_balance / rent_per_epoch;

(or map it to a meaningful error) so bad config can’t trigger a division-by-zero panic.


254-320: Align claim_completed_epoch_rent return value with its documentation

The docstring states:

Returns Some(claimed) if any lamports were claimed; None if account is compressible or nothing to claim.

The implementation returns:

  • Ok(None) when the account is compressible (early return), but
  • Ok(Some(0)) when claimable is None or Some(0) (Line 319).

That makes “nothing to claim” indistinguishable from a real 0‑lamport payout and contradicts the docs.

Consider changing the final line to:

    Ok(None)

so callers can reliably distinguish “no claim performed” from “some amount was claimed”, or else update the docs to say that Some(0) is the “no claimable rent” case and None is reserved for compressible accounts only.


330-362: Fix System Program ID in transfer_lamports_cpi (currently unusable CPI)

transfer_lamports_cpi constructs its instruction with a zeroed program ID:

const SYSTEM_PROGRAM_ID: [u8; 32] = [0, 0, ..., 0];

let transfer_instruction = Instruction {
    program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
    // ...
};

This will not match the real System Program (11111111111111111111111111111111), so the CPI will fail whenever top_up_rent tries to transfer lamports.

You already receive system_program: &AccountInfo as a parameter. Use that instead:

-    // System Program ID
-    const SYSTEM_PROGRAM_ID: [u8; 32] = [0; 32];
-
-    let transfer_instruction = Instruction {
-        program_id: Pubkey::from(SYSTEM_PROGRAM_ID),
+    let transfer_instruction = Instruction {
+        program_id: *system_program.key,
         accounts: vec![
             AccountMeta::new(*from.key, true),
             AccountMeta::new(*to.key, false),
         ],
         data: instruction_data,
     };

Optionally, you can also assert that *system_program.key equals the known system program ID and return a clear error if not.

sdk-libs/macros/src/lib.rs (3)

256-261: DataHasher derive does not actually alias LightHasher as documented

The doc says DataHasher is an alias of LightHasher, but the implementation still delegates to derive_light_hasher_sha:

#[proc_macro_derive(DataHasher, attributes(skip, hash))]
pub fn data_hasher(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemStruct);
    into_token_stream(derive_light_hasher_sha(input))
}

If LightHasher is Poseidon‑based and LightHasherSha is SHA‑based, this is a real behavioral difference.

Either:

  • Switch this to derive_light_hasher(input) so it truly aliases LightHasher, or
  • Update the docs to say that DataHasher aliases the SHA variant, not LightHasher.

Right now the API contract and implementation disagree.


263-295: Allow #[skip] on HasCompressionInfo as the docs and examples show

The HasCompressionInfo derive is documented and exemplified with:

#[derive(HasCompressionInfo)]
pub struct UserRecord {
    #[skip]
    pub compression_info: Option<CompressionInfo>,
    // ...
}

but the proc-macro declaration is:

#[proc_macro_derive(HasCompressionInfo)]
pub fn has_compression_info(input: TokenStream) -> TokenStream { ... }

Without attributes(skip), the compiler rejects #[skip] with “cannot find attribute skip in this scope”.

Change the declaration to:

#[proc_macro_derive(HasCompressionInfo, attributes(skip))]

so users can follow the documented pattern without hitting an attribute error.


153-180: Large commented-out macro implementations add noise; either remove or replace with a TODO

The commented-out LightDiscriminatorSha and add_native_compressible_instructions blocks are sizable and unlikely to be re-enabled as-is. Keeping dead code like this around makes the file harder to navigate.

Either:

  • Delete these blocks entirely if the features are no longer planned, or
  • Replace them with a short // TODO(#issue): Re-enable LightDiscriminatorSha / add_native_compressible_instructions comment that points to a tracking issue.

This keeps the macro surface clearer without losing design intent.

Also applies to: 374-397

sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)

239-242: Decompress path should enforce config’s rent-sponsor/authority invariants, like compress path does

process_decompress_accounts_idempotent loads CompressibleConfig and uses its address_space, but never verifies that:

  • ctx.rent_sponsor().key equals compression_config.rent_sponsor, and/or
  • any configured compression authority matches the context (if you intend that invariant).

On the compress side, similar checks are enforced to ensure the runtime uses the protocol-defined rent sponsor and authority, not arbitrary accounts.

To keep behavior symmetric and avoid accidentally funding decompressed PDAs from the wrong account, consider adding immediately after load_checked:

let compression_config =
    crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
    return Err(ProgramError::from(crate::error::LightSdkError::ConstraintViolation));
}
// Optionally mirror any compression_authority check you enforce on the compress side

This makes it harder for callers to bypass your configured rent-sponsor/authority policy on the decompression path.

Also applies to: 290-295, 326-355


318-324: Resolve or track the // TODO: fix this branch for non-cpi-context builds

The non-cpi-context PDA decompression path still has a bare // TODO: fix this above the LightSystemProgramCpi::new_cpi(...).invoke(cpi_accounts.clone()) call.

If this path is production‑relevant, it would be good to either:

  • Clarify what’s incorrect and address it now, or
  • Replace the comment with a more specific TODO referencing a tracking issue.

As-is, it signals known-but-unspecified breakage without giving future readers enough context.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f30f02 and a125346.

⛔ Files ignored due to path filters (49)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • scripts/lint.sh is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/processor.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/src/variant.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/tests/user_record_tests.rs is excluded by none and included by none
📒 Files selected for processing (41)
  • sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/compressible/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressible-client/src/lib.rs (9 hunks)
  • sdk-libs/macros/Cargo.toml (1 hunks)
  • sdk-libs/macros/src/compressible/GUIDE.md (1 hunks)
  • sdk-libs/macros/src/compressible/README.md (1 hunks)
  • sdk-libs/macros/src/compressible/decompress_context.rs (1 hunks)
  • sdk-libs/macros/src/compressible/instructions.rs (1 hunks)
  • sdk-libs/macros/src/compressible/mod.rs (1 hunks)
  • sdk-libs/macros/src/compressible/pack_unpack.rs (1 hunks)
  • sdk-libs/macros/src/compressible/seed_providers.rs (1 hunks)
  • sdk-libs/macros/src/compressible/traits.rs (1 hunks)
  • sdk-libs/macros/src/compressible/utils.rs (1 hunks)
  • sdk-libs/macros/src/compressible/variant_enum.rs (1 hunks)
  • sdk-libs/macros/src/cpi_signer.rs (1 hunks)
  • sdk-libs/macros/src/lib.rs (7 hunks)
  • sdk-libs/macros/src/rent_sponsor.rs (1 hunks)
  • sdk-libs/macros/src/utils.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (1 hunks)
  • sdk-libs/program-test/src/compressible.rs (5 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (3 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
  • sdk-libs/sdk-types/src/constants.rs (1 hunks)
  • sdk-libs/sdk-types/src/lib.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (2 hunks)
  • sdk-libs/sdk/src/compressible/close.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (2 hunks)
  • sdk-libs/sdk/src/compressible/compress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (4 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (12 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (7 hunks)
  • sdk-libs/sdk/src/compressible/decompress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (2 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
  • sdk-libs/sdk/src/utils.rs (2 hunks)
  • sdk-libs/token-client/Cargo.toml (0 hunks)
💤 Files with no reviewable changes (1)
  • sdk-libs/token-client/Cargo.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-14T00:36:53.191Z
Learnt from: ananas-block
Repo: Lightprotocol/light-protocol PR: 1909
File: program-libs/zero-copy/src/init_mut.rs:241-249
Timestamp: 2025-08-14T00:36:53.191Z
Learning: In the light-protocol zero-copy crate, performance is prioritized over safety checks for edge cases like Vec lengths exceeding u32::MAX, even when there might be wire format inconsistencies.

Applied to files:

  • sdk-libs/macros/src/compressible/variant_enum.rs
  • sdk-libs/macros/src/compressible/pack_unpack.rs
  • sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs
🧬 Code graph analysis (26)
sdk-libs/program-test/src/program_test/test_rpc.rs (1)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
  • new (68-382)
sdk-libs/compressed-token-sdk/src/compressible/mod.rs (1)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/compressible/variant_enum.rs (2)
sdk-libs/macros/src/compressible/decompress_context.rs (2)
  • parse (16-20)
  • parse (28-32)
sdk-libs/sdk/src/compressible/compression_info.rs (6)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
  • pack (17-17)
  • unpack (23-26)
sdk-libs/macros/src/compressible/decompress_context.rs (4)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (11)
  • compressible (204-204)
  • handle_packed_pda_variant (150-221)
  • rent_sponsor (67-67)
  • fee_payer (65-65)
  • config (66-66)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
  • collect_pda_and_token (78-88)
  • process_tokens (94-108)
sdk-libs/sdk-types/src/cpi_accounts/v2.rs (1)
  • system_accounts_end_offset (161-173)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/lib.rs (1)
  • derive_decompress_context (562-567)
sdk-libs/macros/src/compressible/pack_unpack.rs (2)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • is_copy_type (45-76)
  • is_pubkey_type (95-105)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • insert_or_get (301-303)
sdk-libs/macros/src/utils.rs (2)
sdk-libs/macros/src/compressible/instructions.rs (1)
  • syn (303-303)
sdk-libs/macros/src/compressible/variant_enum.rs (1)
  • syn (22-22)
sdk-libs/macros/src/rent_sponsor.rs (3)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_rent_sponsor_pda (615-617)
  • derive_light_rent_sponsor (632-634)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)
  • rent_sponsor (67-67)
sdk-libs/compressed-token-sdk/src/lib.rs (2)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (1)
  • compressible (204-204)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • last_claimed_slot (126-128)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • compressible (204-204)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/macros/src/compressible/utils.rs (1)
  • is_pubkey_type (95-105)
sdk-libs/program-test/src/program_test/compressible_setup.rs (1)
sdk-libs/compressible-client/src/lib.rs (2)
  • initialize_compression_config (80-127)
  • update_compression_config (130-164)
sdk-libs/sdk/src/lib.rs (2)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_rent_sponsor (632-634)
  • derive_light_rent_sponsor_pda (615-617)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor (133-187)
  • derive_light_rent_sponsor_pda (83-124)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • fee_payer (65-65)
  • config (66-66)
  • rent_sponsor (67-67)
  • compressible (204-204)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-43)
sdk-libs/macros/src/cpi_signer.rs (2)
sdk-libs/macros/src/lib.rs (2)
  • derive_light_cpi_signer_pda (581-583)
  • derive_light_cpi_signer (598-600)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_from_config (89-100)
sdk-libs/sdk/src/compressible/mod.rs (3)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • process_compress_pda_accounts_idempotent (28-117)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • compression_info (37-37)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • check_account_types (132-145)
  • handle_packed_pda_variant (150-221)
  • process_decompress_accounts_idempotent (226-358)
sdk-libs/program-test/src/compressible.rs (4)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • config (12-12)
  • rent_sponsor (13-13)
  • compression_authority (14-14)
program-libs/compressible/src/config.rs (1)
  • new_ctoken (163-182)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (70-77)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (288-358)
sdk-libs/macros/src/compressible/traits.rs (2)
sdk-libs/macros/src/compressible/utils.rs (3)
  • extract_fields_from_derive_input (26-39)
  • extract_fields_from_item_struct (11-21)
  • is_copy_type (45-76)
sdk-libs/sdk/src/compressible/compression_info.rs (4)
  • compression_info (37-37)
  • compression_info_mut (38-38)
  • compression_info_mut_opt (39-39)
  • set_compression_info_none (40-40)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • ctoken_program (69-69)
  • ctoken_rent_sponsor (68-68)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
sdk-libs/compressed-token-sdk/src/ctoken/create_token_account/instruction.rs (2)
  • create_token_account (92-118)
  • create_ctoken_account_signed (122-171)
sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs (1)
  • decompress_full_ctoken_accounts_with_indices (43-125)
sdk-libs/macros/src/compressible/utils.rs (2)
sdk-libs/macros/src/accounts.rs (1)
  • type_path (494-502)
sdk-libs/macros/src/compressible/seed_providers.rs (1)
  • is_pubkey_type (871-882)
sdk-libs/sdk/src/compressible/config.rs (2)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • compression_authority (14-14)
  • rent_sponsor (13-13)
  • config (12-12)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • rent_sponsor (67-67)
  • config (66-66)
sdk-libs/macros/src/compressible/instructions.rs (7)
sdk-libs/macros/src/compressible/variant_enum.rs (3)
  • syn (22-22)
  • parse (14-18)
  • compressed_account_variant (21-247)
sdk-libs/macros/src/compressible/decompress_context.rs (3)
  • parse (16-20)
  • parse (28-32)
  • generate_decompress_context_trait_impl (35-185)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (14)
  • compressible (204-204)
  • derive_pda_seeds_with_accounts (122-127)
  • handle_packed_pda_variant (150-221)
  • rent_sponsor (67-67)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
  • collect_pda_and_token (78-88)
  • fee_payer (65-65)
  • config (66-66)
  • process_decompress_accounts_idempotent (226-358)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
  • generate_ctoken_account_variant_enum (9-25)
  • generate_ctoken_seed_provider_implementation (27-363)
  • generate_client_seed_functions (366-499)
sdk-libs/macros/src/compressible/utils.rs (1)
  • generate_empty_ctoken_enum (110-116)
sdk-libs/sdk/src/compressible/compress_runtime.rs (6)
  • rent_sponsor (13-13)
  • fee_payer (11-11)
  • config (12-12)
  • compression_authority (14-14)
  • compress_pda_account (16-23)
  • process_compress_pda_accounts_idempotent (28-117)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
  • prepare_account_for_compression (29-136)
sdk-libs/sdk/src/compressible/compression_info.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • compressible (204-204)
  • rent_sponsor (67-67)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/program-test/src/compressible.rs (1)
  • new (53-73)
sdk-libs/macros/src/lib.rs (7)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (187-226)
sdk-libs/macros/src/cpi_signer.rs (2)
  • derive_light_cpi_signer_pda (7-42)
  • derive_light_cpi_signer (44-91)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (83-124)
  • derive_light_rent_sponsor (133-187)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (5)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • fee_payer (11-11)
  • config (12-12)
  • rent_sponsor (13-13)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • unpack (23-26)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • prepare_account_for_decompression_idempotent (82-148)
  • into_compressed_meta_with_address (25-44)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • compressible (204-204)
  • rent_sponsor (67-67)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_decompressed (104-112)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...

Comment on lines +1183 to +1291
fn generate_pda_seed_derivation_for_trait(
spec: &TokenSeedSpec,
_instruction_data: &[InstructionDataSpec],
) -> Result<TokenStream> {
let mut bindings: Vec<TokenStream> = Vec::new();
let mut seed_refs = Vec::new();

// Recursively rewrite expressions:
// - `data.<field>` -> `seed_params.<field>` (from instruction params, not struct fields!)
// - `ctx.accounts.<account>` -> `accounts.<account>`
// - `ctx.<field>` -> `accounts.<field>`
// While preserving function/method calls and references.
fn map_pda_expr_to_params(expr: &syn::Expr) -> syn::Expr {
match expr {
syn::Expr::Field(field_expr) => {
if let syn::Member::Named(field_name) = &field_expr.member {
// Handle nested field access: ctx.accounts.field_name -> accounts.field_name.as_ref().ok_or(...)?.key()
if let syn::Expr::Field(nested_field) = &*field_expr.base {
if let syn::Member::Named(base_name) = &nested_field.member {
if base_name == "accounts" {
if let syn::Expr::Path(path) = &*nested_field.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "ctx" {
return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError {
let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into();
err.into()
})?.key() };
}
}
}
}
}
}
// Handle direct field access
if let syn::Expr::Path(path) = &*field_expr.base {
if let Some(segment) = path.path.segments.first() {
if segment.ident == "data" {
// data.field -> seed_params.field (from instruction params!)
return syn::parse_quote! { seed_params.#field_name };
} else if segment.ident == "ctx" {
// ctx.field -> accounts.field.as_ref().ok_or(...)?.key() (error if optional account is missing)
return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError {
let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into();
err.into()
})?.key() };
}
}
}
}
expr.clone()
}
syn::Expr::MethodCall(method_call) => {
// Special case: ctx.accounts.account_name.key() -> accounts.account_name.key()
// This is already handled by the Field case transforming ctx.accounts.X -> accounts.X
let mut new_method_call = method_call.clone();
new_method_call.receiver = Box::new(map_pda_expr_to_params(&method_call.receiver));
new_method_call.args = method_call
.args
.iter()
.map(map_pda_expr_to_params)
.collect();
syn::Expr::MethodCall(new_method_call)
}
syn::Expr::Call(call_expr) => {
// Map function args recursively. We do not transform the function path.
let mut new_call_expr = call_expr.clone();
new_call_expr.args = call_expr.args.iter().map(map_pda_expr_to_params).collect();
syn::Expr::Call(new_call_expr)
}
syn::Expr::Reference(ref_expr) => {
let mut new_ref_expr = ref_expr.clone();
new_ref_expr.expr = Box::new(map_pda_expr_to_params(&ref_expr.expr));
syn::Expr::Reference(new_ref_expr)
}
_ => {
// For other expressions (constants, literals, paths), leave as-is
expr.clone()
}
}
}

for (i, seed) in spec.seeds.iter().enumerate() {
match seed {
SeedElement::Literal(lit) => {
let value = lit.value();
seed_refs.push(quote! { #value.as_bytes() });
}
SeedElement::Expression(expr) => {
if let syn::Expr::Path(path_expr) = &**expr {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
seed_refs.push(quote! { #ident.as_bytes() });
continue;
}
}
}

// Generic solution: rewrite any `data.*` occurrences recursively to `self.*`,
// then bind the result to a local to ensure lifetimes are valid,
// and use `.as_ref()` to convert into a seed `&[u8]`.
let binding_name =
syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site());
let mapped_expr = map_pda_expr_to_params(expr);
bindings.push(quote! {
let #binding_name = #mapped_expr;
});
seed_refs.push(quote! { (#binding_name).as_ref() });
}
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 | 🟡 Minor

🧩 Analysis chain

ctx.<field> translation in PDA seed derivation assumes Option and can mis-handle standard accounts

generate_pda_seed_derivation_for_trait’s mapper correctly handles ctx.accounts.<account> by rewriting to:

accounts.<account>.as_ref().ok_or_else(/* MissingSeedAccount */)?.key()

which matches the generated Option<UncheckedAccount<'info>> fields.

However, the ctx.<field> fast-path uses the same pattern:

// ctx.field -> accounts.field.as_ref().ok_or(...)? .key()
return syn::parse_quote! {
    accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError { ... })?.key()
};

This assumes every ctx.<field> in seeds corresponds to an Option<_> in DecompressAccountsIdempotent, which isn’t true for standard accounts like fee_payer: Signer<'info> and config: AccountInfo<'info>. If someone writes a seed like ctx.fee_payer or ctx.config, the generated code will try to call .as_ref() on a non-Option field and fail to compile.

Two options:

  • Restrict ctx.<field> usage in docs to only optional seed accounts and treat ctx.accounts.<account> as the canonical way to reference account fields in seeds, or
  • Make the mapper conditional: if field_name is one of the known optional fields, use .as_ref().ok_or_else(...), otherwise call .key() directly on accounts.#field_name (or on accounts.#field_name.to_account_info() for Signer).

Either way, clarifying and aligning the mapping with the actual account field types will prevent surprising macro expansion errors for legitimate seeds.

If you decide to support ctx.fee_payer / ctx.config as seeds, please add tests that exercise those forms and confirm the generated code compiles and derives the expected PDA.


I'll help you validate this review comment. Let me examine the codebase to verify whether the concern about ctx.<field> translation is accurate.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the DecompressAccountsIdempotent struct definition to check account field types
rg -n "struct DecompressAccountsIdempotent" --type rust -A 30

Length of output: 4836


🏁 Script executed:

#!/bin/bash
# Search for usages of ctx. in seed expressions
rg -n "ctx\." sdk-libs/macros/src/compressible/instructions.rs -B 2 -A 2

Length of output: 3354


🏁 Script executed:

#!/bin/bash
# Look for test files related to PDA seed derivation
fd -e rs -path "*/test*" | xargs rg -l "pda.*seed\|seed.*pda" --type rust 2>/dev/null | head -20

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Check the exact context of the ctx.<field> mapping code
sed -n '1210,1245p' sdk-libs/macros/src/compressible/instructions.rs

Length of output: 1990


Let me dig deeper into how the account fields are generated and whether they're always optional.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for where account_fields is populated/constructed
rg -n "account_fields" sdk-libs/macros/src/compressible/instructions.rs -B 5 -A 5

Length of output: 1974


🏁 Script executed:

#!/bin/bash
# Look for tests that use ctx.<field> in seed expressions
fd -e rs | xargs rg -l "seed" | head -10

Length of output: 482


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or comments about field optionality
rg -n "optional\|Option" sdk-libs/macros/src/compressible/instructions.rs | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for seed usage patterns in instruction generation
rg -n "seed\|pda" sdk-libs/macros/src/compressible/instructions.rs | grep -i "ctx\." | head -20

Length of output: 0


Let me check if there are any actual test cases or examples that exercise seed derivation with ctx.<field>:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for examples or tests that show seed derivation usage
rg -n "#\[seed" sdk-libs/macros/src/ -A 3 -B 1

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for PDA seed specifications in tests
rg -n "pda_seed\|Seed\|seed:" --type rust | grep -i "ctx\|test" | head -15

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's CLAUDE.md or documentation about the macro
find . -name "CLAUDE.md" -o -name "README.md" | grep -i macro | head -5

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Get the full function signature and see how seeds are specified
sed -n '1183,1200p' sdk-libs/macros/src/compressible/instructions.rs

Length of output: 944


Let me check the macros README for documentation about seed derivation:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the macro documentation for seed usage patterns
cat sdk-libs/macros/src/compressible/README.md | head -100

Length of output: 1467


🏁 Script executed:

#!/bin/bash
# Also check if there's any inline documentation in the function
sed -n '1183,1295p' sdk-libs/macros/src/compressible/instructions.rs | head -60

Length of output: 3631


Fix the ctx.<field> mapping to check field optionality before applying .as_ref() semantics

The mapper at lines 1223–1228 unconditionally applies .as_ref().ok_or_else() to all ctx.<field> accesses, but the generated DecompressAccountsIdempotent struct contains both optional and non-optional fields:

  • Non-optional: fee_payer: Signer<'info>, config: AccountInfo<'info>, rent_sponsor: UncheckedAccount<'info>
  • Optional: User-required seed accounts (line 1512): Option<UncheckedAccount<'info>>

Calling .as_ref() on Signer or AccountInfo will fail to compile since those types don't implement Option's interface.

Recommended approach:
Make the mapper conditional: check if the field name is among the known optional seed accounts before applying .as_ref().ok_or_else(). For non-optional standard accounts, generate accounts.#field_name.key() directly (or .to_account_info().key() for Signer).

Add tests confirming generated code compiles for both patterns:

  • seed: ctx.accounts.optional_seed_account
  • seed: ctx.fee_payer / seed: ctx.config
🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/instructions.rs around lines 1183–1291, the
mapper always generates accounts.#field.as_ref().ok_or_else(...)?.key() for
ctx.<field> which breaks for non-optional account types (Signer, AccountInfo).
Update the mapper to consult the instruction/account spec (available via the
spec parameter or the accounts type info) and branch: if the field is an
optional account (Option<...>) emit
accounts.#field.as_ref().ok_or_else(...)?.key(); if it is a non-optional
AccountInfo/UncheckedAccount emit accounts.#field.key(); and if it is a Signer
emit accounts.#field.to_account_info().key(); implement this by building a small
lookup of account optionality/type from the spec before mapping and use that
lookup in the ctx.<field> branch, and add unit tests for both optional and
non-optional cases to ensure generated code compiles.

Comment on lines +60 to +80
for (i, seed) in spec.seeds.iter().enumerate() {
match seed {
SeedElement::Literal(lit) => {
let value = lit.value();
token_seed_refs.push(quote! { #value.as_bytes() });
}
SeedElement::Expression(expr) => {
if let syn::Expr::Path(path_expr) = &**expr {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
if ident_str == "LIGHT_CPI_SIGNER" {
token_seed_refs.push(quote! { #ident.cpi_signer.as_ref() });
} else {
token_seed_refs.push(quote! { #ident.as_bytes() });
}
continue;
}
}
}

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 | 🟡 Minor

Align constant-name detection between on-chain and client seed paths

In generate_ctoken_seed_provider_implementation you treat an expression as an uppercase constant only if ident_str.chars().all(|c| c.is_uppercase() || c == '_') (Lines 70–76), while the client-side analyzer in analyze_seed_spec_for_client allows digits as well (is_uppercase() || '_' || is_ascii_digit(), Lines 642–645).

This means a seed like MY_CONST1 will be treated as a plain expression on-chain but as a constant on the client, potentially producing different seed byte expressions between runtime and generated helpers.

I’d suggest using the same predicate in both places (probably the more permissive one) to keep const handling consistent across server and client generation.

Also applies to: 639-655

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/seed_providers.rs around lines 60-80, the
check that classifies an identifier as an all-uppercase constant
(ident_str.chars().all(|c| c.is_uppercase() || c == '_')) is too strict and
differs from the client analyzer; update this predicate to match the client-side
logic by allowing ASCII digits as well (i.e., include is_ascii_digit() in the
char check) so identifiers like MY_CONST1 are treated consistently; make the
same change in the other occurrence noted around lines 639-655.

Comment on lines +209 to +217
fn unpack(
&self,
remaining_accounts: &[anchor_lang::prelude::AccountInfo],
) -> std::result::Result<Self::Unpacked, anchor_lang::prelude::ProgramError> {
match self {
#(#unpack_match_arms)*
Self::PackedCTokenData(_data) => Ok(self.clone()),
Self::CTokenData(_data) => unreachable!(),
}
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

Unpack must yield CTokenData, not clone the packed variant

Cloning Self::PackedCTokenData means callers that invoke Unpack still receive the packed form, so decompression never surfaces the actual CTokenData. That breaks any flow expecting Unpack to hand back the concrete account data and will leak packed buffers through the API. Please unwrap the packed CToken using the SDK’s Unpack trait and rewrap it as CompressedAccountVariant::CTokenData.

-                    Self::PackedCTokenData(_data) => Ok(self.clone()),
+                    Self::PackedCTokenData(data) => Ok(CompressedAccountVariant::CTokenData(
+                        light_compressed_token_sdk::Unpack::unpack(data, remaining_accounts)?,
+                    )),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/variant_enum.rs around lines 209 to 217, the
current Unpack arm returns a cloned PackedCTokenData which leaves the packed
form intact; instead, call the SDK's Unpack implementation on the packed data to
produce a CTokenData instance and return
Ok(CompressedAccountVariant::CTokenData(unpacked)), replacing the
clone/unreachable pair; ensure you pass the same remaining_accounts to the
Unpack call (handle any Result propagation) and remove the unreachable branch so
Unpack yields the concrete CTokenData variant.

Comment on lines +297 to +335
/// Legacy CompressAs trait implementation (use Compressible instead).
///
/// This derive macro allows you to specify which fields should be reset/overridden
/// during compression while keeping other fields as-is. Only the specified fields
/// are modified; all others retain their current values.
///
/// ## Example
///
/// ```ignore
/// use light_sdk_macros::CompressAs;
/// use light_compressible::CompressionInfo;
/// use solana_pubkey::Pubkey;
///
/// #[derive(CompressAs)]
/// #[compress_as(
/// start_time = 0,
/// end_time = None,
/// score = 0
/// )]
/// pub struct GameSession {
/// #[skip]
/// pub compression_info: Option<CompressionInfo>,
/// pub session_id: u64,
/// pub player: Pubkey,
/// pub game_type: String,
/// pub start_time: u64,
/// pub end_time: Option<u64>,
/// pub score: u64,
/// }
/// ```
///
/// ## Note
///
/// Use the new `Compressible` derive instead - it includes this functionality plus more.
#[proc_macro_derive(CompressAs, attributes(compress_as))]
pub fn compress_as_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemStruct);
into_token_stream(compressible::traits::derive_compress_as(input))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Legacy CompressAs derive is wired correctly; consider calling out its deprecated status more strongly

The new CompressAs derive wrapper correctly delegates to compressible::traits::derive_compress_as and supports the #[compress_as(...)] attribute. Given the comment explicitly calls this “Legacy” and recommends Compressible instead, you might want to add a #[deprecated] attribute on the macro itself (or in docs) to steer new users toward Compressible and reduce future surface area.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/lib.rs around lines 297 to 335, the legacy CompressAs
derive is documented as deprecated but not marked as such; add an explicit
deprecation so users are steered to Compressible. Add a #[deprecated(note = "Use
the Compressible derive instead")] attribute immediately above the
#[proc_macro_derive(CompressAs, attributes(compress_as))] line, and update the
doc comment to more strongly call out deprecation (e.g., prepend "DEPRECATED:")
so both compile-time warnings and docs guide users to the new derive.

[features]
default = []
devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]
devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]
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

Critical: Publishing blockers from previous reviews remain unresolved.

This change contains only trailing whitespace and does not address the critical issues flagged in prior reviews that block publishing light-program-test:

  1. Line 23: light-compressed-token-sdk is still unconditional and should be made optional to prevent publishing cycles
  2. Line 33: light-compressible-client is still unconditional and should be made optional (it's only used under devenv feature gating)
  3. Line 10: The devenv feature is missing dep:light-compressed-token-sdk and dep:light-compressible-client — without these entries, code under #[cfg(feature = "devenv")] that imports from these crates will fail to compile

Per ananas-block's explicit comment: "Make this optional again else we cannot publish a new version of light-program-test until we published light-compressed-token-sdk."

Apply this diff to resolve the publishing blockers:

 [features]
 default = []
-devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]  
+devenv = ["light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-compressed-token-sdk", "dep:light-compressible-client", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"]
 v2 = ["light-client/v2"]

 [dependencies]
@@ -20,11 +20,11 @@
 light-hasher = { workspace = true, features = ["poseidon", "sha256", "keccak", "std"] }
 light-ctoken-types = { workspace = true, optional = true }
 light-compressible = { workspace = true, optional = true }
-light-compressed-token-sdk = { workspace = true }
+light-compressed-token-sdk = { workspace = true, optional = true }
 light-compressed-account = { workspace = true, features = ["anchor", "poseidon"] }
 light-batched-merkle-tree = { workspace = true, features = ["test-only"], optional = true }
 light-event = { workspace = true }
 # unreleased
 light-client = { workspace = true, features = ["program-test"] }
 light-prover-client = { workspace = true }
 light-zero-copy = { workspace = true }
 litesvm = { workspace = true }
 spl-token-2022 = { workspace = true }
-light-compressible-client = { workspace = true, features = ["anchor"] }
+light-compressible-client = { workspace = true, features = ["anchor"], optional = true }

Note: devenv now includes v2 behavior implicitly via dependency gating; the explicit "v2" entry has been removed from the devenv feature to respect ananas-block's requirement that the v2 feature not depend on anchor programs.

Also applies to: 23-23, 33-33

🤖 Prompt for AI Agents
In sdk-libs/program-test/Cargo.toml around lines 10 (and apply corresponding
changes at lines 23 and 33), update the `devenv` feature to remove the explicit
"v2" entry and add `dep:light-compressed-token-sdk` and
`dep:light-compressible-client`; also make the `light-compressed-token-sdk`
(line 23) and `light-compressible-client` (line 33) dependency entries optional
(i.e., add `optional = true` to their dependency blocks) so those crates are
only pulled in via the `devenv` feature and avoid unconditional publish-time
cycles.

Comment on lines +150 to +187
pub fn handle_packed_pda_variant<'a, 'b, 'info, T, P, A, S>(
accounts_rent_sponsor: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'b, 'info>,
address_space: Pubkey,
solana_account: &AccountInfo<'info>,
index: usize,
packed: &P,
meta: &CompressedAccountMetaNoLamportsNoAddress,
post_system_accounts: &[AccountInfo<'info>],
compressed_pda_infos: &mut Vec<CompressedAccountInfo>,
program_id: &Pubkey,
seed_accounts: &A,
seed_params: Option<&S>,
) -> Result<(), ProgramError>
where
T: PdaSeedDerivation<A, S>
+ Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ crate::compressible::HasCompressionInfo
+ 'info,
P: crate::compressible::Unpack<Unpacked = T>,
S: Default,
{
let data: T = P::unpack(packed, post_system_accounts)?;

// CHECK: pda match
// Call the method with account context and seed params
let (seeds_vec, derived_pda) = if let Some(params) = seed_params {
data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)?
} else {
// For implementations without seed params, create a default one
let default_params = S::default();
data.derive_pda_seeds_with_accounts(program_id, seed_accounts, &default_params)?
};
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 | 🟡 Minor

Defaulting SeedParams to S::default() can hide missing seed data

In handle_packed_pda_variant, when seed_params is None you synthesize a default:

let (seeds_vec, derived_pda) = if let Some(params) = seed_params {
    data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)?
} else {
    let default_params = S::default();
    data.derive_pda_seeds_with_accounts(program_id, seed_accounts, &default_params)?
};

This makes it impossible for T::derive_pda_seeds_with_accounts to distinguish “no seed params provided” from “all seed fields are at their default value”, which can mask misconfigured call sites where required data.* fields were never wired into SeedParams.

If some PDAs truly require non‑default seed params, it may be safer to:

  • Treat None as an error for those types, or
  • Document that S: Default is only for variants that don’t use any instruction-data seeds and avoid constructing S::default() when SeedParams is known to be non-empty.

Right now the silent fallback can lead to hard-to-diagnose seed mismatches.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/decompress_runtime.rs around lines 150-187, do
not silently synthesize S::default() when seed_params is None; instead, if
seed_params is None return an explicit error (use an appropriate
ProgramError::Custom code from this crate or ProgramError::InvalidArgument) so
callers can't accidentally omit required seed params — change the if/else to
call data.derive_pda_seeds_with_accounts only when seed_params.is_some(),
otherwise return the error; alternatively, if the codebase uses a domain error
type, map that into ProgramError before returning.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk-libs/macros/src/lib.rs (1)

188-192: Update documentation to remove reference to non-existent AsByteVec trait.

The documentation references AsByteVec trait (lines 190-191), but per reviewer feedback, this trait no longer exists. Update the documentation to reflect the current trait requirements or remove the outdated reference.

Based on learnings

♻️ Duplicate comments (1)
sdk-libs/macros/src/lib.rs (1)

290-294: Critical: Whitelist the #[skip] attribute for HasCompressionInfo derive.

The documentation (lines 277-278) shows using #[skip] on the compression_info field, but the derive macro doesn't whitelist this attribute. Users following the documented pattern will encounter a compiler error: "cannot find attribute skip."

Apply this fix:

-#[proc_macro_derive(HasCompressionInfo)]
+#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
 pub fn has_compression_info(input: TokenStream) -> TokenStream {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a125346 and 93596ca.

📒 Files selected for processing (1)
  • sdk-libs/macros/src/lib.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk-libs/macros/src/lib.rs (9)
sdk-libs/macros/src/hasher/light_hasher.rs (7)
  • derive_light_hasher (51-53)
  • derive_light_hasher_sha (55-76)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (2)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (187-226)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (83-124)
  • derive_light_rent_sponsor (133-187)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: programs (system-cpi-test-v2-functional-read-only, ["cargo-test-sbf -p system-cpi-v2-test -- func...
  • GitHub Check: programs (light-system-program-compression, ["cargo-test-sbf -p system-test -- test_with_compress...
  • GitHub Check: programs (system-cpi-test-v2-functional-account-infos, ["cargo-test-sbf -p system-cpi-v2-test -- ...
  • GitHub Check: programs (system-cpi-test, ["cargo-test-sbf -p system-cpi-test", "cargo test -p light-system-prog...
  • GitHub Check: programs (account-compression-and-registry, ["cargo-test-sbf -p account-compression-test", "cargo...
  • GitHub Check: stateless-js-v2
  • GitHub Check: Forester e2e test
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Test program-libs-slow
🔇 Additional comments (11)
sdk-libs/macros/src/lib.rs (11)

1-18: LGTM: Module structure is well-organized.

The new module imports (compressible, rent_sponsor, utils) align with the expanded macro surface, and the centralized into_token_stream helper standardizes error handling across all macro implementations.


30-46: Documentation example correctly uses ignore fence.

The example appropriately opts out of compilation since it depends on external Anchor macros unavailable in the doctest harness. This prevents CI failures while keeping the documentation clear.


61-76: Consistent error handling pattern applied.

All macros now route through into_token_stream, which provides standardized error-to-compile-error conversion. This improves maintainability and error reporting consistency.


108-144: Documentation and implementation follow established patterns.

The example correctly uses ignore to prevent doctest compilation failures, and the implementation applies the standard into_token_stream wrapper.


238-253: Documentation and implementation are correct.

The example appropriately uses ignore fence, and the implementation follows the standard into_token_stream pattern.


330-334: CompressAs derive is correctly implemented.

The compress_as attribute is properly whitelisted, and the implementation delegates to the appropriate internal module function.


443-447: Compressible derive is properly implemented.

Both compress_as and light_seeds attributes are correctly whitelisted, and the implementation follows the established pattern.


560-566: DecompressContext derive is correctly implemented.

Both pda_types and token_variant attributes are properly whitelisted, and the comprehensive documentation clearly explains usage.


623-627: light_program implementation follows the standard pattern.

The macro correctly parses input as ItemMod and applies the into_token_stream wrapper for consistent error handling.


580-600: Let me check the actual patterns used in lib.rs:

Now let me check the specific pattern in the lib.rs file:

Based on my verification, the original review comment contains an incorrect premise.

The review comment is based on a misunderstanding of error handling in Rust procedural macros.

The parse_macro_input! macro already has error handling built-in and will return a TokenStream containing compile_error! if parsing fails. Both rent sponsor functions already use parse_macro_input!, which means they have proper error handling in place.

The macro has error handling built-in and will early-exit with a stream containing a compile_error! if the stream doesn't match the expected input type. This is the standard, idiomatic Rust pattern for procedural macros—no additional wrapping is required or expected.

The concern about wrapping results with into_token_stream is not applicable here. The rent sponsor functions already return the correct type (TokenStream), and the parse_macro_input! macro already handles all necessary error cases. The code is correct as written.


345-371: The past reviewer's removal request is based on a false premise—this macro should not be removed.

I've thoroughly verified the codebase architecture:

  1. The add_compressible_instructions macro exists only in sdk-libs/macros/src/lib.rs (the public proc_macro wrapper at lines 365-371), not in program-libs/macros.

  2. The implementation is properly modularized: The proc_macro delegates to compressible::instructions::add_compressible_instructions(), which is defined at sdk-libs/macros/src/compressible/instructions.rs:299. This is correct architecture—a public API wrapper with internal implementation details.

  3. No code was moved to program-libs/macros. That directory contains only cpi_signer.rs, pubkey.rs, and lib.rs. The macro is not there.

The past reviewer's comment claiming the macro was "moved to program-libs/macros" does not match the actual codebase state. This macro is exactly where it should be, and the current implementation is correct.

Likely an incorrect or invalid review comment.

Comment on lines +225 to 229
#[proc_macro_derive(LightHasher, attributes(hash, skip))]
pub fn light_hasher(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ItemStruct);
derive_light_hasher(input)
.unwrap_or_else(|err| err.to_compile_error())
.into()
into_token_stream(derive_light_hasher(input))
}
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

Verify that #[skip] attribute functions correctly across all LightHasher variants.

A past comment indicates that #[skip] may not be applied when using sha256::LightAccount (the default). While the attribute is whitelisted here, please verify the implementation properly honors it in all contexts, particularly with SHA256-based variants.

Based on learnings


🏁 Script executed:

#!/bin/bash
# Search for skip attribute handling in hasher implementations
rg -n -C5 'skip.*attribute|attribute.*skip' --type=rust sdk-libs/macros/src/hasher/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all Rust files in macros/src and check structure
echo "=== Directory structure of macros/src ==="
find sdk-libs/macros/src -type f -name "*.rs" | head -20

echo ""
echo "=== Search for derive_light_hasher implementation ==="
rg -n 'fn derive_light_hasher' --type=rust sdk-libs/macros/src/

echo ""
echo "=== Search for skip attribute usage (broader pattern) ==="
rg -n 'skip' --type=rust sdk-libs/macros/src/ -C2

echo ""
echo "=== Search for sha256 in macros ==="
rg -n 'sha256|SHA256' --type=rust sdk-libs/macros/src/ -C3

Length of output: 23589


🏁 Script executed:

#!/bin/bash
# Check if SHA256 path has any special skip handling
echo "=== Search for FieldAttribute::Skip usage ==="
rg -n 'FieldAttribute::Skip' --type=rust sdk-libs/macros/src/

echo ""
echo "=== Look at field_processor.rs to see if skip is handled differently ==="
head -150 sdk-libs/macros/src/hasher/field_processor.rs | tail -50

echo ""
echo "=== Search for any custom serialization logic with skip ==="
rg -n 'serialize|BorshSerialize' --type=rust sdk-libs/macros/src/hasher/ -C2

Length of output: 7265


The #[skip] attribute is accepted but silently ignored in the SHA256-based LightHasherSha variant, causing skipped fields to be included in the hash.

The macro whitelists both hash and skip attributes (sdk-libs/macros/src/lib.rs lines 225, 249), but the SHA256 implementation bypasses field-level attribute processing entirely. Both data_hasher.rs (line 108) and to_byte_array.rs (line 80) use self.try_to_vec() for whole-struct serialization via BorshSerialize, which necessarily includes all fields regardless of #[skip] markers. The Poseidon variant respects #[skip] through field-by-field processing (field_processor.rs:54), but the SHA256 path does not.

Either:

  1. Remove skip from the attribute whitelist for LightHasherSha (lib.rs line 249), or
  2. Implement per-field attribute support in the SHA256 serialization path

Document the limitation if using SHA256 is intentional and field skipping is not required.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/lib.rs around lines 225-229 (and note the attribute
whitelist near line 249), the LightHasher macro currently accepts a #[skip]
attribute but the SHA256-backed LightHasherSha path ignores it because
data_hasher.rs (line ~108) and to_byte_array.rs (line ~80) call
self.try_to_vec(), which serializes all fields; either remove "skip" from the
attribute whitelist for the SHA256 variant to avoid a misleading attribute
acceptance, or implement per-field attribute handling in the SHA256 path: change
the SHA256 serialization to mirror the Poseidon approach by iterating fields,
consulting the same field_processor logic to omit #[skip] fields when
constructing the byte array (update data_hasher.rs and to_byte_array.rs to build
bytes from per-field processing rather than try_to_vec()), and add a short code
comment/doc note explaining the chosen behavior/limitation.

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: 5

♻️ Duplicate comments (4)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)

108-129: Align Rent::get error mapping and consider asserting program ID consistency

Two small but important refinements here:

  1. Rent::get() failures are mapped to LightSdkError::Borsh, which is meant for (de)serialization issues, not sysvar/program errors. It’s more accurate to surface them as ProgramError:
-    let rent = Rent::get().map_err(|err| {
-        msg!("Failed to get rent: {:?}", err);
-        LightSdkError::Borsh
-    })?;
+    let rent = Rent::get().map_err(|err| {
+        msg!("Failed to get rent: {:?}", err);
+        LightSdkError::ProgramError(err)
+    })?;

(or use LightSdkError::from(err) if you have a From<ProgramError> impl).

  1. The new account is created with &cpi_accounts.self_program_id() as owner, while LightAccount::<T>::new_close uses the separate program_id parameter. If these ever diverge, you’d get a logically “owned by X, but typed as Y” PDA. A cheap safeguard is to assert they match before creating the account:
debug_assert_eq!(program_id, &cpi_accounts.self_program_id());

This will catch misconfigurations in tests/dev builds without affecting release behavior.

sdk-libs/sdk/src/compressible/compress_runtime.rs (1)

49-54: Use explicit LightSdkError instead of opaque ProgramError::Custom(0)

The config validation branches silently emit ProgramError::Custom(0), which makes on-chain diagnostics and client handling harder and diverges from the rest of the compressible config code that uses LightSdkError::ConstraintViolation.

Consider mapping these mismatches to the typed error:

-    if *ctx.rent_sponsor().key != compression_config.rent_sponsor {
-        return Err(ProgramError::Custom(0));
-    }
-    if *ctx.compression_authority().key != compression_config.compression_authority {
-        return Err(ProgramError::Custom(0));
-    }
+    if *ctx.rent_sponsor().key != compression_config.rent_sponsor
+        || *ctx.compression_authority().key != compression_config.compression_authority
+    {
+        return Err(crate::error::LightSdkError::ConstraintViolation.into());
+    }

This keeps error codes consistent with CompressibleConfig::load_checked and other constraint checks in the SDK.

sdk-libs/macros/src/lib.rs (2)

227-232: DataHasher derive still aliases the SHA variant, not LightHasher as documented

Doc says this is an alias of LightHasher, but the implementation delegates to derive_light_hasher_sha, changing the hash function semantics.

If the intent is to truly alias LightHasher, switch to the Poseidon‑backed implementation:

 #[proc_macro_derive(DataHasher, attributes(skip, hash))]
 pub fn data_hasher(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as ItemStruct);
-    into_token_stream(derive_light_hasher_sha(input))
+    into_token_stream(derive_light_hasher(input))
 }

If SHA is desired here, the doc comment should explicitly say so.


234-266: HasCompressionInfo docs use #[skip] but the derive doesn’t accept it

The example shows:

#[derive(HasCompressionInfo)]
pub struct UserRecord {
    #[skip]
    pub compression_info: Option<CompressionInfo>,
    // ...
}

Without whitelisting skip in the derive, this pattern will fail with “cannot find attribute skip” when used without another derive that accepts skip. Either:

  • Allow #[skip] on this derive:
-#[proc_macro_derive(HasCompressionInfo)]
+#[proc_macro_derive(HasCompressionInfo, attributes(skip))]
 pub fn has_compression_info(input: TokenStream) -> TokenStream {

or

  • Update the docs to clarify that #[skip] comes from LightHasher and that you must also derive LightHasher on the same struct.

Given the example, I’d strongly recommend the first option for ergonomics.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93596ca and 68a3129.

⛔ Files ignored due to path filters (4)
  • sdk-tests/csdk-anchor-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-full-derived-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/sdk-compressible-test/Cargo.toml is excluded by none and included by none
  • xtask/Cargo.toml is excluded by none and included by none
📒 Files selected for processing (7)
  • sdk-libs/macros/src/compressible/decompress_context.rs (1 hunks)
  • sdk-libs/macros/src/compressible/seed_providers.rs (1 hunks)
  • sdk-libs/macros/src/lib.rs (7 hunks)
  • sdk-libs/program-test/Cargo.toml (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_runtime.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (7 hunks)
  • sdk-libs/sdk/src/compressible/decompress_runtime.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
  • fee_payer (65-65)
  • config (66-66)
  • rent_sponsor (67-67)
  • compressible (204-204)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-43)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (2)
  • compressible (204-204)
  • rent_sponsor (67-67)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (37-37)
  • new_decompressed (104-112)
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
  • rent_sponsor (13-13)
sdk-libs/macros/src/compressible/seed_providers.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (3)
  • compressible (204-204)
  • get_seeds (36-40)
  • get_authority_seeds (43-47)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/macros/src/compressible/utils.rs (1)
  • is_pubkey_type (95-105)
sdk-libs/macros/src/compressible/decompress_context.rs (3)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (10)
  • compressible (204-204)
  • handle_packed_pda_variant (150-221)
  • rent_sponsor (67-67)
  • fee_payer (65-65)
  • config (66-66)
  • ctoken_rent_sponsor (68-68)
  • ctoken_program (69-69)
  • ctoken_cpi_authority (70-70)
  • ctoken_config (71-71)
  • collect_pda_and_token (78-88)
sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs (1)
  • process_decompress_tokens_runtime (37-187)
sdk-libs/macros/src/lib.rs (1)
  • derive_decompress_context (509-514)
sdk-libs/macros/src/lib.rs (9)
sdk-libs/macros/src/hasher/light_hasher.rs (6)
  • derive_light_hasher (51-53)
  • syn (198-198)
  • syn (260-260)
  • syn (344-344)
  • syn (418-418)
  • syn (480-480)
sdk-libs/macros/src/compressible/instructions.rs (2)
  • syn (303-303)
  • add_compressible_instructions (299-883)
sdk-libs/macros/src/utils.rs (1)
  • into_token_stream (17-19)
sdk-libs/macros/src/program.rs (1)
  • program (290-301)
sdk-libs/macros/src/accounts.rs (2)
  • process_light_system_accounts (12-68)
  • process_light_accounts (128-344)
sdk-libs/macros/src/compressible/traits.rs (4)
  • input (27-27)
  • derive_has_compression_info (211-217)
  • derive_compress_as (192-209)
  • derive_compressible (219-256)
sdk-libs/macros/src/compressible/pack_unpack.rs (1)
  • derive_compressible_pack (8-20)
sdk-libs/macros/src/compressible/decompress_context.rs (1)
  • derive_decompress_context (180-219)
sdk-libs/macros/src/rent_sponsor.rs (2)
  • derive_light_rent_sponsor_pda (83-124)
  • derive_light_rent_sponsor (133-187)
sdk-libs/sdk/src/compressible/decompress_runtime.rs (4)
sdk-libs/sdk/src/compressible/compress_runtime.rs (3)
  • fee_payer (11-11)
  • config (12-12)
  • rent_sponsor (13-13)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • prepare_account_for_decompression_idempotent (82-145)
  • into_compressed_meta_with_address (25-44)
sdk-libs/sdk-types/src/cpi_accounts/config.rs (1)
  • cpi_signer (30-32)
sdk-libs/sdk/src/compressible/config.rs (1)
  • load_checked (113-147)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test program-libs-fast
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Test program-libs-slow
🔇 Additional comments (4)
sdk-libs/program-test/Cargo.toml (1)

10-10: Rewrite requires verification: light-compressed-token-sdk usage is unverified.

I've examined the codebase and found critical issues with two of the three claims:

Confirmed Issue:

  • light-compressible-client is genuinely unconditionally imported in compressible_setup.rs (line 10: use light_compressible_client::compressible_instruction;). Since this module is only used during testing with the devenv feature, making this dependency optional and adding dep:light-compressible-client to the devenv feature is correct.

Unconfirmed Issue:

  • I cannot find any usage of light-compressed-token-sdk in the codebase. Extensive searches reveal zero imports of this crate. The code uses light_compressed_token (a different crate, already optional) and light_ctoken_types (already optional), but not light-compressed-token-sdk. The review claims it's "only used behind feature gates," but I cannot locate these usages.

Before proceeding, this needs clarification:

  1. Where is light-compressed-token-sdk actually used? Is it a dead dependency, or used indirectly through another import?
  2. Should it be removed entirely if unused, or is it required for future development?

The light-compressible-client fix is solid and should be applied. The light-compressed-token-sdk portion requires manual verification.

sdk-libs/sdk/src/compressible/compress_runtime.rs (1)

56-87: Bounds checks on system and PDA account slicing look solid

The new guard on system_accounts_offset_usize and the use of checked_sub plus .get(..) around the PDA slice eliminate the previous panic risk from malformed remaining_accounts / compressed_accounts inputs. This is the right pattern for handling attacker-controlled account arrays.

sdk-libs/macros/src/lib.rs (1)

60-76: Good use of into_token_stream wrapper for attribute macros

Refactoring light_system_accounts / light_accounts to parse via syn and route through into_token_stream means macro failures become proper compile errors with diagnostics instead of panics, which is the right pattern for these proc-macros.

sdk-libs/macros/src/compressible/decompress_context.rs (1)

35-69: Seed param handling in generated DecompressContext impl is now lifetime‑sound

The generated PDA match arms now forward the seed_params: Option<&SeedParams> directly into handle_packed_pda_variant, letting that helper construct S::default() when needed. This removes the previous lifetime issue around referencing a temporary SeedParams::default() while keeping the behavior (optional seed params with a sensible default) intact.

The rest of the generated impl (PDA vs token dispatch, account accessors, and associated types) matches the runtime trait expectations.

Comment on lines +291 to +304
syn::Expr::Path(path_expr) => {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
if ident_str == "LIGHT_CPI_SIGNER" {
auth_seed_refs
.push(quote! { #ident.cpi_signer.as_ref() });
} else {
auth_seed_refs.push(quote! { #ident.as_bytes() });
}
handled = true;
}
}
}
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 | 🟡 Minor

Inconsistent constant detection in authority seeds path.

The constant identifier check at line 294 excludes is_ascii_digit(), while the parallel logic for token seeds (lines 70-72) and client generation (lines 645-647, 850-851) includes it. This means an identifier like MY_CONST1 will be treated as a plain expression here but as a constant elsewhere, potentially producing different seed derivation logic.

Apply this diff to align the check:

-                                if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
+                                if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) {
📝 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
syn::Expr::Path(path_expr) => {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_') {
if ident_str == "LIGHT_CPI_SIGNER" {
auth_seed_refs
.push(quote! { #ident.cpi_signer.as_ref() });
} else {
auth_seed_refs.push(quote! { #ident.as_bytes() });
}
handled = true;
}
}
}
syn::Expr::Path(path_expr) => {
if let Some(ident) = path_expr.path.get_ident() {
let ident_str = ident.to_string();
if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) {
if ident_str == "LIGHT_CPI_SIGNER" {
auth_seed_refs
.push(quote! { #ident.cpi_signer.as_ref() });
} else {
auth_seed_refs.push(quote! { #ident.as_bytes() });
}
handled = true;
}
}
}
🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/seed_providers.rs around lines 291 to 304,
the constant identifier detection excludes digits causing identifiers like
MY_CONST1 to be misclassified; update the character check so it accepts ASCII
digits as well (i.e., change the chars().all predicate to allow is_ascii_digit()
in addition to is_uppercase() and '_' ), and ensure the subsequent branching for
LIGHT_CPI_SIGNER vs other constants remains unchanged so constant names with
digits are handled the same as in the other modules.

Comment on lines +556 to +574
if segment.ident == "data" {
if let Some(data_spec) = instruction_data
.iter()
.find(|d| d.field_name == *field_name)
{
let param_type = &data_spec.field_type;
let param_with_ref = if is_pubkey_type(param_type) {
quote! { #field_name: &#param_type }
} else {
quote! { #field_name: #param_type }
};
parameters.push(param_with_ref);
expressions.push(quote! { #field_name.as_ref() });
} else {
return Err(syn::Error::new_spanned(
field_name,
format!("data.{} used in seeds but no type specified", field_name),
));
}
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

Type inference gap: non-Pubkey instruction_data fields without method calls will generate invalid code.

When a seed references data.field_name where field_name has type other than Pubkey (e.g., u64, u32, [u8; 32]), the code generates:

parameters.push(quote! { field_name: u64 });
expressions.push(quote! { field_name.as_ref() });

But primitive numeric types don't implement AsRef<[u8]>, causing compilation failure. Users must write data.amount.to_le_bytes() explicitly, but this isn't validated or documented here.

Consider either:

  1. Detecting primitive numeric types and generating .to_le_bytes().as_ref() automatically, or
  2. Returning a compile-time error with a helpful message that numeric types require explicit conversion methods in seed specs.

Also applies to: 599-621, 795-815

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/seed_providers.rs around lines 556-574 (and
similarly at 599-621 and 795-815), the macro blindly generates
field_name.as_ref() for non-Pubkey data fields which fails for primitive numeric
types; change the generation to detect the field type: if it's a primitive
integer (u8/u16/u32/u64/u128/i8/... etc.) emit
field_name.to_le_bytes().as_ref(); if it's an array/byte-slice-like type that
already implements AsRef<[u8]> keep field_name.as_ref(); otherwise emit a
syn::Error::new_spanned with a clear message instructing the user to provide an
explicit conversion method for that type in the seed spec. Ensure this detection
logic is applied in all three code locations so the macro either adapts integers
to little-endian bytes or fails at compile time with a helpful error.

Comment on lines +695 to +711
if !parameters.iter().any(|p| {
p.to_string()
.contains(&field_name.to_string())
}) {
parameters.push(param_with_ref);
}
}
return quote! { #field_name };
} else if segment.ident == "ctx" {
// ctx.field -> add as Pubkey parameter
if !parameters.iter().any(|p| {
p.to_string()
.contains(&field_name.to_string())
}) {
parameters.push(quote! { #field_name: &solana_pubkey::Pubkey });
}
return quote! { #field_name };
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Fragile duplicate parameter detection using string comparison on TokenStream.

Lines 696-698 and 705-708 check for duplicate parameters by converting TokenStream to string and checking if the field name is contained. This is brittle—TokenStream formatting isn't guaranteed, so field_name: &Pubkey vs field_name : & Pubkey could bypass the check, or false positives could occur if the field name appears in a type annotation (e.g., PubkeyField).

Maintain a HashSet<String> of added parameter names instead:

 fn map_client_call_arg(
     arg: &syn::Expr,
     instruction_data: &[InstructionDataSpec],
     parameters: &mut Vec<TokenStream>,
+    seen_params: &mut std::collections::HashSet<String>,
 ) -> TokenStream {
     // ... existing code ...
-    if !parameters.iter().any(|p| p.to_string().contains(&field_name.to_string())) {
+    if seen_params.insert(field_name.to_string()) {
         parameters.push(param_with_ref);
     }

Then thread seen_params through all recursive calls.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk-libs/macros/src/compressible/seed_providers.rs around lines 695 to 711,
the duplicate-parameter check currently converts TokenStreams to strings and
does substring matching which is fragile; instead add a HashSet<String> (e.g.,
seen_params) of parameter names, check membership with
seen_params.contains(&field_name.to_string()), and when adding a parameter push
the TokenStream into the parameters Vec and also insert field_name.to_string()
into seen_params; thread this seen_params HashSet through the recursive calls so
every call consults and updates the same set rather than relying on TokenStream
string matching.

Comment on lines +239 to +246
let compression_config =
crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
let address_space = compression_config.address_space[0];

let (has_tokens, has_pdas) = check_account_types(&compressed_accounts);
if !has_tokens && !has_pdas {
return Ok(());
}
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

Decompression path does not enforce rent-sponsor / authority invariants like the compression path

Here you load CompressibleConfig but only use address_space, whereas process_compress_pda_accounts_idempotent enforces that ctx.rent_sponsor() and ctx.compression_authority() match the config before proceeding.

If decompression should be constrained to the same on-chain config (for symmetric safety and easier reasoning), consider adding equivalent checks:

let compression_config =
    crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?;
let address_space = compression_config.address_space[0];

if *ctx.rent_sponsor().key != compression_config.rent_sponsor
    || /* if applicable */ ctx.ctoken_cpi_authority().map(|a| a.key) != Some(&compression_config.compression_authority)
{
    return Err(crate::error::LightSdkError::ConstraintViolation.into());
}

If the design intentionally allows a different rent sponsor on decompression, documenting that asymmetry near this call would help future maintainers.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/decompress_runtime.rs around lines 239 to 246,
the loaded CompressibleConfig is only used for address_space but misses the
authority/rent-sponsor invariant checks present in the compression path; add
checks that compare *ctx.rent_sponsor().key to compression_config.rent_sponsor
and, if applicable, ctx.ctoken_cpi_authority().map(|a| a.key) (or
ctx.compression_authority() if used) to
compression_config.compression_authority, and return
crate::error::LightSdkError::ConstraintViolation.into() on mismatch; if the
asymmetry is intentional instead, add a clear comment documenting why
decompression may accept different rent sponsor/authority.

Comment on lines +323 to +329
// TODO: fix this
#[cfg(not(feature = "cpi-context"))]
if has_pdas {
LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof)
.with_account_infos(&compressed_pda_infos)
.invoke(cpi_accounts.clone())?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify or remove the // TODO: fix this in the non-cpi-context PDA path

The TODO above the non‑cpi-context PDA branch doesn’t explain what is wrong with the current behavior. Either:

  • Fix the underlying issue and drop the comment, or
  • Replace it with a precise description and a tracking issue reference (e.g. // TODO(#1234): ...).

Right now it just signals “something is broken here” without enough context.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/decompress_runtime.rs around lines 323 to 329,
the stray "// TODO: fix this" above the non-`cpi-context` PDA branch is
ambiguous; either remove it if there is no bug or replace it with a precise
comment or issue reference describing the actual problem and expected behavior.
Inspect the non-`cpi-context` PDA path (the LightSystemProgramCpi::new_cpi
invocation using cpi_accounts.config().cpi_signer and compressed_pda_infos),
confirm it behaves correctly; if it does, delete the TODO and leave a concise
comment describing why this branch is correct; if it is broken, implement the
needed fix and then replace the TODO with a targeted comment like "//
TODO(#<issue>): <short description of remaining problem and reproduction steps>"
linking the tracker.

@ananas-block ananas-block merged commit 0f92d72 into main Nov 24, 2025
32 checks passed
@ananas-block ananas-block deleted the swen/csdk-macro-rent branch November 24, 2025 15:38
This was referenced Nov 28, 2025
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