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 3 commits intomainfrom
Conversation
📝 WalkthroughWalkthroughThis pull request introduces an ATA-decompress idempotency feature for the compressed-token program. When a Changes
Sequence DiagramsequenceDiagram
participant Client as Client/Instruction
participant Processor as Transfer2 Processor
participant InputRes as Token Input Resolver
participant MerkleTree as BatchedMerkleTreeAccount
Client->>Processor: Transfer2 (decompress mode, CompressedOnly is_ata=true)
Processor->>Processor: Detect ATA-decompress path
Processor->>Processor: Validate single input & single compression
Processor->>Processor: Compute deterministic account hash
Processor->>MerkleTree: Load BatchedMerkleTreeAccount
Processor->>MerkleTree: check_input_queue_non_inclusion(account_hash)
alt Account Already Spent (Idempotent)
MerkleTree-->>Processor: Account found in bloom filter
Processor-->>Client: Return Ok(()) - No-op
else Account Not Spent
MerkleTree-->>Processor: Account not in queue
Processor->>InputRes: Set input with is_ata_decompress=true
InputRes->>InputRes: Skip verify_owner_or_delegate_signer
InputRes-->>Processor: Signer resolved (permissionless)
Processor->>Processor: Continue normal decompress processing
Processor-->>Client: Return Ok(()) - Decompressed
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
Outdated
Show resolved
Hide resolved
sdk-libs/compressed-token-sdk/src/compressed_token/v2/account2.rs
Outdated
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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
programs/compressed-token/program/CLAUDE.md (1)
72-72: 🧹 Nitpick | 🔵 TrivialThe permissionless ATA decompress documentation may need scope clarification.
This accurately documents the new feature, but per the earlier review discussion, consider clarifying that this permissionless path applies specifically to CToken-associated token accounts (where
is_ata=truein theCompressedOnlyextension), not to standard SPL token accounts. The implementation intransfer2/compression/spl.rshandles SPL token accounts differently.The referenced
docs/compressed_token/TRANSFER2.mdprovides the detailed breakdown, so you could tighten this line to:- - ATA decompress (is_ata=true) is permissionless and idempotent (bloom filter check) + - CToken ATA decompress (is_ata=true) is permissionless and idempotent (bloom filter check)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@programs/compressed-token/program/CLAUDE.md` at line 72, The sentence about "ATA decompress (is_ata=true) is permissionless and idempotent (bloom filter check)" is ambiguous—update the doc to state that the permissionless ATA decompress path applies only to CToken-associated token accounts that have the CompressedOnly extension with is_ata=true (i.e., CToken-associated ATAs), and not to regular SPL token accounts; reference the implementation in transfer2/compression/spl.rs which treats SPL accounts differently and ensure the wording explicitly distinguishes "CToken-associated token accounts (CompressedOnly.is_ata=true)" from "standard SPL token accounts."
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@programs/compressed-token/program/CLAUDE.md`:
- Line 72: The sentence about "ATA decompress (is_ata=true) is permissionless
and idempotent (bloom filter check)" is ambiguous—update the doc to state that
the permissionless ATA decompress path applies only to CToken-associated token
accounts that have the CompressedOnly extension with is_ata=true (i.e.,
CToken-associated ATAs), and not to regular SPL token accounts; reference the
implementation in transfer2/compression/spl.rs which treats SPL accounts
differently and ensure the wording explicitly distinguishes "CToken-associated
token accounts (CompressedOnly.is_ata=true)" from "standard SPL token accounts."
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 43685556-5eb4-446d-829c-c9268d5b531a
⛔ Files ignored due to path filters (2)
program-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 none
📒 Files selected for processing (3)
programs/compressed-token/program/CLAUDE.mdprograms/compressed-token/program/docs/compressed_token/TRANSFER2.mdprograms/compressed-token/program/src/compressed_token/transfer2/processor.rs
Summary by CodeRabbit
New Features
Documentation