Conversation
WalkthroughAdds 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 Changes
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (1)
⏰ 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)
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. Comment |
There was a problem hiding this comment.
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 centralizingrent_sponsorderivationThe wrapper constructs
CreateCompressibleTokenAccountwith fields that line up with the newcreate_compressible_token_account_instructionhelper, and the PDA derivations forcompressible_configandrent_sponsorlook consistent with the on-chain layout.Given this PR introduces a
derive_rent_sponsor!-style macro elsewhere, it might be worth routing therent_sponsorPDA 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: Newauto_register_custom_programs_for_pda_compressionflag is wired correctly; document its semanticsThe new
auto_register_custom_programs_for_pda_compressionfield is properly guarded bydevenv, andDefaultinitializes it totrue, 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 redundantEnabling
keccakboth via the base dependency features (light-hasher = { ..., features = ["poseidon", "keccak"] },light-compressed-account = { ..., features = ["std", "keccak"] }) and again undersolana/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
*/keccaklines. If you want it gated bysolana/pinocchio, move thekeccakflag 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 changesThe processor correctly expects six accounts in order: token_account, mint, payer, compressible_config, system_program, and rent_payer. However, the
system_programfield (line 68 ofprograms/compressed-token/program/src/create_token_account.rs) is parsed into theCompressibleAccountsstruct but then never dereferenced or validated anywhere inprocess_create_token_account().The actual account creation happens via
create_pda_account()andcreate_account_with_custom_rent_payer(), neither of which referencessystem_program. This means line 81 of the instruction builder—which passesPubkey::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 64forester/Cargo.toml (1)
31-83: Avoid duplicatinglight-compressed-token-sdkin both deps and dev-depsYou already depend on
light-compressed-token-sdkin 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:
- Checking
token_account_info.ownerto distinguish between SPL Token and Token-2022- 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 inefficiencyYour analysis is spot-on. I've confirmed:
- The
auto_mine_cold_state_programsfield 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 vectorTwo refinements to action:
Nit #1 is a real inefficiency. Since
PubkeyisCopy, 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?; + }Nit #2 stands as written. The
CompressibleAccountStorerecreation on each warp is fine for correctness; if historical state tracking becomes needed later, storing it onLightProgramTestwould 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 confusingThe 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 ofcompress_and_close_ctoken_accounts_with_indices, but the parameter namerent_sponsor_is_signerno 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_signerorwith_compression_authority, or- Adding a short doc comment near the function signature explaining that
rent_sponsor_is_signer == trueimplies “compression authority closes & signs”.Behavior looks correct; this is a clarity/maintainability tweak.
Please verify that all call sites indeed pass
with_compression_authorityinto this parameter and not something rent‑sponsor related.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (77)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/cli-v1.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonecli/src/commands/token-balance/index.tsis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/account-compression-test/Cargo.tomlis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/compress_and_close.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/create.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/shared.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/mint/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/transfer2/spl_ctoken.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/v1.rsis excluded by none and included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by noneprogram-tests/system-cpi-test/Cargo.tomlis excluded by none and included by noneprogram-tests/system-cpi-test/tests/test.rsis excluded by none and included by noneprogram-tests/utils/src/assert_mint_to_compressed.rsis excluded by none and included by noneprogram-tests/utils/src/assert_token_tx.rsis excluded by none and included by noneprogram-tests/utils/src/assert_transfer2.rsis excluded by none and included by noneprogram-tests/utils/src/conversions.rsis excluded by none and included by noneprogram-tests/utils/src/e2e_test_env.rsis excluded by none and included by noneprogram-tests/utils/src/spl.rsis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/client-test/tests/light_client.rsis excluded by none and included by nonesdk-tests/client-test/tests/light_program_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/processor.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/variant.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/Anchor.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/Xargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/package.jsonis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/constants.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/errors.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/mod.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/lib.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/seeds.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/state.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/user_record_tests.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_four_transfer2.rsis 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
| 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) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Validate LUT account owner and simplify Pubkey conversions
Two small but important points here:
-
load_lookup_tabletrusts any account atlookup_table_addressand only relies onAddressLookupTable::deserialize. It’s usually safer to also assertraw_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.keyand.addressescan likely be constructed directly from the existingPubkeyvalues (e.g.key: *lookup_table_address,addresses: address_lookup_table.addresses.clone()), without the intermediateto_bytes().into()hop, assuming thePubkeytypes fromsolana_messageandsolana_pubkeyare 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.
| /// 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, | ||
| ], | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 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
authorityaccount toinvoke, but there is no matchingAccountMetafor it in the instruction, and it is not marked as a signer in the instruction either. That means the compressed-token program will never treatauthorityas a signer. If onlypayeris meant to sign, that’s fine, but then includingauthorityhere is misleading; ifauthoritymust sign, it needs to be reflected in theInstruction’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 thirdAccountMetaso it matches thesystem_programAccountInfoyou pass intoinvoke. - Either drop
authorityfrom the non‑idempotent CPI accounts list, or (if the c-token program expects it) add a correspondingAccountMeta::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 30Length 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 rustLength 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 2Length 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.rsLength 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 3Length 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.rsLength 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.rsLength 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.rsLength 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
- Ensure account order matches the callee’s instruction AccountMeta order exactly (client/order you pass to Instruction must match the callee). [1][5]
- 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]
- 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]
- 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]
- 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]
- 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]
- 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:
-
system_program mismatch (both wrappers): The instruction at line 208 hardcodes
system_programas[0; 32], but bothcreate_associated_ctoken_accountandcreate_associated_ctoken_account_idempotentpass the realsystem_programAccountInfo tosolana_cpi::invoke. During CPI validation, the runtime compares eachAccountMeta.pubkeywith the correspondingAccountInfo.key; a mismatch causesProgramError::InvalidArgument. These calls will fail unless your actual system program has the all-zeros pubkey. -
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_accountpasses 6 accounts to invoke—includingauthority. 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])toPubkey::from(solana_program::system_program::ID)so the instruction'ssystem_programAccountMeta matches what you pass to invoke. - Remove
authorityfrom the accounts slice passed tosolana_cpi::invokein the non-idempotent wrapper (line 438–450), since there is no correspondingAccountMetain the instruction andauthorityis not marked as a signer. If the compressed-token program documentation specifies thatauthoritymust be included and must sign, then add it as anAccountMeta::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.
| /// 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 | ||
| } |
There was a problem hiding this comment.
🧹 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_addressdirectly. - 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs
Outdated
Show resolved
Hide resolved
| /// 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(()) | ||
| } |
There was a problem hiding this comment.
🧩 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 -SLength 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.rsLength 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 50Length 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.rsLength 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 -60Length 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:
payergets matched toCPI_AUTHORITY_PDA❌ - Position 1:
compressed_token_program_authoritygets matched topayer❌
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.
| /// 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(), | ||
| )), | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| /// 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()?)) | ||
| } |
There was a problem hiding this comment.
🧩 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:
-
Error mapping for
Rent::get(Lines 108–111)
MappingRent::get()failures toLightSdkError::Borshis semantically odd; this isn’t a Borsh issue. Given you already useLightSdkError::ProgramErrorelsewhere 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 -40Length 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 rustLength 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 -nLength 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 -40Length 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 rustLength 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.rsLength 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.rsLength 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 -50Length of output: 61
Address error type mapping and add program_id consistency validation
Two refinements would strengthen this function:
-
Lines 108–111: Map
Rent::get()toLightSdkError::ProgramErrorinstead ofBorsh
TheLightSdkErrorenum includes aProgramErrorvariant (error.rs:101) that's semantically appropriate for system program failures. Borsh is for serialization errors, not rent queries. -
Lines 97, 124: Validate
program_idandcpi_accounts.self_program_id()consistency
The function receivesprogram_idas a parameter and passes it toLightAccount::<T>::new_close(), but the account is created withcpi_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 threadprogram_idthrough toinvoke_create_account_with_heap().
There was a problem hiding this comment.
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-typesis listed as a non-optional workspace dependency but has no direct imports or usage in the SDK source. The prior reviewer found thatprogram-test/Cargo.tomlcorrectly treats it as optional (feature-gated behinddevenv), 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
devenvfeature). If the crate is genuinely required by the new compressed-account logic, add explicituse 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
DataHasheris an "Alias ofLightHasher", but the implementation on line 236 callsderive_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
📒 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, andsolana-loader-v3-interface(withserdefeature 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 likeInstruction::new_with_bincode, which justifies the use ofsolana-loader-v3-interfacefor 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 (
anchorfeature augmented): Adding"light-compressible/anchor"is aligned with the PR's goal of integrating compressible account runtime/macros.- Line 29 (new
anchor-discriminatorfeature): Clean feature gate for discriminator macro support, properly delegates tolight-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-compressiblealigns 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 theanchorfeature (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
bincodeis a direct, necessary dependency. It's used atsdk-libs/sdk/src/compressible/config.rs:391to deserializeUpgradeableLoaderStatefrom 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 theadd_compressible_instructionsattribute are properly implemented:
- Consistent use of
into_token_streamfor 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 genericaccountattribute 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:
- The rename from
light_accounttoaccountis intentional for API simplification- This could create confusion with other account-related attributes (e.g., Anchor's
#[account])- 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 throughinto_token_stream.The refactoring to wrap all macro implementations with
into_token_streamprovides standardized error handling across the codebase. This ensures thatsyn::Errorresults 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
sdk-libs/program-test/Cargo.toml
Outdated
| # 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", |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
Make this optional again else we cannot publish a new version of light-program-test until we published light-compressed-token-sdk.
There was a problem hiding this comment.
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 reasonableUsing
CtokenCompressibleConfig::new_ctokenplusCtokenCompressibleConfig::derive_pdato materialize a synthetic config and then copying outrent_sponsor/compression_authorityand 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
versionhere 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 unfoundedThe code at line 594–595 does have a panic vulnerability: accessing
.0.unwrap()on the ValidityProof's innerOption<CompressedProof>will panic if that Option is ever None. This is a legitimate invariant risk if_get_validity_proof_v1_implementationever constructs aValidityProofWithContextwith a None proof value.However, the concern about the From trait implementation is incorrect:
impl From<Option<CompressedProof>> for ValidityProofis 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_infosgating mirrorsget_latest_active_state_treesSame
devenv-based split here: non‑devenvbuilds see only v1 trees,devenvbuilds see only v2 trees. This keeps the behavior consistent withget_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 implementationsThe program-test
rpc.rsnow gates tree selection ondevenvfeature, but the actual client RPC (lines 693, 783 insdk-libs/client/src/rpc/client.rs) still gates onv2feature. These are orthogonal controls—someone building withfeature = "v2"but withoutfeature = "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.rsThe 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 treatTreeInfouniformly regardless of whether it contains a v1 or v2 tree). Thedevenvfeature correctly gates program dependencies per CLAUDE.md guidance.However, there's a documentation issue:
sdk-libs/program-test/src/lib.rsclaims the feature isv2, but the actual feature defined inCargo.tomlisdevenv. 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: Newauto_register_custom_programs_for_pda_compressionflag could use documentationThe new
auto_register_custom_programs_for_pda_compressionflag is an important behavioral switch but is currently undocumented in the struct. Given it’s public and alters how additional programs are treated inLightProgramTest, a short doc comment (what “auto‑register” means, how it interacts withadditional_programsand cold‑state mining) would help test authors understand the implications.
122-165: Defaultingauto_register_custom_programs_for_pda_compressiontotruechanges test behaviorIn the
Defaultimpl,auto_register_custom_programs_for_pda_compressionis nowtrueunderdevenv, which means all default configs will auto‑registeradditional_programsfor 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
falseas 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 tofalsewhen they need the old behavior.sdk-libs/program-test/src/program_test/light_program_test.rs (2)
28-38:auto_mine_cold_state_programswiring looks sound; consider small API polishThe new
auto_mine_cold_state_programsfield, its initialization innew, anddisable_cold_state_miningform a coherent configuration surface for cold‑state PDA mining underdevenv. Theretaincall is correct and avoids duplicate entries via the earliercontainscheck.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, takingimpl Into<Pubkey>(or at least&Pubkey) would make it more ergonomic to call from sites that already have a reference rather than a by‑valuePubkey.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 setupThe new block under
#[cfg(feature = "devenv")]that pullsauto_register_custom_programs_for_pda_compressionandadditional_programsfromconfigand seedsauto_mine_cold_state_programsis 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 causesauto_compress_program_pdasto 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:
- Call sites that want additional programs but don’t want them auto‑compressed are correctly setting
auto_register_custom_programs_for_pda_compression = false.- There’s no scenario where tests accidentally end up doing more work than expected (e.g., long
warp_slot_forwardtimes) because they pass a largeadditional_programslist 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 indevenv.The
devenvfeature declares conditional dependencies likedep:light-compressed-tokenbut omitsdep:light-compressed-token-sdkanddep: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-testcan 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: Honorcompression_authorityfrom on-chain config inauto_compress_program_pdasRight now you ignore the authority stored in the config and hard-code
compression_authorityto 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_chunkswallows errors, the effect is a silent no-op: compression will just never happen oncecfg.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 keepsauto_compress_program_pdascorrect 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_chunkbest-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:
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.Batch validity proof requests for better RPC efficiency (previously noted)
get_validity_proofis 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.hashvalues for the chunk.- Call
get_validity_proofonce 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
⛔ Files ignored due to path filters (5)
sdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis 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
devenvfeature 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.rsswitch fromv2todevenv, thev2feature gate remains active in 40+ locations across the codebase (sdk,sdk-pinocchio,client, and others). More importantly,sdk-libs/client/Cargo.tomlshowsdevenv = ["v2"]—meaningdevenvis a meta-feature that enablesv2, not a replacement for it.This indicates one of two scenarios:
- PR is intentionally scoped to only
program-test(acceptable if documented, sincedevenvwill transitively enablev2)- 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 consistentThe separation between
CtokenCompressibleConfig(program-libs cToken config),RentConfig/SLOTS_PER_EPOCH, andCpdaCompressibleConfig(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 forMerkleProofWithContextaligned with devenv-only usageSwitching the import to
#[cfg(feature = "devenv")]matches howMerkleProofWithContextis 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:Hashimport looks consistent with existing usageImporting
Hashfromlight_sdk::light_hashermatches its usage inget_compressed_account_by_hashand 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 foundThe original concern was well-founded as a verification step. However, the search confirms no compilation risk: the only reference to
ProgramTestConfig::new_v2in 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"] } |
There was a problem hiding this comment.
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.
| 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).
| use light_compressed_token_sdk::compat::{TokenData, TokenDataWithMerkleContext}; | ||
| use light_event::event::PublicTransactionEvent; | ||
| use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; |
There was a problem hiding this comment.
🧩 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_sdkas a dependency ofsdk-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' -SLength 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/"
fiLength 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.rsLength 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.
9076271 to
a02a0cb
Compare
There was a problem hiding this comment.
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:
- N separate transactions instead of N/BATCH_SIZE transactions (BATCH_SIZE=5 from Line 211 is now unused)
- N separate
get_validity_proofRPC calls instead of one batched call per chunk- N separate
get_compressed_accountRPC calls instead of bulk fetchingFor 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_idempotentAPI (from relevant snippets) accepts arrays of accounts, so batching is supported. The outer loop (Lines 211-226) still batches PDAs into chunks buttry_compress_chunkignores 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_pubkeyis declared but not yet initialized.Lines 346-369: The
elseblock tries to:
- Derive from compressible extension (lines 347-357)
- Fallback to finding first signer in packed_accounts (lines 360-367)
- Return error if still None (line 368)
Problem: The code attempts to assign to
rent_sponsor_pubkey(anOption<Pubkey>) inside the else block and then unwrap it at line 368, but the outeractual_rent_sponsorbinding shadows this. The logic appears to be trying to mutaterent_sponsor_pubkeyafter checking it, which creates confusing control flow.Recommendation: Simplify this by directly computing
actual_rent_sponsorwithout 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_pubkeywithin 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 PDALine [60] debits
rent_sponsorwithout ever supplying its signer seeds, sosystem_instruction::create_accountwill fail withMissingRequiredSignatureas soon as the rent sponsor is the PDA derived byderive_rent_sponsor!. Please plumb the rent-sponsor signer seeds and pass them toinvoke_signedalongside 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_accountswhen 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 ofcfg.compression_authoritycauses compress instructions to fail authorization checks whenever the config specifies a different authority. Becausetry_compress_chunkswallows 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_authorityequal topayer.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-buildproperly includesanchorfor IDL generation ✓anchorfeature correctly gateslight-compressible/anchor✓light-compressibleworkspace 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 (likeprogram-testdoes withdevenv), or remove it if truly unused.Verify whether
light-ctoken-typesis 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 rustAlso 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
v2→devenvfeature flag updates are applied consistently across:
- Import of
MerkleProofWithContext(line 20-21)get_validity_proofv2 implementation with queue handling (lines 455-599)get_queue_elementsimplementation (lines 627-865)get_subtreesimplementation (lines 875-911)get_address_queue_with_proofsimplementation (lines 923-997)However, past review comments flagged critical issues that should be addressed:
Line 43: Unresolved crate import for
light_compressed_token_sdkcauses compilation failures. The dependency needs to be added to thedevenvfeature insdk-libs/program-test/Cargo.toml.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 forRent::get()Line [110] again maps
Rent::get()failures toLightSdkError::Borsh, undoing the fix we landed earlier. Please switch back toLightSdkError::ProgramError(err)(orLightSdkError::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 PDAsLine [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 firstSome(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 valueLine [506] still builds a
HashSet<&Pubkey>, so duplicate entries with equal value but distinct backing slots aren’t caught. StorePubkeyby 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_SPACEgrows beyond 1.sdk-libs/sdk/src/compressible/compression_info.rs (2)
319-320: ReturnNonewhen no rent was claimedLine [319] still returns
Ok(Some(0)), contradicting the doc comment that promisesNonewhen nothing is claimable. Please returnOk(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_cpibuilds the instruction withPubkey::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_rentcan never move lamports.programs/system/Cargo.toml (1)
28-29: Profile feature refactoring looks intentional.The
profile-programfeature is now empty andprofile-heapno longer depends onlight-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 satisfyMD041.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 emptyaddress_spacebefore indexing
prepare_account_for_compressionstill indexesaddress_space[0]directly when deriving the compressed PDA. This will panic ifaddress_spaceis 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 accountThe new rent‑based gating looks coherent (it builds an
AccountRentState, checksis_compressible, and logs detailed context), but the function still allowsowner_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
CpiAccountsearly.Also applies to: 114-117
sdk-libs/macros/src/rent_sponsor.rs (2)
11-12: Tighten macro argument validation and avoid silently defaulting invalid versionsTwo correctness issues remain in the proc‑macro API:
Extra arguments are silently ignored
Args::parseaccepts an arbitrary number ofExprarguments but only consumeselems[0]andelems[1]. If a caller passes a third parameter, it’s dropped without any error, which is very hard to debug at call‑sites.Invalid version literals quietly become
1Both macros parse the version via
lit.base10_parse::<u16>().unwrap_or(1u16). Any parse error (including out‑of‑range literals) silently coerces to version1, 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 = 1when 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 logicBoth
derive_light_rent_sponsor_pdaandderive_light_rent_sponsorperform the same sequence: parse args, validate the program ID, compute version, build seeds, callPubkey::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: Makelight-compressible-clientoptional and gate it viadevenv
light-compressible-clientis pulled in unconditionally, while all of the other compression‑related crates here are optional and enabled viadep:in thedevenvfeature. As noted in earlier reviews, keeping this dependency non‑optional will block publishinglight-program-testuntillight-compressible-clientis published, and it also drags anchor‑based code into consumers that don’t enabledevenv.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-testand thatdevenvbuilds 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 lookupThe two safety bugs called out previously are still present:
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 returnProgramError::InvalidAccountDatainstead.
cpi_accounts.cpi_context()is invoked unconditionally, so callers without a CPI-context PDA hitMissingRequiredSignatureeven whenhas_pdasisfalse. Derive the optional pubkey only when needed and feed that into the instruction builder.While you’re here, the boxed
Vecis 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 ofProgramError::Custom(0)for config mismatchesHard-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 (whereLightSdkError::ConstraintViolationis 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 ProgramErrorimpls; 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_checkedand other callers.
56-71: Add bounds checks before slicing PDA/account arrays to avoid panics on malformed inputsThe current indexing logic assumes consistency between
remaining_accounts,compressed_accounts, and the internalCpiAccountslayout: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_offsetorsystem_accounts_startexceeds the available account count, or ifcompressed_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 generatedDefaultsemantics forCompressedAccountVariantexplicit
Defaultcurrently 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:
- Generate a doc comment on
CompressedAccountVariantexplaining that the first account type becomes the default (with an example), or- Extend the macro input syntax to allow specifying an explicit default type (preferred), and emit
Defaultbased 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 aroundunreachable!()variants (esp. packed/CToken cases)Across the generated impls you treat several variants as unreachable:
CompressedAccountVariant::Packed{Type}inDataHasher,HasCompressionInfo,Size,Pack, andUnpackCompressedAccountVariant::PackedCTokenData/CTokenDatain the same traits, withPackedCTokenData’sUnpackdoingOk(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, orsizeon a packed or CToken variant will panic at runtime.PackedCTokenData::unpackreturningOk(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
CompressedAccountVariantthat spells out which variants are valid for which traits and that using the others will panic, and- Making
PackedCTokenData’sUnpackbranch 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 driftWithin
generate_ctoken_seed_provider_implementation, you repeat the same string list to decide whether actx.accounts.*field is a plainAccountInfoor anOption<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 logsIn
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::ConstraintViolationequivalent 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 invariantsTwo related issues in
process_decompress_accounts_idempotent:
No rent-sponsor (or authority) check vs config
You load
CompressibleConfigand use itsaddress_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 incompression_config. On the compress side,process_compress_pda_accounts_idempotentdoes 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:DataHasherstill aliases the SHA variant, notLightHasheras documentedThe docs say “Alias of
LightHasher,” but the implementation delegates toderive_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 trackedThe fully commented
LightDiscriminatorShaandadd_native_compressible_instructionsblocks 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:TokenSeedSpeclookup comparesIdenttoStringand will not compileIn
pda_seed_provider_impls, you look up theTokenSeedSpecfor each account type by comparing asyn::Identto aString: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 { ... };
Identdoes not implementPartialEq<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.keyand avoid brittleunwraprewritesThe expression rewriter in
generate_pda_seed_derivation_for_traitcurrently mapsctx-based paths to invalid field accesses:
ctx.accounts.foobecomes*accounts.foo.as_ref().unwrap().keyctx.foobecomes*accounts.foo.as_ref().unwrap().keyProblems:
Invalid
.keyfield accessTypes like
Signer<'info>andUncheckedAccount<'info>(and theOption<UncheckedAccount<'info>>wrapper used inDecompressAccountsIdempotent) don’t expose a public.keyfield. They implement theKeytrait and provide a.key()method, typically returning aPubkey. Generating.keywill not compile.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().Pubkeydoes not have akey()method, so this also fails to compile.Unchecked
unwrap()on optional accountsFor optional seed accounts (e.g. fields added as
Option<UncheckedAccount<'info>>inDecompressAccountsIdempotent),.as_ref().unwrap()will panic if the caller forgets to pass the account, rather than surfacing a structuredCompressibleInstructionError::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.fieldaccess, useto_account_info().key()instead of assuming anOption:- } 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 yieldMissingSeedAccountinstead of panicking, but that would require adjusting thePdaSeedDerivationtrait 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
sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs
Show resolved
Hide resolved
| 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)) |
There was a problem hiding this comment.
🧹 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
ClockandRentsysvars, - Builds an
AccountRentStatewithnum_bytes,current_slot,current_lamports, andlast_claimed_slot, - Calls
calculate_close_distributionand then movesdist.to_userto the fee payer anddist.to_rent_sponsorto the rent sponsor, withchecked_sub/checked_addto guard under/overflow.
Two minor refinements:
-
Both
checked_addfailures are mapped toProgramError::Custom(0), which collapses a rare but distinct condition into a generic code that may collide with other errors. Consider adding a dedicated variant toCompressibleInstructionError(e.g.OverflowDuringRentDistribution) and mapping to that instead. -
If this ever becomes a hot path over many accounts, consider hoisting the
Clock::get()andRent::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.
| #[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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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_typeincompressible::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.
| 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(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| /// 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 -100Length of output: 3310
🏁 Script executed:
#!/bin/bash
# Search for usages of is_copy_type
rg -n 'is_copy_type' --type rust -B2 -A2Length of output: 6137
🏁 Script executed:
#!/bin/bash
# Look at the file more carefully to understand array handling
rg -n 'Type::Array' --type rust -B5 -A5Length 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).
| #[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, | ||
| } |
There was a problem hiding this comment.
🧹 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.
| // 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), | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
CompressionInfo initialization using Clock is correct, but verify error handling.
The new initialization path:
- Gets current slot via
Clock::get()?.slot(line 72) ✓ - Creates
CompressionInfofrom config withnew_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.
There was a problem hiding this comment.
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_sponsorbranch currently can’t hit theSomearmInside
compress_and_close_ctoken_accounts,rent_sponsor_pubkeyis always initialized toNoneat the start of each loop iteration and only set inside theelsebranch of theactual_rent_sponsorif 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_pubkeyfrom 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-profilerremains required despite unused features.This appears to be the same concern previously flagged. Both
profile-program(line 28) andprofile-heap(line 29) no longer referencelight-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:
- If
light-program-profileris unused across the codebase: Remove the dependency from line 55 entirely.- If
light-program-profileris 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-clientshould be made optional to unblock publishinglight-program-test. The previous review marked this as critical, noting:"Make this optional again else we cannot publish a new version of
light-program-testuntil we publishedlight-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-clientto thedevenvfeature 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_configallows a caller-specifiedconfig_bump:let config_bump = config_bump.unwrap_or(0); let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump);but
update_compression_configalways derives the config PDA with a hardcoded bump of0:let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0);If anyone initializes with a non-zero bump,
update_compression_configwill 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: u8parameter toupdate_compression_config(and thread it through toderive_pda), or- If
0is the only supported bump, remove theOption<u8>frominitialize_compression_configand hardcode0there too, documenting that only a single config instance is supported.Right now the surface suggests arbitrary bumps are supported, but only
0is 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
unwrapit: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:
compressed_accounts[0]might be a pure token withcpi_context == None, which will panic onunwrap().- 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 discriminatorLeaving the old
COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATORvalue 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 useso it’s obvious which discriminator is authoritative.
287-299:accounts_to_compressparameter is still unused beyond a length checkIn
compress_accounts_idempotent, you accept bothaccount_pubkeysandaccounts_to_compress, but after the length check you never actually use theAccountdata: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_compressfrom 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 derivingDefaultforRentSponsorplaceholdersSince
RentSponsoris a simple POD-like struct used across crates, supportingRentSponsor::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_rentreturn value still contradicts its documentationThe doc says:
Returns
Some(claimed)if any lamports were claimed;Noneif account is compressible or nothing to claim.The implementation:
- Returns
Ok(None)when the account is compressible (good), but- Returns
Ok(Some(0))whencalculate_claimable_rentyieldsNoneorSome(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 returnOk(Some(amount))whenamount > 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 intransfer_lamports_cpi
transfer_lamports_cpicurrently 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 whentop_up_renttries to transfer lamports.You already receive
system_program: &AccountInfoas a parameter; that should be used as the instruction’sprogram_idinstead, 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.keyis 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 magiccompression_infofieldThe derive is powerful, but there are a few important implicit assumptions that are still not enforced in code:
Generics are not handled at all
derive_compressible_packand the helper fns emit impls for#struct_nameandPacked#struct_namewithout threading throughinput.genericsor where-clauses. If someone writesstruct Foo<T> { .. }and applies this derive, the expansion will try toimpl 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 namedclone” error from inside the generated code. At minimum, call this out clearly in the derive’s docs (GUIDE + rustdoc) so consumers know to deriveCloneon any struct using this macro.
Magic by-name behavior for
compression_info
Any field literally namedcompression_infois forced toNonein 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_infowould silently lose its data. The new GUIDE does mention the expectedcompression_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_pdasstill ignorescompression_authorityfrom config and can silently no-opThe 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_chunkswallowing per-account errors, this has two practical consequences:
Wrong authority when config uses a distinct key
As soon ascfg.compression_authoritydiffers frompayer, 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, soauto_compress_program_pdasquietly becomes a no-op for those programs.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_metasand sign the transaction with the corresponding keypair in tests, or- Explicitly document that in test setups
cfg.compression_authoritymust equal the payer (and enforce that invariant before calling this helper).Separately,
derive_pda(&program_id, 0)hard-codes version/bump0. 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 is0when 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_authoritywould 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 to1Two behaviors here are risky for a PDA that controls rent flows:
Args::parseaccepts 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.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 version1, deriving the wrong rent‑sponsor PDA with no compiler signal.I’d strongly recommend:
- Enforce
elems.len() <= 2inArgs::parse, returningsyn::Errorwhen there are extra arguments.- Replace
unwrap_or(1u16)with explicit error propagation, preserving the default of1only 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_pdaandderive_light_rent_sponsorboth repeat the same core steps: parseArgs, validate the program id string, computeversion_u16, build seeds, callPubkey::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_infohandling looks good; tightenRent::geterror mapping and owner/payer assumptionsThe new pieces here are mostly solid:
- Computing
spaceasdiscriminator_len + T::size(&light_account.account) + CompressionInfo::INIT_SPACEcorrectly accounts for switchingcompression_info: None→Some(...)on the decompressed PDA, assumingINIT_SPACEis the serialized size ofCompressionInfo.- Initializing
compression_infowithSome(CompressionInfo::new_decompressed()?)before serializing into the PDA keeps the on‑chain representation consistent with the v2 rent model.- Threading
rent_sponsorthrough toinvoke_create_account_with_heapand using it as the payer matches the new naming.Two previously‑raised issues remain, though:
Rent::geterror mapping (Lines 108–111, unchanged here)
Mapping a sysvar access failure toLightSdkError::Borshis misleading; this isn’t a serialization problem. It would be clearer and more consistent with other CPI/sysvar sites to wrap the underlyingProgramErrorinLightSdkError::ProgramError(or viaFrom<ProgramError>), while still logging the cause.Program owner consistency & payer semantics
- The created account is owned by
cpi_accounts.self_program_id(), whileLightAccount::<T>::new_closeis parameterized byprogram_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 threadingprogram_idthrough to the helper) would guard against configuration bugs.- Now that
rent_sponsoris 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 theDecompressContextimplementations always satisfy System Program’s expectations for thefromaccount increate_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_contextcurrently:
- Uses
input.generics.lifetimes().next()and silently takes the first lifetime.- Parses
#[pda_types(...)]and#[token_variant(...)]payloads as bareIdents.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::Pathinstead ofIdentso 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 runtimeThe core structure of
process_decompress_tokens_runtimeis solid, but the previously reported critical issues are still present:
Packed account indexing can panic (on-chain abort):
Lines 72–75 indexpacked_accounts[mint_index as usize]andpacked_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 againstpacked_accounts.len()and returnProgramError::InvalidAccountDataon failure instead of panicking.
has_pdasflag does not guardcpi_context()acquisition:
Lines 64–69 callcpi_accounts.cpi_context()unconditionally, even whenhas_pdasisfalse. This means a caller that legitimately has no CPI context account will still fail withMissingRequiredSignaturebefore the flag is consulted. Compute anOption<Pubkey>instead, only callingcpi_context()whenhas_pdasis true, and pass that down todecompress_full_ctoken_accounts_with_indices.Unnecessary boxing of
token_decompress_indices:
Lines 58–59 allocate aBox<Vec<_>>, but only&token_decompress_indicesis ever used. A plainVecavoids an extra heap indirection with no downside.Unused
get_authority_seedsinCTokenSeedProvider:
The trait exposesget_authority_seedsbut 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 duplicatecompression_infoinitialization and size double-counting in generated implsThe structural issues around
compression_infonoted earlier are still present:
generate_compress_as_implalways emitscompression_info: None,in the struct literal.generate_compress_as_field_assignmentswalks all fields (except#[skip]), so it will also emit an assignment for thecompression_infofield (self.compression_infoor an override), producing a duplicate field initializer and a hard compile error.generate_size_impladds acompression_info_sizeconstant, butgenerate_size_fieldswill also includecompression_infoin the serialized-length sum, effectively double-counting it and overestimating account size.You can fix this by treating
compression_infoas 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_infounder explicit control in bothCompressAsandSize, 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
DefaultforCompressedAccountVariantstill 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 consistentAcross the generated impls (
DataHasher,HasCompressionInfo,Size,Pack,Unpack), mostPacked*and CToken variants are handled viaunreachable!(), 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::unpackbehaves differently from other packed variants.To make this clearer and more consistent:
- Add a generated doc comment on
CompressedAccountVariantexplaining which variants are valid for which traits and that others will panic if used incorrectly. Mention thatPackedCTokenDatais the only supported input for token decompression paths, if that’s the intent.- Consider making
PackedCTokenData’sUnpackarm 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 comparePubkeyvalues, not references
validate_address_space_no_duplicatesis still building aHashSetover&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_spaceever allowslen > 1, two identicalPubkeyvalues at different indices will not be flagged.Switch the set to store
Pubkeyby 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 helperTwo things worth tightening here:
Opaque
ProgramError::Custom(0)for config mismatches (Lines 49–53)
ReturningProgramError::Custom(0)whenctx.rent_sponsor()orctx.compression_authority()disagrees withCompressibleConfigobscures the failure mode and diverges from the rest of the compressible module, which already maps validation issues toLightSdkError::ConstraintViolationviaFrom<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(), andcompressed_accounts.len() <= all_post_system.len().If either is violated (e.g., a malformed
remaining_accountsarray or an incorrectcompressed_accountscount), 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 maintenanceThe 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.*andctx.*(Lines ~95–106, 137–148).- Authority seeds path for
ctx.accounts.*andctx.*(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 withis_standard_account_field(field_name_str.as_str()).This keeps all branches (token vs. authority,
ctx.accountsvs.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_typehelper with the one incompressible::utilsto avoid divergent definitions over time.sdk-libs/macros/src/lib.rs (1)
232-237: AlignDataHasherderive implementation with its “Alias of LightHasher” documentationThe
DataHasherderive 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 usesderive_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 panicsTwo robustness gaps in
process_decompress_accounts_idempotent:
Rent-sponsor invariants are not enforced (Lines 236–239)
You loadCompressibleConfigforaddress_space, but unlike the compression path you never verify thatctx.rent_sponsor()matchescompression_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: FixTokenSeedSpeclookup: comparingIdenttoStringstill breaks compilationIn
pda_seed_provider_implsyou 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.variantis asyn::Identandname_stris aString;Identdoes not implementPartialEq<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 uncheckedunwrap()
generate_pda_seed_derivation_for_trait(and its helpermap_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>andUncheckedAccount<'info>, the public API is via theKeytrait (.key()method) rather than a.keyfield, and optional accounts are wrapped inOption. The generated code is therefore brittle:
- It mixes field and method access and can end up calling
.key()on aPubkey.- It force-unwraps optional seed accounts (
.as_ref().unwrap()), so a missing optional account will panic instead of returningCompressibleInstructionError::MissingSeedAccount, unlike the more defensive c-token seed provider.To make PDA seed derivation safe and consistent, you should:
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() };Mirror the token seed provider’s error behavior by avoiding
unwrap()and usingok_or/ok_or_elseinstead.Consider making
PdaSeedDerivation::derive_pda_seeds_with_accountsreturn aResult<(Vec<Vec<u8>>, solana_pubkey::Pubkey), solana_program_error::ProgramError>so thatMissingSeedAccountcan be propagated cleanly fromhandle_packed_pda_variantinstead of panicking.As it stands, any PDA seed that refers to optional accounts (or uses
.key()) risks either compilation failures or on-chain panics.
| 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, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| /// 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Refine Copy detection for arrays and unify Pubkey helpers
Two small correctness/maintainability concerns:
-
is_copy_typetreats everyType::Array(_)as Copy. In Rust,[T; N]isCopyonly ifT: Copy. For arrays of non‑Copy types this will misclassify the field and cause the generatedcompress_asimpl to try to copy by value instead of cloning, resulting in confusing compile errors. Better to inspect the element type and recurse intois_copy_typerather than blanket‑marking arrays as Copy. -
is_pubkey_typehere only matches identPubkey, but there’s still a separateis_pubkey_typeinseed_providers.rsthat uses a loosercontains("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.
sdk-libs/macros/src/cpi_signer.rs
Outdated
| 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() | ||
| } |
There was a problem hiding this comment.
🧹 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.
| /// 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() | ||
| } |
There was a problem hiding this comment.
🧹 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.
| #[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, |
There was a problem hiding this comment.
🧹 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")] |
There was a problem hiding this comment.
| #[cfg(feature = "devenv")] | |
| #[cfg(feature = "v2")] |
| ) -> Result<Response<Items<[u8; 32]>>, IndexerError> { | ||
| #[cfg(not(feature = "v2"))] | ||
| #[cfg(not(feature = "devenv"))] | ||
| unimplemented!("get_subtrees"); | ||
| #[cfg(feature = "v2")] | ||
| #[cfg(feature = "devenv")] |
sdk-libs/macros/src/rent_sponsor.rs
Outdated
| } | ||
| }; | ||
|
|
||
| let seeds = &[b"rent_sponsor".as_slice(), &version_u16.to_le_bytes()[..]]; |
There was a problem hiding this comment.
can we use a method for the derivation?
sdk-libs/macros/src/rent_sponsor.rs
Outdated
| let seeds = &[b"rent_sponsor".as_slice(), &version_u16.to_le_bytes()[..]]; | ||
| let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id); |
There was a problem hiding this comment.
can we use a method for the derivation?
| let pda_bytes = pda.to_bytes(); | ||
| let bytes = pda_bytes | ||
| .iter() | ||
| .map(|b| proc_macro2::Literal::u8_unsuffixed(*b)); |
There was a problem hiding this comment.
we could add a helper function that returns [#(#program_id_literals),*] this is used in multiple places
sdk-libs/macros/src/lib.rs
Outdated
| /// #[derive(Compressible)] | ||
| /// #[compress_as(start_time = 0, end_time = None, score = 0)] | ||
| /// pub struct GameSession { | ||
| /// #[skip] |
There was a problem hiding this comment.
#[skip] is not applied if we use sha256::LightAccount (the default LightAccount)
sdk-libs/macros/src/lib.rs
Outdated
| pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream { | ||
| cpi_signer::derive_light_cpi_signer(input) | ||
| } |
There was a problem hiding this comment.
pls remove I moved this to program-libs/macros since it is used in the ctoken program
| /// | ||
| /// ## Example | ||
| /// | ||
| /// ```ignore |
There was a problem hiding this comment.
| /// ```ignore | |
| /// ```rust |
| /// | ||
| /// ## Example | ||
| /// | ||
| /// ```ignore |
There was a problem hiding this comment.
| /// ```ignore | |
| /// ```rust |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
sdk-libs/macros/src/lib.rs (1)
242-247:DataHasherderive still doesn’t match its documentation.This entry point is documented as an alias of
LightHasher, yet it forwards toderive_light_hasher_sha. Either the implementation must callderive_light_hasheror 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
⛔ Files ignored due to path filters (3)
scripts/lint.shis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/lib.rsis 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...
| #[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)) | ||
| } |
There was a problem hiding this comment.
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.
| #[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.
There was a problem hiding this comment.
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
LightDiscriminatorShahas 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 completeBased on learnings
291-295: Addattributes(skip)to allow#[skip]attribute as documented.The documentation shows using
#[skip]on thecompression_infofield (line 278), but the proc_macro_derive declaration doesn't whitelist this attribute. Users following the documentation will encounter "cannot find attributeskip" 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
📒 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 callsderive_light_hasher_sha(SHA256 variant) instead ofderive_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
DataHasheraliases the SHA-based hasher, not the Poseidon-basedLightHasher.-/// 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 theinto_token_streamhelper 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_streamfor error handling and the well-structured examples (appropriately marked withignore) demonstrate attention to lessons from previous reviews. The only critical issue is the missingattributes(skip)declaration forHasCompressionInfo(flagged separately).Also applies to: 332-335, 366-372, 447-450, 474-477, 564-569, 584-586, 600-602, 617-619
| /// | ||
| /// ## Example | ||
| /// | ||
| /// ```ignore |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Use rust language identifier for code fence.
Specify the language identifier for better syntax highlighting in the generated documentation.
-/// ```
+/// ```rustBased 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 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Use rust language identifier for code fence.
Specify the language identifier for better syntax highlighting in the generated documentation.
-/// ```
+/// ```rustBased 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.
|
@coderabbitai reset |
|
@SwenSchaeferjohann: I'll reset and perform a fresh review of the entire PR. ✅ Actions performedFull review triggered. |
|
@coderabbitai pause |
✅ Actions performedReviews paused. |
There was a problem hiding this comment.
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:
- Line 23:
light-compressed-token-sdkis unconditional but only used behind#[cfg(feature = "devenv")]throughout the codebase- Line 33:
light-compressible-clientis unconditional but only used behind#[cfg(feature = "devenv")]throughout the codebase- Line 10: The
devenvfeature lacks activation entries for both dependenciesThe 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: Alignwarp_slot_forwardtrait docs with the new PDA auto-compression behaviorThe
LightProgramTest::warp_slot_forwardimplementation now clearly auto-compresses PDAs for allauto_mine_cold_state_programsin addition to ctoken accounts (lines 155–162), but theTestRpctrait‑level doc comment at Lines 107–110 still only mentions “compressible ctoken accounts.” To avoid confusion for otherTestRpcimplementors 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 appropriateThe new
auto_mine_cold_state_programs: Vec<Pubkey>field (Line 37) and itsVec::new()initialization innew()(Line 83) are consistent with howwarp_slot_forwarduses this list to drive PDA auto‑compression. Given the expected small number of programs in tests, aVecis a reasonable choice; if this ever grows large or becomes highly dynamic, consider aHashSetto 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 totrue(devenv)This will auto‑register custom programs for PDA compression in all
ProgramTestConfig::default()‑based flows underdevenv. 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 tofalseexplicitly.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 missingdiscriminatorparameter to doc commentThe
discriminator: &[u8]parameter at line 132 is documented nowhere in the function's doc comment (lines 110–122). Add it to the# Argumentssection to clearly explain its purpose—specifically, that it's the discriminator forwarded to the underlyingcompressible_instruction::update_compression_configcall.
61-99: Update doc comments to documentdiscriminatorandconfig_bumpparametersConfirmed:
initialize_compression_config(lines 64–72) acceptsdiscriminator: &[u8]andconfig_bump: Option<u8>, but the doc comment (lines 54–63) stops ataddress_space. Similarly,update_compression_config(lines 103–114) acceptsdiscriminatorbut the doc comment (lines 94–102) omits it.Both functions correctly forward these parameters into their respective
compressible_instructioncalls, so the implementation is sound. Please update both doc comments to include descriptions of:
discriminator– its purpose and expected shapeconfig_bump– its purpose and when to useNonevsSome(_)This keeps the module self-documenting for callers.
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)
342-369: Remove deadif let Some(sponsor)branch and clarifypacked_accountsexpectationsThe outer
if let Some(sponsor) = rent_sponsor_pubkeyat line 343 is unreachable—rent_sponsor_pubkeyis initialized toNoneat 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_accountsorder 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 forclose/ runtime traitsThe new
compress_runtime/decompress_idempotent/decompress_runtimemodules and their re-exports are consistently gated underfeature = "v2", which keeps the expanded runtime surface opt‑in. One small behavioral change is thatclose::closeis only re-exported under v2, even though the module itself is always compiled; non-v2 consumers now need to importcrate::compressible::close::closeinstead ofcrate::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/DecompressContextwhich 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 currentToByteArraytrait, not the removedAsByteVecThe
light-hashercrate now uses the ToByteArray trait for byte conversion. The docstring at lines 181-207 incorrectly states that fields must implementAsByteVec, which is not present in the current code.Update the comment to reference
ToByteArrayor provide guidance on the actual trait(s) thatlight-hasherexpects, 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'binstead of suppressing Clippy
compress_and_close_ctoken_accounts_signed<'b, 'info>(...)doesn’t use'bin 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_lifetimesallow.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 macrosBoth
derive_light_cpi_signer_pdaandderive_light_cpi_signer:
- Parse the same
LitStrprogram ID,- Validate it via
Pubkey::from_str, and- Derive the
"cpi_authority"PDA withfind_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
CpiSignervariants.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 controlsCompressedAccountVariant’sDefaultThe
Defaultimpl always picks the first account type passed intocompressed_account_variantand calls itsDefault. That’s a reasonable convention, but it’s implicit: reordering the macro arguments silently changes the default.Consider either:
- Emitting a doc comment on
CompressedAccountVariantexplaining 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 andunreachable!()invariants on variants explicitAcross
DataHasher,HasCompressionInfo,Size,Pack, andUnpack, mostPacked*and the CToken‑related variants are handled viaunreachable!()(exceptPackedCTokenData::unpack, which clonesself). 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
CompressedAccountVariantthat 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
PackedCTokenDataUnpackimpl consistent with the rest (either alsounreachable!()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 valuesDespite the past comment claiming this was addressed,
validate_address_space_no_duplicatesstill builds aHashSetthat infersHashSet<&Pubkey>from the context. When iteratingfor pubkey in address_space,pubkeyis&Pubkey, soseen.insert(pubkey)stores the reference, not the value.With the current
MAX_ADDRESS_TREES_PER_SPACE == 1this 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_configallows a configurableconfig_bump(line 89), butupdate_compression_configalways hardcodes0at 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_bumpparameter toupdate_compression_configor remove theOption<u8>frominitialize_compression_configand hardcode 0 in both places.
206-207: Avoid panicking on missing CPI context when mixing tokens and PDAsLine 207 calls
.unwrap()oncompressed_accounts[0].0.tree_info.cpi_context, which will panic if the first account is a token withcpi_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 unusedaccounts_to_compressparameterThe
accounts_to_compressparameter 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-codedProgramError::Custom(0)withLightSdkError::ConstraintViolationThe validation checks return opaque
Custom(0)errors, making debugging difficult and potentially colliding with other uses of error code 0. The codebase already hasLightSdkError::ConstraintViolationfor exactly this purpose (mapped to error code 16001), andconfig.rsuses 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 arraysTwo unguarded slice operations will panic with attacker-controlled inputs:
- Line 67:
&cpi_accounts.to_account_infos()[system_accounts_start..]panics ifsystem_accounts_startexceeds the list length- Lines 70-71:
all_post_system.len() - compressed_accounts.len()underflows ifcompressed_accountsis longer, then the slice panicsSince
remaining_accountsis external CPI input, these invariants aren't guaranteed. Add bounds checks that returnLightSdkError::ConstraintViolationbefore 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_cpiuses a zeroed array as the System Program ID (lines 340-343), which doesn't match the real System Program ID (11111111111111111111111111111111). Every call totop_up_rentwill fail when attempting the CPI.You already receive
system_program: &AccountInfoas 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 documentationThe doc comment promises
Nonefor "nothing to claim," but line 319 returnsOk(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 calculationIf
rent_curve_per_epochreturns 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 unusedlight-ctoken-typesdependency
light-compressibleis actively used (imports incompress_account.rs,compression_info.rs, andconfig.rs), butlight-ctoken-typeshas no direct imports in the SDK source. The program-test crate gates it behind adevenvfeature, which is the more appropriate pattern.Remove the unconditional
light-ctoken-typesdependency or make it optional and feature-gated like inprogram-test/Cargo.toml.sdk-libs/sdk-types/src/lib.rs (1)
22-28: Consider addingDefaultfor ergonomic placeholder constructionIf
RentSponsoris ever constructed with zeroed values as a placeholder, adding#[derive(Default)]would enableRentSponsor::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 markdownlintThe 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, butaddress_spaceand owner invariants still need hardeningThe new rent-based gating via
AccountRentStateandRent::get()is a good replacement for the old slot-delay check and the error logging is nicely contextual.Two invariants remain fragile here:
Potential panic on empty
address_spacelet derived_c_pda = derive_address( &account_info.key.to_bytes(), &address_space[0].to_bytes(), &program_id.to_bytes(), );If
address_spaceis ever empty (e.g., misconfigured macro use or a future call site outside the current flow), this will panic instead of returning aProgramError. For an on-chain SDK helper, it’s safer to guard explicitly and fail gracefully.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_idbut useowner_program_idto own the compressed account. If these ever differ (misconfiguredCpiAccounts), 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 PDAsFor macros that derive PDAs controlling rent flows, the current argument handling is too forgiving:
Extra arguments silently ignored
Args::parsestores onlyelems[0]andelems[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", + )); + }
Invalid version literals quietly fall back to
1Both 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, + };
Shared PDA derivation logic
derive_light_rent_sponsor_pdaandderive_light_rent_sponsorduplicate 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: MapRent::get()failures to aProgramError-backed variant instead ofLightSdkError::Borsh
Rent::get()returns aProgramErrorfor sysvar access issues, but this code wraps failures asLightSdkError::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 existingFrom<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 betweencompressed_accountsandsolana_accountsto avoid on-chain panics
collect_pda_and_tokeniteratescompressed_accountsand uses the loop indexito indexsolana_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 aProgramError.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 bareIdents 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::Pathinstead ofIdentfor 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: Usecfg.compression_authorityinstead of forcingcompression_authority = payer
auto_compress_program_pdasdeserializesCpdaCompressibleConfigbut then ignores the on-chaincompression_authorityand hard-codes it topayer.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_chunkswallows 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 == payerwhen 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: Fixis_copy_typefor arrays: only mark[T; N]asCopywhenTisCopy
is_copy_typecurrently treats all arrays asCopy:match ty { Type::Path(type_path) => { /* ... */ } Type::Array(_) => true, _ => false, }In Rust,
[T; N]isCopyonly whenT: Copy. For arrays of non-Copytypes (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
Copysemantics.sdk-libs/macros/src/compressible/traits.rs (1)
86-129: Treatcompression_infoas a reserved field: avoid duplicate initialization and double‑counting in generated implsRight now the macro both:
- Hardcodes
compression_info: Nonein theCompressAsstruct literal, and- Includes
compression_infoin 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_assignmentsandgenerate_size_fieldsonly skip fields with#[skip], so for any struct that does have acompression_infofield (whichvalidate_compression_info_fieldrequires forderive_compressible), the macro will:
- Emit two initializers for
compression_info(compile error), and- Count its serialized length again in
size(), on top ofcompression_info_size.You can fix this by treating
compression_infoas 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_ascallvalidate_compression_info_fieldso standalone#[derive(CompressAs)]gets the same clear diagnostics asderive_compressiblewhen the field is missing.As-is, any struct using
derive_compressiblewill 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:DataHasherderive is documented as alias ofLightHasherbut still calls SHA variantThe docs for
DataHashersay:/// 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
DataHashereffectively an alias ofLightHasherSha, notLightHasher. 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 aliasesLightHasher, or- Update the docstring to clearly state that
DataHasheris 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:HasCompressionInfoderive does not whitelist#[skip]attribute as docs requireThe new
HasCompressionInfoderive 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 attributeskipin 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_infoas skipped without breaking compilation.
374-397: Large commented-outadd_native_compressible_instructionsblock should be removed or trackedThere is still a substantial commented‑out implementation of
add_native_compressible_instructionsand its docs. As with the earlierLightDiscriminatorShablock, 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_instructionsand 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 bugThe authority‑seed path mirrors the token‑seed expression handling, including the same
is_standard_fieldlist and.key()vs.as_ref()split. That means thectoken_*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 localis_pubkey_typeduplication; reuse the shared helperThis file defines its own
is_pubkey_type(ty: &syn::Type)again, butcompressible::utilsalready exposes a canonical helper for this. Keeping two subtly different implementations makes it harder to reason about howdata.*fields are mapped into client parameters and increases drift risk.Prefer:
- Exporting a single
is_pubkey_typefromcompressible::utils, and- Importing and using it here in both
analyze_seed_spec_for_clientandanalyze_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 -C3sdk-libs/macros/src/compressible/instructions.rs (6)
53-141: Parser still mishandles seeds starting withctx.*/data.*and similar identifier expressionsThe
TokenSeedSpecparser 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/dataidentifier, dropping.accounts.user/.index. The resulting seed becomes an invalidSeedElement::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_authorityis still applicable:
- Use a fork/peek to check whether the first ident is
is_token,is_ata,true,is_pda, orfalse.- 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_variantcurrently 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
PdaOnlyandTokenOnlyas impossible viaunreachable!():
generate_decompress_accounts_struct(Lines 1441–1488)generate_error_codes(Lines 1550–1553)compress_accountsstruct 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: comparingsyn::IdenttoStringinpda_seed_provider_implsIn 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.variantis asyn::Ident, andname_stris aString.Identdoes not implementPartialEq<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) errorThe rent close distribution logic:
- Loads
ClockandRentsysvars,- Computes
AccountRentStateand callscalculate_close_distribution,- Uses
checked_subandchecked_addto move lamports to the fee payer and rent sponsor.The structure looks correct and defensive, but both
checked_addfailures map toProgramError::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.keyfield andunwrap()on optional accounts
generate_pda_seed_derivation_for_trait’smap_pda_expr_to_paramsstill rewrites:
ctx.accounts.fooas*accounts.foo.as_ref().unwrap().keyctx.fooas*accounts.foo.as_ref().unwrap().keyIssues:
Invalid accessor for most account wrapper types
Signer<'info>andUncheckedAccount<'info>don’t expose a public.keyfield; they implement theKeytrait and provide a.key()method (or deref intoAccountInfo).- The generated
*accounts.foo.as_ref().unwrap().keywill 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.Unchecked
unwrap()on optional accountsEverywhere else (e.g., in
generate_ctoken_seed_provider_implementation) you return a structuredCompressibleInstructionError::MissingSeedAccountinstead 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 */)?.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 behaviorIn
generate_decompress_accounts_struct:
InstructionVariant::PdaOnlyandTokenOnlyarms still callunreachable!(), 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 earlierinstruction_variantmatch.)The
rent_sponsorfield is documented as:/// CHECK: anyone can pay #[account(mut)] pub rent_sponsor: UncheckedAccount<'info>but the runtime path in
compress_runtimeanddecompress_runtimetreatsrent_sponsoras a specific PDA configured inCompressibleConfig, not an arbitrary payer. This comment is now misleading.Suggestions:
- Either disallow
PdaOnly/TokenOnlyat argument‑parsing time, or implement them and remove theunreachable!()calls.- Update the
rent_sponsordoc to describe it as “the program’s configured rent sponsor PDA; checked againstCompressibleConfigat 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_variantchecksderived_pdavs 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_idempotentFor 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 panicIn
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 validaterent_sponsoragainstCompressibleConfigAfter 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().keyequalscompression_config.rent_sponsor, but the decompress side does not. That means a program could decompress using a differentrent_sponsorthan 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
InvalidRentSponsoror 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 thisin non‑CPI context branchThe non‑
cpi-contexthas_pdaspath 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-contextimplementation, 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
⛔ Files ignored due to path filters (48)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by nonescripts/lint.shis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/processor.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/variant.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/lib.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/user_record_tests.rsis 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.rssdk-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 |
There was a problem hiding this comment.
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.
| 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() } | ||
| } |
There was a problem hiding this comment.
🧩 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].keyIf 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.rsLength 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 -C2Length 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 -20Length 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 -20Length 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 -40Length 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 -A2Length 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].keyIf 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.
| 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) | ||
| }; |
There was a problem hiding this comment.
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: Defaultis for “no seed params used”, and/or - Returning an error when
seed_paramsisNonebutSactually 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
1f6e5b1 to
53aa4c6
Compare
There was a problem hiding this comment.
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 accessstate_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 correctUsing
RENT_SPONSOR_SEEDplusversion.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 forinvoke_signed, consider a sibling helper that also returns the&[&[u8]]seeds array (similar toget_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 fundingcreate_account.
invoke_create_account_with_heapdebitsrent_sponsorbut never provides signer seeds for it. Because the rent sponsor is a PDA,create_accountwill immediately fail withMissingRequiredSignature—the helper signs only for the destination PDA. Thread the rent-sponsor seeds (or derive them here) and pass both signer sets toinvoke_signed; otherwise decompression can never create the destination account when rent is supposed to come from the sponsor PDA. citeturn0files3
♻️ 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_configallows callers to pass a customconfig_bump(defaulting to 0 at line 89), butupdate_compression_configalways hardcodes0at line 138. If a non-zero bump is used during initialization, updates will target the wrong PDA.Either add a
config_bumpparameter toupdate_compression_configor remove theOption<u8>frominitialize_compression_configand document that only bump0is 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 hasNoneforcpi_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_compressparameter 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
Accountdata 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()onstate_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, butaddress_space[0]can still panic on empty slicesThe new rent-function gating via
AccountRentState::is_compressibleis a nice improvement: it enforces rent‑based eligibility and logs detailed diagnostics (slot, lamports, bytes, rent exemption, last_claimed_slot, rent_config) before returningConstraintViolation.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_spaceis ever empty (e.g., a misconfigured caller outside the standard macro flow), this will panic rather than returning aProgramError, 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()matchesprogram_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 descriptionTwo small doc/consistency items here:
Heading level (line 1)
markdownlint expects the first line to be a top‑level heading. Change to:# Compressible macros — caller program usage (first draft)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 with1.instead of2.,3., etc. (the rendered numbering will still be sequential).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_sponsorPDA). 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-checkremaining_accountsin generatedUnpackto avoid panicsFor
Pubkeyfields, the generatedUnpackimpl currently does:#field_name: *remaining_accounts[self.#field_name as usize].keyIf
self.#field_namepoints pastremaining_accounts.len(), this will panic instead of returning aProgramError, 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
ProgramErrorinstead of aborting the program.sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs (1)
405-408: Prefer removing unused lifetime'bover silencing Clippy
compress_and_close_ctoken_accounts_signed<'b, 'info>(...)doesn’t currently use'bin its parameters or body, so#[allow(clippy::extra_unused_lifetimes)]is just papering over an unused generic.Two options:
- If
'bisn’t needed, simplify the signature to<'info>and drop theallow.- If a second lifetime is intentionally reserved for future use (e.g., to tie
signer_seedslifetimes), keep it but add a brief comment explaining why theallowis 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 ofdecompress_runtimehelpersYou now expose
process_decompress_tokens_runtimeandCTokenSeedProviderboth via:
compressed_token_sdk::compressible::process_decompress_tokens_runtime, andcompressed_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: MakeDefaultsemantics ofCompressedAccountVariantexplicit or configurable
Defaultimplicitly 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
CompressedAccountVariantthat clearly states: “Defaultuses 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 aroundunreachable!()variants andPackedCTokenData::unpackAcross the generated impls (
DataHasher,HasCompressionInfo,Size,Pack,Unpack), most packed variants and the CToken variants are handled via bareunreachable!(), exceptPackedCTokenDatainUnpack, which returnsOk(self.clone()). That leaves a few rough edges:
- Calling these traits on the “wrong” variant panics at runtime with no context.
PackedCTokenData::unpackreturning a clonedPackedCTokenDatavariant 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
Unpackbehavior forPackedCTokenDataconsistent with the rest (eitherunreachable!()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 derivingDefaultforRentSponsorIf 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 indexingsolana_accounts.
collect_pda_and_tokenstill indexessolana_accounts[i]without verifyingi < solana_accounts.len(). A mismatched slice (macro misuse, future refactor, adversarial input) will panic on-chain instead of returning a clearProgramError. Please guard the index and returnProgramError::InvalidAccountDatawhen the slices diverge. citeturn0files2sdk-libs/macros/src/compressible/utils.rs (1)
41-75: Treat arrays as Copy only when their element type is Copy.
is_copy_typestill returnstruefor everyType::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 intoarray.elemand only return true when the element type is Copy (matching Rust’s rules). citeturn0files4sdk-libs/sdk/src/compressible/config.rs (1)
338-353:address_spaceupdates are still effectively locked and duplicate-check compares referencesTwo related issues around
address_space:
- Updates are effectively disabled
With
MAX_ADDRESS_TREES_PER_SPACE == 1and: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_spaceis one that is exactly equal to the existingconfig.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_spaceis immutable in v1.If you do want to allow future extensions, consider relaxing the invariants once
MAX_ADDRESS_TREES_PER_SPACEis increased.
- Duplicate-check compares pointer identity, not Pubkey values
validate_address_space_no_duplicatescurrently 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’slen() == 1this 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: Treatcompression_infoas a reserved field inCompressAsandSizegeneratorsRight now
compression_infois handled twice:
generate_compress_as_implalways setscompression_info: None, andgenerate_compress_as_field_assignmentswill also emit an assignment for thecompression_infofield (clone/copy), producing a duplicate field in the struct literal and a compile error for any struct that actually hascompression_info.Similarly,
generate_size_impladds a fixedcompression_info_size, butgenerate_size_fieldswill also includecompression_infoin thetry_to_vec().len()sum, double‑counting its bytes.You should exclude
compression_infofrom 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_fieldfromderive_compress_asso 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 ofProgramError::Custom(0)for config mismatchesThese 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 throughLightSdkError.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 -A5sdk-libs/macros/src/compressible/seed_providers.rs (1)
57-161: Optionalctoken_*accounts are handled correctly now; consider deduplicating helper logicThe seed-provider generation now:
- Treats
fee_payer/rent_sponsor/config/compression_authorityas 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 optionalctoken_*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 likefn is_standard_account_field(&str) -> boolwould reduce drift.- The local
is_pubkey_typeat the bottom duplicatescompressible::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::parsestill 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/dataas 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 toparse_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: FixTokenSeedSpeclookup: comparingIdenttoStringdoes not compileIn 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::variantis asyn::Ident, andIdentdoes not implementPartialEq<String>, sos.variant == name_strdoes 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_strpurely for the error message.
985-1070: Use a dedicated overflow error instead ofProgramError::Custom(0)in rent distributionThe rent-based close distribution inside the generated
CompressContext::compress_pda_accountimpl correctly:
- Computes
AccountRentState,- Uses
calculate_close_distribution,- Moves lamports with
checked_sub/checked_add.However, on
checked_addoverflow you currently returnProgramError::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 code0.Consider adding a specific
CompressibleInstructionErrorvariant (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: Avoidunreachable!()forPdaOnly/TokenOnlyinstruction variants or reject them explicitly
InstructionVariantsupportsPdaOnly,TokenOnly, andMixed, andadd_compressible_instructionshappily constructsPdaOnly/TokenOnlywhen 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
CompressAccountsIdempotentdefinition (compress side)generate_error_codesvariant-specific errorsIf 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/TokenOnlysupport and remove theunreachable!()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 configWe load
compression_configbut never assert thatctx.rent_sponsor()matchescompression_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 afterload_checkedand returnLightSdkError::ConstraintViolation(log if you wish) when they differ so the transaction fails fast.sdk-libs/sdk/src/compressible/compression_info.rs (2)
290-320: ReturnNonewhen nothing was claimedPer the docstring, callers expect
Nonewhen no rent was actually claimed. The current fallback returnsOk(Some(0)), forcing them to pattern-match a value that should have beenNoneand making it impossible to distinguish “not claimable” from “claim succeeded with value.” Please returnOk(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_cpihard-codes an all-zero Pubkey as the System Program, so every invocation fails withIncorrectProgramId. Leverage the account we already receive: assert it equalssolana_program::system_program::ID(to catch wiring bugs) and set the instruction’sprogram_idto*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: MakeDataHasheractually aliasLightHasherThe 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 callderive_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]onHasCompressionInfoas documentedThe derive example requires
#[skip]on thecompression_infofield, but the macro attribute list doesn’t whitelist it. As soon as users follow the docs, compilation fails with “cannot find attributeskip.” Addattributes(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
⛔ Files ignored due to path filters (49)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by nonescripts/lint.shis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/processor.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/variant.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/errors.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/lib.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/user_record_tests.rsis 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.rssdk-libs/macros/src/compressible/variant_enum.rssdk-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
| /// 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>; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Safety fixes in decompression runtime look solid; consider clarifying get_authority_seeds
The runtime now:
- Validates
owner_index/mint_indexagainstpacked_accounts.len()before indexing. - Only derives a CPI context pubkey when
has_pdasis true. - Uses plain
Vecfortoken_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.
| 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); | ||
|
|
There was a problem hiding this comment.
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.
| #[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)) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.
sdk-libs/macros/src/cpi_signer.rs
Outdated
| 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() | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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() | ||
| } |
There was a problem hiding this comment.
🧹 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().
| 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), | ||
| ]; |
There was a problem hiding this comment.
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. citeturn0files0
🤖 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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_configallows callers to specify an arbitraryconfig_bumpviaOption<u8>(line 87, defaulting to 0 at line 89), butupdate_compression_configalways hardcodes0when 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_configwill target a different PDA than the one created during initialization, causing updates to fail or hit the wrong account.Fix by either:
- Add a
config_bump: u8parameter toupdate_compression_configand pass it through toderive_pda, ensuring callers use the same bump, or- Remove
Option<u8>frominitialize_compression_configand hardcode0there 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_bumpargument.Also applies to: 129-164
166-284: Replace panicking unwraps with graceful error handling.Two unwrap sites remain that can panic on malformed input:
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 withNone. If the first account is a token and a later one is a PDA with context, this will panic.Lines 230-233:
packed_tree_infos.state_trees.as_ref().unwrap()will panic if state_trees isNone.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 unusedaccounts_to_compressparameter and replace remaining unwrap.The
accounts_to_compressparameter (line 292) is accepted but never meaningfully used—only its length is checked againstaccount_pubkeys.len()at lines 296-298. Beyond that validation, no lamports, owner, or otherAccountdata is read or encoded.Additionally, line 321 unwraps
state_trees, which can panic if it'sNone.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_sponsorlogic is a clear improvement over unwrapping: you prefer the compressible extension’srent_sponsor, and you now fail withInvalidAccountDataif nothing suitable is found.Two follow‑ups to consider:
- Using the first signer in
packed_accountsas the fallback “fee payer” couples rent semantics to account ordering. Since this function already takes an explicitfee_payer: Pubkey, it might be less error‑prone to use that as the fallback rent sponsor (and rely onfind_account_indicesto fail if the fee payer account isn’t actually present inpacked_accounts), or at least document the assumption that “first signer == intended fee payer”.- Right now
rent_sponsor_pubkeyis alwaysNoneat the top of the loop, so theif let Some(sponsor)branch is effectively dead. Either setrent_sponsor_pubkeyearlier 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 emptyaddress_spaceThis 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 expandingCompressibleConfiglayoutAdding
write_top_up,compression_authority, andrent_config(and updatingLEN/size_for_address_space) changes the serialized layout ofCompressibleConfig. 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 neededThe update routine now allows changing
compression_authority,rent_config, andwrite_top_upin place. The wiring looks correct, but it currently applies no extra validation on these fields beyond whatRentConfigitself enforces.If there are invariants you rely on (e.g.,
write_top_upnonzero, or bounded within some range), consider enforcing them here or inCompressibleConfig::validateso 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-boundsremaining_accountsindexing in generatedUnpackThe generated
Unpackfor the packed struct still uses direct indexing:#field_name: *remaining_accounts[self.#field_name as usize].keyIf
self.#field_namepoints pastremaining_accounts.len(), this will panic instead of returning aProgramError. Since the generatedunpacksignature isResult<_, 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
ProgramErrorinstead 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
NotEnoughAccountKeysis the desired error variant here.
7-61: Make derive constraints on generics andCloneexplicit (or enforce them)This derive still:
- Ignores
input.generics, emitting non-genericimpl Pack/Unpack for #struct_nameandPacked#struct_nameeven if the source struct is generic.- Assumes non-
Copyfields implementClone, emittingself.#field_name.clone().- Treats a field literally named
compression_infoas transient metadata (always set toNoneon 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_spannedmessage (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
Packedtype 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_pdasstill ignorescfg.compression_authorityHere:
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_sponsorfrom the on-chain config but hard-codecompression_authorityto the payer. That conflicts with the runtime check inprocess_compress_pda_accounts_idempotent, which comparesctx.compression_authority().keyagainstcompression_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 intry_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 wherecompression_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: Tightenis_copy_typefor arrays and centralize Pubkey detectionTwo small but non-trivial points:
is_copy_typestill treats everyType::Array(_)asCopy:Type::Array(_) => true,In Rust,
[T; N]isCopyonly ifT: 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) + }
- You now have a shared
is_pubkey_typehere, butseed_providers.rsstill defines its ownis_pubkey_typewith slightly different logic. Keeping two implementations invites drift. Making this the canonical helper (and using it fromseed_providers.rs) would ensure consistent behavior across all compressible macros.After changing
Type::Arrayhandling, 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:TokenSeedSpecstill mishandles seeds starting with identifier expressions (ctx.*,data.*, etc.)The parser now special-cases the first identifier inside
(...)and, in the default arm, synthesizes aSeedElement::Expressionfrom 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 justctxordata. 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 arbitrarySeedElement::Expressioncorrectly. 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, ormax_key(ctx.accounts.user, data.bump)are preserved intact and later remapped bygenerate_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 comparesIdenttoStringand is likely to fail to compileHere:
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.variantis asyn::Ident, andname_stris aString.syn::Identdoesn’t implementPartialEq<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
namedirectly 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
IdenttoIdent.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 unsupportedInstructionVariants instead of panicking withunreachable!(); update rent sponsor docYou still construct
InstructionVariant::PdaOnlyandTokenOnly: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_structandgenerate_error_codestreat those variants asunreachable!(). 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_codeshandling only theMixedcase.Separately, in the generated
DecompressAccountsIdempotentstruct 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_idempotentenforce thatctx.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'blifetime instead of suppressing Clippy.
compress_and_close_ctoken_accounts_signed<'b, 'info>doesn’t use'banywhere in its parameters or body, soclippy::extra_unused_lifetimesis 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
Compressibleon a PDA +add_compressible_instructionsusage + 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_sponsorPDA, 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 correctlyThe updated
process_decompress_tokens_runtimeaddresses the earlier critical concerns:
- Validates
mint_indexandowner_indexagainstpacked_accounts.len()and returnsProgramError::InvalidAccountDatainstead of panicking.- Computes
cpi_context_pubkeyonly whenhas_pdasis true, so a missing CPI context no longer fails the pure‑token path.- Uses plain
Vecfortoken_decompress_indicesand signer seeds, avoiding unnecessary boxing while keeping the API straightforward.The remaining
get_authority_seedsmethod onCTokenSeedProvideris 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 indexingsolana_accounts[i]to avoid panics in generated codeInside the generated
collect_pda_and_token, the loop assumescompressed_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
ProgramErrorinstead of an abort.Also applies to: 132-143
sdk-libs/macros/src/compressible/traits.rs (1)
86-129: Treatcompression_infoas a reserved field in generated impls to avoid duplicate init and size double‑countingFor structs with a
compression_infofield, the current generators have two coupled problems:
Duplicate field initialization in
CompressAs
generate_compress_as_implalways emits:Self { compression_info: None, #(#field_assignments)* }but
generate_compress_as_field_assignmentsiterates all fields except those with#[skip], so it will also emit an assignment forcompression_info. The result is a struct literal withcompression_infospecified twice, which fails to compile.Double‑counting in
Size
generate_size_impladds an explicitcompression_info_sizeconstant:let compression_info_size = 1 + <light_sdk::compressible::CompressionInfo as light_sdk::compressible::Space>::INIT_SPACE; compression_info_size #(#size_fields)*but
generate_size_fieldsalso includescompression_infoin thetry_to_vec().len()sum, so its bytes are counted twice.To fix both issues, reserve
compression_infofor 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_infounder explicit control (both for initialization and for sizing), prevents compile‑time field duplication, and brings theSizebehavior in line with the comments about always allocating space for aSome(CompressionInfo).Also applies to: 150-181, 192-209
sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
46-54: Replace opaqueProgramError::Custom(0)with a structured constraint violation errorConfig 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 toLightSdkError::ConstraintViolationand then into a well‑definedProgramErrorcode (as inconfig::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 branchesBoth the token seed and authority seed handling duplicate the same
is_standard_fieldmatch 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 sharedis_pubkey_typehelper instead of redefining it hereThis file defines its own
is_pubkey_typethat differs fromcompressible::utils::is_pubkey_type. Maintaining two implementations increases the chance of subtle divergence in how “pubkey-like” types are treated when inferring client parameters frominstruction_data.Recommend deleting this local function and importing the canonical helper from
compressible::utilsso 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 againstrent_per_epoch == 0to avoid on-chain panics
calculate_top_up_lamportsdivides byrent_per_epochfromrent_config.rent_curve_per_epoch(num_bytes)(Line 175). If a misconfiguredRentConfigever yields0here, this will panic and abort the transaction instead of returning a clear error.Given
RentConfigis 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: Alignclaim_completed_epoch_rentreturn value with its documentationThe docstring states:
Returns
Some(claimed)if any lamports were claimed;Noneif account is compressible or nothing to claim.The implementation returns:
Ok(None)when the account is compressible (early return), butOk(Some(0))whenclaimableisNoneorSome(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 andNoneis reserved for compressible accounts only.
330-362: Fix System Program ID intransfer_lamports_cpi(currently unusable CPI)
transfer_lamports_cpiconstructs 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 whenevertop_up_renttries to transfer lamports.You already receive
system_program: &AccountInfoas 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.keyequals the known system program ID and return a clear error if not.sdk-libs/macros/src/lib.rs (3)
256-261:DataHasherderive does not actually aliasLightHasheras documentedThe doc says
DataHasheris an alias ofLightHasher, but the implementation still delegates toderive_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
LightHasheris Poseidon‑based andLightHasherShais SHA‑based, this is a real behavioral difference.Either:
- Switch this to
derive_light_hasher(input)so it truly aliasesLightHasher, or- Update the docs to say that
DataHasheraliases the SHA variant, notLightHasher.Right now the API contract and implementation disagree.
263-295: Allow#[skip]onHasCompressionInfoas the docs and examples showThe
HasCompressionInfoderive 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 attributeskipin 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 TODOThe commented-out
LightDiscriminatorShaandadd_native_compressible_instructionsblocks 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_instructionscomment 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_idempotentloadsCompressibleConfigand uses itsaddress_space, but never verifies that:
ctx.rent_sponsor().keyequalscompression_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 sideThis 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 thisbranch for non-cpi-contextbuildsThe non-
cpi-contextPDA decompression path still has a bare// TODO: fix thisabove theLightSystemProgramCpi::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
⛔ Files ignored due to path filters (49)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by nonescripts/lint.shis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/processor.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/src/variant.rsis excluded by none and included by nonesdk-tests/csdk-anchor-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/errors.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/src/lib.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/sdk-compressible-test/tests/user_record_tests.rsis 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.rssdk-libs/macros/src/compressible/pack_unpack.rssdk-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...
| 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() }); | ||
| } |
There was a problem hiding this comment.
🧩 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 treatctx.accounts.<account>as the canonical way to reference account fields in seeds, or - Make the mapper conditional: if
field_nameis one of the known optional fields, use.as_ref().ok_or_else(...), otherwise call.key()directly onaccounts.#field_name(or onaccounts.#field_name.to_account_info()forSigner).
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 2Length 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 -20Length 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.rsLength 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 -10Length 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 -20Length 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 -20Length 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 -15Length 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 -5Length 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.rsLength 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 -60Length 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_accountseed: 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.
| 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; | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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!(), | ||
| } |
There was a problem hiding this comment.
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.
| /// 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)) | ||
| } |
There was a problem hiding this comment.
🧹 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.
sdk-libs/program-test/Cargo.toml
Outdated
| [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"] |
There was a problem hiding this comment.
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:
- Line 23:
light-compressed-token-sdkis still unconditional and should be made optional to prevent publishing cycles - Line 33:
light-compressible-clientis still unconditional and should be made optional (it's only used underdevenvfeature gating) - Line 10: The
devenvfeature is missingdep:light-compressed-token-sdkanddep: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.
| 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)? | ||
| }; |
There was a problem hiding this comment.
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
Noneas an error for those types, or - Document that
S: Defaultis only for variants that don’t use any instruction-data seeds and avoid constructingS::default()whenSeedParamsis 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.
There was a problem hiding this comment.
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-existentAsByteVectrait.The documentation references
AsByteVectrait (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 thecompression_infofield, but the derive macro doesn't whitelist this attribute. Users following the documented pattern will encounter a compiler error: "cannot find attributeskip."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
📒 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 centralizedinto_token_streamhelper standardizes error handling across all macro implementations.
30-46: Documentation example correctly usesignorefence.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
ignoreto prevent doctest compilation failures, and the implementation applies the standardinto_token_streamwrapper.
238-253: Documentation and implementation are correct.The example appropriately uses
ignorefence, and the implementation follows the standardinto_token_streampattern.
330-334: CompressAs derive is correctly implemented.The
compress_asattribute is properly whitelisted, and the implementation delegates to the appropriate internal module function.
443-447: Compressible derive is properly implemented.Both
compress_asandlight_seedsattributes are correctly whitelisted, and the implementation follows the established pattern.
560-566: DecompressContext derive is correctly implemented.Both
pda_typesandtoken_variantattributes 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
ItemModand applies theinto_token_streamwrapper for consistent error handling.
580-600: Let me check the actual patterns used inlib.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 aTokenStreamcontainingcompile_error!if parsing fails. Both rent sponsor functions already useparse_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_streamis not applicable here. The rent sponsor functions already return the correct type (TokenStream), and theparse_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:
The
add_compressible_instructionsmacro exists only insdk-libs/macros/src/lib.rs(the public proc_macro wrapper at lines 365-371), not inprogram-libs/macros.The implementation is properly modularized: The proc_macro delegates to
compressible::instructions::add_compressible_instructions(), which is defined atsdk-libs/macros/src/compressible/instructions.rs:299. This is correct architecture—a public API wrapper with internal implementation details.No code was moved to
program-libs/macros. That directory contains onlycpi_signer.rs,pubkey.rs, andlib.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.
| #[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)) | ||
| } |
There was a problem hiding this comment.
🧩 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/ -C3Length 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/ -C2Length 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:
- Remove
skipfrom the attribute whitelist forLightHasherSha(lib.rs line 249), or - 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.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (4)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1)
108-129: AlignRent::geterror mapping and consider asserting program ID consistencyTwo small but important refinements here:
Rent::get()failures are mapped toLightSdkError::Borsh, which is meant for (de)serialization issues, not sysvar/program errors. It’s more accurate to surface them asProgramError:- 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 aFrom<ProgramError>impl).
- The new account is created with
&cpi_accounts.self_program_id()as owner, whileLightAccount::<T>::new_closeuses the separateprogram_idparameter. 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 explicitLightSdkErrorinstead of opaqueProgramError::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 usesLightSdkError::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_checkedand other constraint checks in the SDK.sdk-libs/macros/src/lib.rs (2)
227-232:DataHasherderive still aliases the SHA variant, notLightHasheras documentedDoc says this is an alias of
LightHasher, but the implementation delegates toderive_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:HasCompressionInfodocs use#[skip]but the derive doesn’t accept itThe example shows:
#[derive(HasCompressionInfo)] pub struct UserRecord { #[skip] pub compression_info: Option<CompressionInfo>, // ... }Without whitelisting
skipin the derive, this pattern will fail with “cannot find attributeskip” when used without another derive that acceptsskip. 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 fromLightHasherand that you must also deriveLightHasheron 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
⛔ Files ignored due to path filters (4)
sdk-tests/csdk-anchor-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-full-derived-test/Cargo.tomlis excluded by none and included by nonesdk-tests/sdk-compressible-test/Cargo.tomlis excluded by none and included by nonextask/Cargo.tomlis 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-sdkusage is unverified.I've examined the codebase and found critical issues with two of the three claims:
Confirmed Issue:
light-compressible-clientis genuinely unconditionally imported incompressible_setup.rs(line 10:use light_compressible_client::compressible_instruction;). Since this module is only used during testing with thedevenvfeature, making this dependency optional and addingdep:light-compressible-clientto the devenv feature is correct.Unconfirmed Issue:
- I cannot find any usage of
light-compressed-token-sdkin the codebase. Extensive searches reveal zero imports of this crate. The code useslight_compressed_token(a different crate, already optional) andlight_ctoken_types(already optional), but notlight-compressed-token-sdk. The review claims it's "only used behind feature gates," but I cannot locate these usages.Before proceeding, this needs clarification:
- Where is
light-compressed-token-sdkactually used? Is it a dead dependency, or used indirectly through another import?- Should it be removed entirely if unused, or is it required for future development?
The
light-compressible-clientfix is solid and should be applied. Thelight-compressed-token-sdkportion requires manual verification.sdk-libs/sdk/src/compressible/compress_runtime.rs (1)
56-87: Bounds checks on system and PDA account slicing look solidThe new guard on
system_accounts_offset_usizeand the use ofchecked_subplus.get(..)around the PDA slice eliminate the previous panic risk from malformedremaining_accounts/compressed_accountsinputs. This is the right pattern for handling attacker-controlled account arrays.sdk-libs/macros/src/lib.rs (1)
60-76: Good use ofinto_token_streamwrapper for attribute macrosRefactoring
light_system_accounts/light_accountsto parse viasynand route throughinto_token_streammeans 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 generatedDecompressContextimpl is now lifetime‑soundThe generated PDA match arms now forward the
seed_params: Option<&SeedParams>directly intohandle_packed_pda_variant, letting that helper constructS::default()when needed. This removes the previous lifetime issue around referencing a temporarySeedParams::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.
| 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; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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), | ||
| )); | ||
| } |
There was a problem hiding this comment.
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:
- Detecting primitive numeric types and generating
.to_le_bytes().as_ref()automatically, or - 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.
| 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 }; |
There was a problem hiding this comment.
🧹 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.
| 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(()); | ||
| } |
There was a problem hiding this comment.
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.
| // 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())?; | ||
| } |
There was a problem hiding this comment.
🧹 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.
stacked on top of #2020, merge 2020 first
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 tests for the macros at every level:
Summary by CodeRabbit
New Features
Documentation
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.