Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions program-libs/compressible/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ pub struct CreateAccountsProof {
pub address_tree_info: PackedAddressTreeInfo,
/// Output state tree index for new compressed accounts.
pub output_state_tree_index: u8,
/// State merkle tree index (needed for mint creation decompress validation).
/// This is optional to maintain backwards compatibility.
pub state_tree_index: Option<u8>,
}
7 changes: 2 additions & 5 deletions programs/system/src/cpi_context/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,14 @@ impl<'a> ZCpiContextAccount2<'a> {
&'a mut self,
instruction_data: &WrappedInstructionData<'b, T>,
) -> Result<(), SystemProgramError> {
let pre_address_len = self.new_addresses.len();
// Cache owner bytes to avoid repeated calls
let owner_bytes = instruction_data.owner().to_bytes();

// Store new addresses
for address in instruction_data.new_addresses() {
let assigned_index = address.assigned_compressed_account_index();
// Use checked arithmetic to prevent overflow
let assigned_account_index = (assigned_index.unwrap_or(0) as u8)
.checked_add(pre_address_len as u8)
.ok_or(ZeroCopyError::Size)?;
// Use the assigned index directly - caller provides absolute index
let assigned_account_index = assigned_index.unwrap_or(0) as u8;
let new_address = CpiContextNewAddressParamsAssignedPacked {
owner: owner_bytes, // Use cached owner bytes
seed: address.seed(),
Expand Down
55 changes: 44 additions & 11 deletions sdk-libs/compressible-client/src/create_accounts_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
//! - Returns a single `address_tree_info` since all accounts use the same tree

use light_client::{
indexer::{AddressWithTree, Indexer, IndexerError},
indexer::{AddressWithTree, Indexer, IndexerError, ValidityProofWithContext},
rpc::{Rpc, RpcError},
};
use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
use light_sdk::instruction::PackedAddressTreeInfo;
use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address;
use solana_instruction::AccountMeta;
use solana_pubkey::Pubkey;
use thiserror::Error;

use crate::pack::{pack_proof, PackError};
use crate::pack::{pack_proof, pack_proof_for_mints, PackError};

/// Error type for create accounts proof operations.
#[derive(Debug, Error)]
Expand Down Expand Up @@ -136,7 +138,28 @@ pub async fn get_create_accounts_proof<R: Rpc + Indexer>(
inputs: Vec<CreateAccountsProofInput>,
) -> Result<CreateAccountsProofResult, CreateAccountsProofError> {
if inputs.is_empty() {
return Err(CreateAccountsProofError::EmptyInputs);
// Token-only instructions: no addresses to derive, but still need tree info
let state_tree_info = rpc
.get_random_state_tree_info()
.map_err(CreateAccountsProofError::Rpc)?;

// Pack system accounts with empty proof
let packed = pack_proof(
program_id,
ValidityProofWithContext::default(),
&state_tree_info,
None, // No CPI context needed for token-only
)?;

return Ok(CreateAccountsProofResult {
create_accounts_proof: CreateAccountsProof {
proof: ValidityProof::default(),
address_tree_info: PackedAddressTreeInfo::default(),
output_state_tree_index: packed.output_tree_index,
state_tree_index: None,
},
remaining_accounts: packed.remaining_accounts,
});
}

// 1. Get address tree (opinionated: always V2)
Expand Down Expand Up @@ -169,7 +192,7 @@ pub async fn get_create_accounts_proof<R: Rpc + Indexer>(
.get_random_state_tree_info()
.map_err(CreateAccountsProofError::Rpc)?;

// 6. Determine CPI context
// 6. Determine CPI context and whether we have mints
// For INIT with mints: need CPI context for cross-program invocation
let has_mints = inputs
.iter()
Expand All @@ -180,13 +203,22 @@ pub async fn get_create_accounts_proof<R: Rpc + Indexer>(
None
};

// 7. Pack proof
let packed = pack_proof(
program_id,
validity_proof.clone(),
&state_tree_info,
cpi_context,
)?;
// 7. Pack proof (use mint-aware packing if mints are present)
let packed = if has_mints {
pack_proof_for_mints(
program_id,
validity_proof.clone(),
&state_tree_info,
cpi_context,
)?
} else {
pack_proof(
program_id,
validity_proof.clone(),
&state_tree_info,
cpi_context,
)?
};

// All addresses use the same tree, so just take the first packed info
let address_tree_info = packed
Expand All @@ -201,6 +233,7 @@ pub async fn get_create_accounts_proof<R: Rpc + Indexer>(
proof: validity_proof.proof,
address_tree_info,
output_state_tree_index: packed.output_tree_index,
state_tree_index: packed.state_tree_index,
},
remaining_accounts: packed.remaining_accounts,
})
Expand Down
7 changes: 7 additions & 0 deletions sdk-libs/compressible-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ pub mod compressible_instruction {
}

/// Returns program account metas for PDA-only decompression (no CToken accounts).
/// Note: Still passes all 7 accounts because the struct has Optional fields that
/// Anchor still deserializes. Uses rent_sponsor as placeholder for ctoken_rent_sponsor.
pub fn accounts_pda_only(
fee_payer: Pubkey,
config: Pubkey,
Expand All @@ -278,6 +280,11 @@ pub mod compressible_instruction {
AccountMeta::new(fee_payer, true),
AccountMeta::new_readonly(config, false),
AccountMeta::new(rent_sponsor, false),
// Optional token accounts - use placeholders that satisfy constraints
AccountMeta::new(rent_sponsor, false), // ctoken_rent_sponsor (mut) - reuse rent_sponsor
AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false),
AccountMeta::new_readonly(COMPRESSIBLE_CONFIG_V1, false),
]
}
}
Expand Down
56 changes: 55 additions & 1 deletion sdk-libs/compressible-client/src/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ pub struct PackedProofResult {
pub packed_tree_infos: PackedTreeInfos,
/// Index of output tree in remaining accounts. Pass to instruction data.
pub output_tree_index: u8,
/// Index of state merkle tree in remaining accounts (when included for mint creation).
pub state_tree_index: Option<u8>,
/// Offset where system accounts start. Pass to instruction data if needed.
pub system_accounts_offset: u8,
}
Expand All @@ -81,6 +83,39 @@ pub fn pack_proof(
proof: ValidityProofWithContext,
output_tree: &TreeInfo,
cpi_context: Option<Pubkey>,
) -> Result<PackedProofResult, PackError> {
pack_proof_internal(program_id, proof, output_tree, cpi_context, false)
}

/// Packs a validity proof with state merkle tree for mint creation.
///
/// Same as `pack_proof` but also includes the state merkle tree in remaining accounts.
/// This is required for mint creation because the decompress operation needs the state
/// merkle tree for discriminator validation.
///
/// # Arguments
/// - `program_id`: Your program's ID
/// - `proof`: Validity proof from `get_validity_proof()`
/// - `output_tree`: Tree info for writing outputs (from `get_random_state_tree_info()`)
/// - `cpi_context`: CPI context pubkey. Required for mint creation.
///
/// # Returns
/// `PackedProofResult` with `state_tree_index` populated.
pub fn pack_proof_for_mints(
program_id: &Pubkey,
proof: ValidityProofWithContext,
output_tree: &TreeInfo,
cpi_context: Option<Pubkey>,
) -> Result<PackedProofResult, PackError> {
pack_proof_internal(program_id, proof, output_tree, cpi_context, true)
}

fn pack_proof_internal(
program_id: &Pubkey,
proof: ValidityProofWithContext,
output_tree: &TreeInfo,
cpi_context: Option<Pubkey>,
include_state_tree: bool,
) -> Result<PackedProofResult, PackError> {
let mut packed = PackedAccounts::default();

Expand All @@ -97,7 +132,25 @@ pub fn pack_proof(
.unwrap_or(output_tree.queue);
let output_tree_index = packed.insert_or_get(output_queue);

let client_packed_tree_infos = proof.pack_tree_infos(&mut packed);
// For mint creation: pack address tree first (must be at index 1 per program validation),
// then state tree. For non-mint: just pack tree infos normally.
let (client_packed_tree_infos, state_tree_index) = if include_state_tree {
// Pack tree infos first to ensure address tree is at index 1
let tree_infos = proof.pack_tree_infos(&mut packed);

// Then add state tree (will be after address tree)
let state_tree = output_tree
.next_tree_info
.as_ref()
.map(|n| n.tree)
.unwrap_or(output_tree.tree);
let state_idx = packed.insert_or_get(state_tree);

(tree_infos, Some(state_idx))
Comment on lines +135 to +149
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify the "index 1" comment—it's about ordering, not a literal index.

The comment says "address tree must be at index 1" but the actual invariant being enforced is that address trees are packed before the state tree. The actual index depends on prior insertions (system accounts, output queue, etc.).

Consider rewording to avoid confusion:

-    // For mint creation: pack address tree first (must be at index 1 per program validation),
-    // then state tree. For non-mint: just pack tree infos normally.
+    // For mint creation: pack address trees before the state tree to ensure deterministic
+    // ordering in remaining accounts. For non-mint: just pack tree infos normally.
🤖 Prompt for AI Agents
In `@sdk-libs/compressible-client/src/pack.rs` around lines 135 - 149, The comment
"must be at index 1" is misleading—update the comment near include_state_tree
and pack_tree_infos to state that the address tree must be packed before the
state tree (i.e., ensure ordering) rather than asserting a literal index, and
mention that the actual index returned by packed.insert_or_get depends on prior
insertions (system accounts, output queue, etc.); keep references to
proof.pack_tree_infos, packed, packed.insert_or_get, output_tree and
state_tree_index so readers know which variables enforce the ordering.

} else {
let tree_infos = proof.pack_tree_infos(&mut packed);
(tree_infos, None)
};
let (remaining_accounts, system_offset, _) = packed.to_account_metas();

// Convert from light_client's types to our local types
Expand All @@ -115,6 +168,7 @@ pub fn pack_proof(
remaining_accounts,
packed_tree_infos,
output_tree_index,
state_tree_index,
system_accounts_offset: system_offset as u8,
})
}
2 changes: 1 addition & 1 deletion sdk-libs/macros/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Detailed macro documentation is in the `docs/` directory:
src/
├── lib.rs # Macro entry points
├── rentfree/ # RentFree macro system
│ ├── account/ # Trait derive macros for account data structs
│ ├── accounts/ # #[derive(RentFree)] for Accounts structs
│ ├── program/ # #[rentfree_program] attribute macro
│ ├── traits/ # Trait derive macros
│ └── shared_utils.rs # Common utilities
└── hasher/ # LightHasherSha derive macro
```
Expand Down
41 changes: 26 additions & 15 deletions sdk-libs/macros/docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,34 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros
| **`rentfree_program/`** | `#[rentfree_program]` attribute macro |
| **`rentfree_program/architecture.md`** | Architecture overview, usage, generated items |
| **`rentfree_program/codegen.md`** | Technical implementation details (code generation) |
| **`traits/`** | Trait derive macros for compressible data structs |
| **`accounts/`** | Field-level attributes for Accounts structs |
| **`account/`** | Trait derive macros for account data structs |

### Traits Documentation
### Accounts Field Attributes

Field-level attributes applied inside `#[derive(RentFree)]` Accounts structs:

| File | Attribute | Description |
|------|-----------|-------------|
| **`accounts/light_mint.md`** | `#[light_mint(...)]` | Creates compressed mint with automatic decompression |

See also: `#[rentfree]` attribute documented in `rentfree.md`

### Account Trait Documentation

| File | Macro | Description |
|------|-------|-------------|
| **`traits/has_compression_info.md`** | `#[derive(HasCompressionInfo)]` | Accessor methods for compression_info field |
| **`traits/compress_as.md`** | `#[derive(CompressAs)]` | Creates compressed representation for hashing |
| **`traits/compressible.md`** | `#[derive(Compressible)]` | Combined: HasCompressionInfo + CompressAs + Size |
| **`traits/compressible_pack.md`** | `#[derive(CompressiblePack)]` | Pack/Unpack with Pubkey-to-index compression |
| **`traits/light_compressible.md`** | `#[derive(LightCompressible)]` | All traits for rent-free accounts |
| **`account/has_compression_info.md`** | `#[derive(HasCompressionInfo)]` | Accessor methods for compression_info field |
| **`account/compress_as.md`** | `#[derive(CompressAs)]` | Creates compressed representation for hashing |
| **`account/compressible.md`** | `#[derive(Compressible)]` | Combined: HasCompressionInfo + CompressAs + Size |
| **`account/compressible_pack.md`** | `#[derive(CompressiblePack)]` | Pack/Unpack with Pubkey-to-index compression |
| **`account/light_compressible.md`** | `#[derive(LightCompressible)]` | All traits for rent-free accounts |

## Navigation Tips

### Starting Points

- **Data struct traits**: Start with `traits/light_compressible.md` for the all-in-one derive macro for compressible data structs
- **Data struct traits**: Start with `account/light_compressible.md` for the all-in-one derive macro for compressible data structs
- **Building account structs**: Use `rentfree.md` for the accounts-level derive macro that marks fields for compression
- **Program-level integration**: Use `rentfree_program/architecture.md` for program-level auto-discovery and instruction generation
- **Implementation details**: Use `rentfree_program/codegen.md` for technical code generation details
Expand All @@ -52,21 +63,21 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros
|
+-- Generates LightPreInit + LightFinalize impls
|
+-- Uses trait derives (traits/):
- HasCompressionInfo <- traits/has_compression_info.md
- CompressAs <- traits/compress_as.md
- Compressible <- traits/compressible.md
- CompressiblePack <- traits/compressible_pack.md
- LightCompressible <- traits/light_compressible.md (combines all)
+-- Uses trait derives (account/):
- HasCompressionInfo <- account/has_compression_info.md
- CompressAs <- account/compress_as.md
- Compressible <- account/compressible.md
- CompressiblePack <- account/compressible_pack.md
- LightCompressible <- account/light_compressible.md (combines all)
```

## Related Source Code

```
sdk-libs/macros/src/rentfree/
├── account/ # Trait derive macros for account data structs
├── accounts/ # #[derive(RentFree)] implementation
├── program/ # #[rentfree_program] implementation
├── traits/ # Trait derive macros
├── shared_utils.rs # Common utilities
└── mod.rs # Module exports
Comment on lines 76 to 82
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

Add language specifier to fenced code block.

Per static analysis, the code block at line 65 should have a language specifier for proper rendering and syntax highlighting.

📝 Suggested fix
-```
+```text
 sdk-libs/macros/src/rentfree/
 ├── account/         # Trait derive macros for account data structs
 ├── accounts/        # #[derive(RentFree)] implementation
 ├── program/         # #[rentfree_program] implementation
 ├── shared_utils.rs  # Common utilities
 └── mod.rs           # Module exports
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

65-65: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@sdk-libs/macros/docs/CLAUDE.md` around lines 65 - 71, Update the fenced code
block in sdk-libs/macros/docs/CLAUDE.md that contains the tree listing starting
with "sdk-libs/macros/src/rentfree/" to include a language specifier (e.g.,
change the opening ``` to ```text) so the block renders with proper syntax
highlighting; ensure only the opening fence is modified and no other content is
changed.

```
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ let user_record = packed_record.unpack(ctx.remaining_accounts)?;
- All methods are marked `#[inline(never)]` for smaller program size
- The packed struct derives `AnchorSerialize` and `AnchorDeserialize`

### Limitation: Option<Pubkey> Fields

Only direct `Pubkey` fields are converted to `u8` indices. `Option<Pubkey>` fields remain as `Option<Pubkey>` in the packed struct because `None` doesn't map cleanly to an index.

```rust
pub struct Record {
pub owner: Pubkey, // -> u8 in packed struct
pub delegate: Option<Pubkey>, // -> Option<Pubkey> in packed struct (unchanged)
}
```

Comment on lines +323 to +333
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
# Find relevant documentation and implementation files
echo "=== Finding compressible_pack.md ==="
find . -name "compressible_pack.md" -type f

echo "=== Finding pack_unpack.rs ==="
find . -name "pack_unpack.rs" -type f

echo "=== Finding CLAUDE.md files in macros ==="
find sdk-libs/macros -name "CLAUDE.md" -type f

Repository: Lightprotocol/light-protocol

Length of output: 344


🏁 Script executed:

#!/bin/bash
# Read the documentation file to see the context and Field Handling table
echo "=== compressible_pack.md context (lines 280-340) ==="
sed -n '280,340p' ./sdk-libs/macros/docs/account/compressible_pack.md

echo ""
echo "=== File length and structure ==="
wc -l ./sdk-libs/macros/docs/account/compressible_pack.md

Repository: Lightprotocol/light-protocol

Length of output: 2023


🏁 Script executed:

#!/bin/bash
# Read the pack_unpack.rs implementation to verify Option<Pubkey> handling
echo "=== pack_unpack.rs full contents ==="
wc -l ./sdk-libs/macros/src/rentfree/account/pack_unpack.rs

echo ""
echo "=== Searching for Option handling ==="
rg -n "Option" ./sdk-libs/macros/src/rentfree/account/pack_unpack.rs

echo ""
echo "=== Searching for Pubkey handling ==="
rg -n -A3 -B3 "Pubkey" ./sdk-libs/macros/src/rentfree/account/pack_unpack.rs | head -100

Repository: Lightprotocol/light-protocol

Length of output: 244


🏁 Script executed:

#!/bin/bash
# Read the actual implementation file
echo "=== pack_unpack.rs full contents ==="
cat -n ./sdk-libs/macros/src/rentfree/account/pack_unpack.rs

echo ""
echo "=== Check for pack/unpack method implementations ==="
rg -n "fn pack|fn unpack" ./sdk-libs/macros/src/rentfree/account/pack_unpack.rs

Repository: Lightprotocol/light-protocol

Length of output: 7834


🏁 Script executed:

#!/bin/bash
# Now check the is_pubkey_type function to see how it handles Option<Pubkey>
echo "=== Finding utils.rs ==="
find ./sdk-libs/macros/src/rentfree/account -name "utils.rs" -type f

echo ""
echo "=== Check utils.rs for is_pubkey_type implementation ==="
cat -n ./sdk-libs/macros/src/rentfree/account/utils.rs

Repository: Lightprotocol/light-protocol

Length of output: 4769


🏁 Script executed:

#!/bin/bash
# Verify the logic by checking what happens with a struct containing Option<Pubkey>
echo "=== Checking if there are any test cases or examples with Option<Pubkey> ==="
rg -n "Option.*Pubkey\|Option<Pubkey>" ./sdk-libs/macros/tests/ --type rust

echo ""
echo "=== Check CLAUDE.md for guidance on macro patterns ==="
cat -n ./sdk-libs/macros/CLAUDE.md | head -50

Repository: Lightprotocol/light-protocol

Length of output: 2421


🏁 Script executed:

#!/bin/bash
# Let me verify the logic flow one more time to be absolutely certain
echo "=== Simulating what happens with Option<Pubkey> ==="
echo "1. is_pubkey_type for Option<Pubkey>:"
echo "   - Checks if last path segment is 'Pubkey'"
echo "   - Option<Pubkey> has last segment 'Pubkey'? NO, it's the outer Option"
echo "   - Result: FALSE"
echo ""
echo "2. is_copy_type for Option<Pubkey>:"
echo "   - Checks if type_name == 'Option'"
echo "   - has_copy_inner_type checks inner type (Pubkey)"
echo "   - Pubkey is in copy list (line 67)"
echo "   - Result: TRUE (because Option<Pubkey> inner is copy)"
echo ""
echo "3. Packed field generation (line 32-35):"
echo "   - is_pubkey_type = FALSE"
echo "   - packed_type = original type = Option<Pubkey>"
echo ""
echo "4. Pack assignment (line 54-62):"
echo "   - is_pubkey_type = FALSE"
echo "   - is_copy_type = TRUE"
echo "   - Uses: self.#field_name (direct copy, line 59)"
echo ""
echo "5. Unpack assignment (line 109-118):"
echo "   - is_pubkey_type = FALSE"
echo "   - is_copy_type = TRUE"
echo "   - Uses: self.#field_name (direct copy, line 114)"

Repository: Lightprotocol/light-protocol

Length of output: 920


Add Option<Pubkey> row to the Field Handling table for documentation completeness.

The new limitation section correctly documents that Option<Pubkey> fields are not converted to indices and remain unchanged. However, the Field Handling table (lines 284-289) omits this case, leaving the documentation incomplete. A single-row addition clarifies the behavior:

Field Type Pack Behavior Unpack Behavior
Option<Pubkey> Direct copy (unchanged) Direct copy (unchanged)

This brings the table in line with the documented behavior where Option<Pubkey> fields bypass Pubkey-to-index compression.

The example would also benefit from showing the generated PackedRecord struct alongside the input struct, similar to other examples in the documentation, for users to see the transformation clearly.

🤖 Prompt for AI Agents
In `@sdk-libs/macros/docs/account/compressible_pack.md` around lines 323 - 333,
Add a table row documenting Option<Pubkey> handling and show the generated
PackedRecord example: update the Field Handling table to include a row for
`Option<Pubkey>` with "Direct copy (unchanged)" for both Pack and Unpack, and
add a brief example showing the input struct `Record { owner: Pubkey, delegate:
Option<Pubkey> }` alongside the generated `PackedRecord` to illustrate that
`owner` becomes a u8 index while `delegate` remains `Option<Pubkey>`; reference
the documented examples for consistency with naming and formatting.

---

## 10. Related Macros
Expand Down
Loading
Loading