refactor: relax ata decompress signer check, feat: add ata decompress idempotent#2360
refactor: relax ata decompress signer check, feat: add ata decompress idempotent#2360ananas-block wants to merge 2 commits intomainfrom
Conversation
📝 WalkthroughWalkthroughAdds a new Transfer2 compression mode Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Transfer2Processor
participant TokenInput
participant BatchedMerkleTree
participant SystemProgram
Caller->>Transfer2Processor: submit Transfer2 (mode=DecompressIdempotent)
Transfer2Processor->>TokenInput: validate inputs (must be single, CompressedOnly is_ata)
alt validation fails
TokenInput-->>Transfer2Processor: return TokenError (single-input / ATA)
Transfer2Processor-->>Caller: return Err
else validation passes
Transfer2Processor->>BatchedMerkleTree: derive account_hash for input
Transfer2Processor->>BatchedMerkleTree: check_input_queue_non_inclusion(account_hash)
alt already present (spent)
BatchedMerkleTree-->>Transfer2Processor: present
Transfer2Processor-->>Caller: Ok() // idempotent no-op
else not present
Transfer2Processor->>SystemProgram: perform decompression CPI / apply state changes
SystemProgram-->>Transfer2Processor: ok
Transfer2Processor-->>Caller: Ok()
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (1)
97-129:⚠️ Potential issue | 🟠 Major
DecompressIdempotentdoes not validate that the ATA is pre-initialized as required by the specification.The TRANSFER2.md documentation states "ATA must be pre-created" for
DecompressIdempotent, but the implementation passes the same validation path to bothDecompressandDecompressIdempotentwithout mode awareness. Thevalidate_and_apply_compressed_only()helper does not receive the compression mode, so it cannot enforce mode-specific initialization requirements. This meansDecompressIdempotenthas no way to verify that the destination ATA already exists and is initialized, as the spec requires.Pass the mode to the helper and add a mode-aware check:
Decompress: destination may be fresh or pre-existing (current behavior)DecompressIdempotent: destination must be pre-initialized (currently unenforced)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs` around lines 97 - 129, The DecompressIdempotent branch doesn't enforce the TRANSFER2.md requirement that the destination ATA be pre-initialized; update the Decompress/DecompressIdempotent handling to pass the current ZCompressionMode into validate_and_apply_compressed_only (or add an extra parameter) and implement a mode-aware check inside validate_and_apply_compressed_only that asserts the destination ATA is already created/initialized when mode == ZCompressionMode::DecompressIdempotent but allows fresh destinations for ZCompressionMode::Decompress; reference validate_and_apply_compressed_only and ZCompressionMode::DecompressIdempotent when adding the validation and adjust callers accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@programs/compressed-token/program/CLAUDE.md`:
- Around line 71-73: The doc statement about permissionless ATA decompress is
too broad; update the line referencing Decompress and DecompressIdempotent to
specify that the permissionless path applies only to CToken-associated token
accounts (CToken-ATA) rather than all SPL token accounts — mention the specific
symbols Decompress, DecompressIdempotent (mode 3), and the is_ata=true flag and
align wording with the implementation in transfer2/compression/spl.rs which
rejects DecompressIdempotent for regular SPL token accounts.
In
`@programs/compressed-token/program/src/compressed_token/transfer2/compression/spl.rs`:
- Around line 81-84: The match arm for ZCompressionMode::DecompressIdempotent
currently returns ProgramError::InvalidInstructionData which hides the real
cause; change it to return a named token error (e.g.,
Err(TokenError::InvalidCompressionMode.into()) or a new TokenError variant
dedicated to unsupported DecompressIdempotent) so callers can distinguish
malformed payloads from unsupported compression modes; update imports/usages in
transfer2::compression::spl.rs to bring TokenError into scope and add the new
TokenError variant if it doesn't exist, ensuring the error maps to ProgramError
via .into().
In `@sdk-libs/compressed-token-sdk/src/compressed_token/v2/account2.rs`:
- Around line 248-271: is_decompress() currently only matches
CompressionMode::Decompress and thus returns false for idempotent flows; update
the is_decompress() implementation to also treat
CompressionMode::DecompressIdempotent as a decompress case so that calls like
decompress_idempotent (which sets Compression::decompress_idempotent) are
recognized as decompress operations; locate is_decompress() and add
DecompressIdempotent to the match/conditional alongside Decompress to return
true.
---
Outside diff comments:
In
`@programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs`:
- Around line 97-129: The DecompressIdempotent branch doesn't enforce the
TRANSFER2.md requirement that the destination ATA be pre-initialized; update the
Decompress/DecompressIdempotent handling to pass the current ZCompressionMode
into validate_and_apply_compressed_only (or add an extra parameter) and
implement a mode-aware check inside validate_and_apply_compressed_only that
asserts the destination ATA is already created/initialized when mode ==
ZCompressionMode::DecompressIdempotent but allows fresh destinations for
ZCompressionMode::Decompress; reference validate_and_apply_compressed_only and
ZCompressionMode::DecompressIdempotent when adding the validation and adjust
callers accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 44c31050-3598-4586-ac6f-b42f507cb049
⛔ Files ignored due to path filters (5)
js/compressed-token/src/v3/layout/layout-transfer2.tsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/compress_only/ata_decompress.rsis excluded by none and included by noneprogram-tests/utils/src/actions/legacy/instructions/transfer2.rsis excluded by none and included by noneprogram-tests/utils/src/actions/legacy/transfer2/decompress.rsis excluded by none and included by noneprogram-tests/utils/src/assert_transfer2.rsis excluded by none and included by none
📒 Files selected for processing (11)
program-libs/token-interface/src/error.rsprogram-libs/token-interface/src/instructions/transfer2/compression.rsprograms/compressed-token/program/CLAUDE.mdprograms/compressed-token/program/docs/compressed_token/TRANSFER2.mdprograms/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rsprograms/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rsprograms/compressed-token/program/src/compressed_token/transfer2/compression/mod.rsprograms/compressed-token/program/src/compressed_token/transfer2/compression/spl.rsprograms/compressed-token/program/src/compressed_token/transfer2/processor.rsprograms/compressed-token/program/src/shared/token_input.rssdk-libs/compressed-token-sdk/src/compressed_token/v2/account2.rs
programs/compressed-token/program/src/compressed_token/transfer2/compression/spl.rs
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@programs/compressed-token/program/src/compressed_token/transfer2/processor.rs`:
- Around line 316-318: Before deserializing the tree with
BatchedMerkleTreeAccount::state_from_account_info, add an explicit validation
that the provided tree_account refers to a V2 tree by checking the merkle tree
metadata type against STATE_MERKLE_TREE_TYPE_V2; if the metadata type is not V2,
return an error. Concretely, read the merkle-tree metadata/header from
tree_account (using the merkle tree metadata helper or by checking the metadata
type field exposed by the merkle_tree module), compare it to
merkle_tree::STATE_MERKLE_TREE_TYPE_V2, and only then call
BatchedMerkleTreeAccount::state_from_account_info; this mirrors the defensive
pattern around DecompressIdempotent/is_ata and makes the version requirement
explicit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d04d31c7-2503-4d70-a43a-6a7b5b577369
⛔ Files ignored due to path filters (4)
Cargo.lockis excluded by!**/*.lockand included by noneprogram-tests/compressed-token-test/tests/compress_only/ata_decompress.rsis excluded by none and included by noneprogram-tests/utils/src/actions/legacy/instructions/transfer2.rsis excluded by none and included by noneprogram-tests/utils/src/actions/legacy/transfer2/decompress.rsis excluded by none and included by none
📒 Files selected for processing (4)
program-libs/token-interface/src/instructions/transfer2/compression.rsprograms/compressed-token/program/Cargo.tomlprograms/compressed-token/program/src/compressed_token/transfer2/config.rsprograms/compressed-token/program/src/compressed_token/transfer2/processor.rs
| let mut tree = | ||
| light_batched_merkle_tree::merkle_tree::BatchedMerkleTreeAccount::state_from_account_info(tree_account) | ||
| .map_err(ProgramError::from)?; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for how CompressedOnly extension and is_ata relate to tree version requirements
rg -nC5 "CompressedOnly" --type rustRepository: Lightprotocol/light-protocol
Length of output: 50385
🏁 Script executed:
# First, find and examine the state_from_account_info implementation
rg -n "state_from_account_info" --type rust -A 10 | head -100Repository: Lightprotocol/light-protocol
Length of output: 9454
🏁 Script executed:
# Next, examine the validation at lines 121-141 in the transfer2 processor
head -n 145 programs/compressed-token/program/src/compressed_token/transfer2/processor.rs | tail -n 30Repository: Lightprotocol/light-protocol
Length of output: 1562
🏁 Script executed:
# Search for tree type validation patterns
rg -n "STATE_MERKLE_TREE_TYPE|AddressV2|StateV2" --type rust -B 2 -A 2Repository: Lightprotocol/light-protocol
Length of output: 50386
🏁 Script executed:
# Read the processor file around the validation block and the deserialization call
sed -n '100,150p' programs/compressed-token/program/src/compressed_token/transfer2/processor.rsRepository: Lightprotocol/light-protocol
Length of output: 2451
🏁 Script executed:
# Read around the state_from_account_info call
sed -n '310,330p' programs/compressed-token/program/src/compressed_token/transfer2/processor.rsRepository: Lightprotocol/light-protocol
Length of output: 758
🏁 Script executed:
# Search for tree type validation in transfer2 processor specifically
rg -n "tree_type|TreeType" programs/compressed-token/program/src/compressed_token/transfer2/processor.rsRepository: Lightprotocol/light-protocol
Length of output: 54
🏁 Script executed:
# Look at state_from_account_info implementation to see what validation it does
rg -n "fn state_from_account_info" --type rust -A 20Repository: Lightprotocol/light-protocol
Length of output: 4459
🏁 Script executed:
# Check if BatchedMerkleTreeAccount discriminator validation happens internally
rg -n "BatchMta|BatchedMerkleTreeAccount" program-libs/batched-merkle-tree/src/merkle_tree.rs -A 5 | head -80Repository: Lightprotocol/light-protocol
Length of output: 3436
🏁 Script executed:
# Look for account discriminator constants related to trees
rg -n "BatchMta|discriminator" program-libs/batched-merkle-tree/src/ --type rust | head -30Repository: Lightprotocol/light-protocol
Length of output: 3475
Add explicit tree type validation before tree account deserialization.
The code assumes state_from_account_info validates that the tree is StateV2, but this validation is implicit (hardcoded via generic parameter). The validation at lines 121-141 checks the DecompressIdempotent mode and is_ata=true flag without explicitly verifying the tree version. While state_from_account_info will reject non-V2 trees during deserialization, there's no explicit guard at the instruction level. Per the codebase pattern (seen in programs/account-compression/src/context.rs and the learnings), tree type should be explicitly validated before account operations.
Add a check against STATE_MERKLE_TREE_TYPE_V2 from the merkle tree metadata before proceeding—this makes the requirement clear and matches the defensive validation pattern used elsewhere in the codebase.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@programs/compressed-token/program/src/compressed_token/transfer2/processor.rs`
around lines 316 - 318, Before deserializing the tree with
BatchedMerkleTreeAccount::state_from_account_info, add an explicit validation
that the provided tree_account refers to a V2 tree by checking the merkle tree
metadata type against STATE_MERKLE_TREE_TYPE_V2; if the metadata type is not V2,
return an error. Concretely, read the merkle-tree metadata/header from
tree_account (using the merkle tree metadata helper or by checking the metadata
type field exposed by the merkle_tree module), compare it to
merkle_tree::STATE_MERKLE_TREE_TYPE_V2, and only then call
BatchedMerkleTreeAccount::state_from_account_info; this mirrors the defensive
pattern around DecompressIdempotent/is_ata and makes the version requirement
explicit.
Summary by CodeRabbit
New Features
Behavior Changes
Bug Fixes / Errors
Documentation