Skip to content

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

Open
ananas-block wants to merge 2 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 2 commits intomainfrom
jorrit/chore-relax-ata-decompress

Conversation

@ananas-block
Copy link
Contributor

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

Summary by CodeRabbit

  • New Features

    • Added DecompressIdempotent mode: permissionless ATA-only decompression limited to a single input; SDK updated to support it.
  • Behavior Changes

    • Decompress and DecompressIdempotent now follow the same validation and balance update paths; owner/delegate signer checks are skipped for ATA decompression.
  • Bug Fixes / Errors

    • New explicit validation with clearer error responses when idempotent decompress requirements (single input, ATA) are not met.
  • Documentation

    • Transfer2 docs updated to describe mode values, constraints, and semantics.

@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
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds a new Transfer2 compression mode DecompressIdempotent (discriminant 3) and implements idempotent ATA decompression: new error codes, mode decoding/constructors, validation changes, an idempotency no-op check in processing, SDK support, and updated docs.

Changes

Cohort / File(s) Summary
Error Types
program-libs/token-interface/src/error.rs
Added TokenError::IdempotentDecompressRequiresSingleInput and ::IdempotentDecompressRequiresAta with codes 18067 and 18068.
Compression Mode Interface
program-libs/token-interface/src/instructions/transfer2/compression.rs
Added CompressionMode::DecompressIdempotent, DECOMPRESS_IDEMPOTENT = 3u8, updated is_decompress()/decoding and added decompress_idempotent() constructor.
Documentation
programs/compressed-token/program/CLAUDE.md, programs/compressed-token/program/docs/compressed_token/TRANSFER2.md
Documented new DecompressIdempotent (3) mode, explicit discriminants, and mode-specific ATA / single-input constraints.
Compression Control Flow
programs/compressed-token/program/src/.../compress_or_decompress_ctokens.rs, .../ctoken/inputs.rs, .../compression/mod.rs, .../compression/spl.rs
Treated Decompress and DecompressIdempotent equivalently for ctoken decompression branches and validation; added explicit SPL rejection for idempotent mode.
Main Validation & Authorization
programs/compressed-token/program/src/compressed_token/transfer2/processor.rs, programs/compressed-token/program/src/shared/token_input.rs
Validation now enforces single input and ATA extension for idempotent mode with new TokenError variants; added idempotency early-exit check via batched merkle tree bloom filter; resolve_ata_signer now returns (signer_account, is_ata_decompress) and owner/delegate signer verification is skipped for ATA decompress.
SDK Support
sdk-libs/compressed-token-sdk/src/compressed_token/v2/account2.rs
Added CTokenAccount2::decompress_idempotent(amount, source_index) mirroring existing decompress behavior.
Build / Dependency
programs/compressed-token/program/Cargo.toml
Added workspace dependency light-batched-merkle-tree.
Config Wiring
programs/compressed-token/program/src/compressed_token/transfer2/config.rs
Added is_decompress_idempotent boolean to Transfer2Config, derived from instruction compression entries.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

✨ A new mode walks in, idempotent and neat,
One input, one ATA — no duplicate beat.
Bloom-checks nod, signers take a rest,
Decompress once or twice, the result is the best. 🎉

🚥 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 accurately reflects the main changes: refactoring ATA decompress signer checks and adding a new idempotent decompress mode.
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
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
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
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.

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