Skip to content

refactor: relax ata decompress signer check, feat: add ata decompress idempotent#2360

Open
ananas-block wants to merge 3 commits intomainfrom
jorrit/chore-relax-ata-decompress
Open

refactor: relax ata decompress signer check, feat: add ata decompress idempotent#2360
ananas-block wants to merge 3 commits intomainfrom
jorrit/chore-relax-ata-decompress

Conversation

@ananas-block
Copy link
Copy Markdown
Contributor

@ananas-block ananas-block commented Mar 25, 2026

Summary by CodeRabbit

  • New Features

    • Added permissionless ATA decompress mode with idempotent validation support.
  • Documentation

    • Updated compression mode specifications with numeric discriminants and detailed ATA decompress behavior documentation.

@ananas-block ananas-block changed the title refactor: relax ata decompress signer check, feat: add ata decompress… refactor: relax ata decompress signer check, feat: add ata decompress idempotent Mar 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

This pull request introduces an ATA-decompress idempotency feature for the compressed-token program. When a Transfer2 instruction specifies decompress mode with CompressedOnly extension set to is_ata=true, the processor performs an early idempotency check using bloom filter validation. If the compressed account is already spent, the operation completes as a no-op. For non-idempotent cases, signer verification is bypassed, and deterministic account hash validation occurs via merkle tree lookups.

Changes

Cohort / File(s) Summary
Documentation Updates
programs/compressed-token/program/CLAUDE.md, programs/compressed-token/program/docs/compressed_token/TRANSFER2.md
Added explicit documentation of ATA-decompress mode (is_ata=true) with numeric compression mode discriminants (Compress→0, Decompress→1, CompressAndClose→2), bloom filter idempotency checks, single-input/compression constraints, and permissionless behavior.
Processor ATA-Decompress Logic
programs/compressed-token/program/src/compressed_token/transfer2/processor.rs
Added early ATA-decompress idempotency path in process_with_system_program_cpi that detects decompress mode + CompressedOnly with is_ata=true, validates single-input/single-compression constraint, computes deterministic compressed account hash, queries BatchedMerkleTreeAccount, and returns early if account already spent.
Token Input Signer Resolution
programs/compressed-token/program/src/shared/token_input.rs
Modified resolve_ata_signer to return tuple (signer_account, is_ata_decompress) flag. Updated set_input_compressed_account to conditionally bypass verify_owner_or_delegate_signer when is_ata_decompress=true, enabling permissionless ATA decompress.
Dependency Addition
programs/compressed-token/program/Cargo.toml
Added workspace dependency light-batched-merkle-tree for merkle tree account operations in ATA-decompress idempotency checks.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

🌳 Bloom filters bloom with idempotent grace,
ATA decompresses without signer trace,
Once spent, forever marked—no double-spend,
Permissionless paths that cleanly descend.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the two main changes: relaxing the ATA decompress signer check and adding ATA decompress idempotency, which are directly reflected in the code and documentation changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jorrit/chore-relax-ata-decompress

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

DecompressIdempotent does 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 both Decompress and DecompressIdempotent without mode awareness. The validate_and_apply_compressed_only() helper does not receive the compression mode, so it cannot enforce mode-specific initialization requirements. This means DecompressIdempotent has 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2809be4 and f521153.

⛔ Files ignored due to path filters (5)
  • js/compressed-token/src/v3/layout/layout-transfer2.ts is excluded by none and included by none
  • program-tests/compressed-token-test/tests/compress_only/ata_decompress.rs is excluded by none and included by none
  • program-tests/utils/src/actions/legacy/instructions/transfer2.rs is excluded by none and included by none
  • program-tests/utils/src/actions/legacy/transfer2/decompress.rs is excluded by none and included by none
  • program-tests/utils/src/assert_transfer2.rs is excluded by none and included by none
📒 Files selected for processing (11)
  • program-libs/token-interface/src/error.rs
  • program-libs/token-interface/src/instructions/transfer2/compression.rs
  • programs/compressed-token/program/CLAUDE.md
  • programs/compressed-token/program/docs/compressed_token/TRANSFER2.md
  • programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs
  • programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs
  • programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs
  • programs/compressed-token/program/src/compressed_token/transfer2/compression/spl.rs
  • programs/compressed-token/program/src/compressed_token/transfer2/processor.rs
  • programs/compressed-token/program/src/shared/token_input.rs
  • sdk-libs/compressed-token-sdk/src/compressed_token/v2/account2.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between f521153 and 3655ac3.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock and included by none
  • program-tests/compressed-token-test/tests/compress_only/ata_decompress.rs is excluded by none and included by none
  • program-tests/utils/src/actions/legacy/instructions/transfer2.rs is excluded by none and included by none
  • program-tests/utils/src/actions/legacy/transfer2/decompress.rs is excluded by none and included by none
📒 Files selected for processing (4)
  • program-libs/token-interface/src/instructions/transfer2/compression.rs
  • programs/compressed-token/program/Cargo.toml
  • programs/compressed-token/program/src/compressed_token/transfer2/config.rs
  • programs/compressed-token/program/src/compressed_token/transfer2/processor.rs

Comment on lines +316 to +318
let mut tree =
light_batched_merkle_tree::merkle_tree::BatchedMerkleTreeAccount::state_from_account_info(tree_account)
.map_err(ProgramError::from)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for how CompressedOnly extension and is_ata relate to tree version requirements
rg -nC5 "CompressedOnly" --type rust

Repository: 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 -100

Repository: 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 30

Repository: 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 2

Repository: 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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 20

Repository: 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 -80

Repository: 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 -30

Repository: 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
programs/compressed-token/program/CLAUDE.md (1)

72-72: 🧹 Nitpick | 🔵 Trivial

The 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=true in the CompressedOnly extension), not to standard SPL token accounts. The implementation in transfer2/compression/spl.rs handles SPL token accounts differently.

The referenced docs/compressed_token/TRANSFER2.md provides 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3655ac3 and aa0b202.

⛔ Files ignored due to path filters (2)
  • program-tests/compressed-token-test/tests/compress_only/ata_decompress.rs is excluded by none and included by none
  • program-tests/utils/src/actions/legacy/instructions/transfer2.rs is excluded by none and included by none
📒 Files selected for processing (3)
  • programs/compressed-token/program/CLAUDE.md
  • programs/compressed-token/program/docs/compressed_token/TRANSFER2.md
  • programs/compressed-token/program/src/compressed_token/transfer2/processor.rs

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant