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
5 changes: 5 additions & 0 deletions program-libs/compressible/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ pub mod error;
pub mod registry_instructions;
pub mod rent;

/// Decompressed PDA discriminator - marks a compressed account as a decompressed PDA placeholder.
/// When a CMint or other PDA is decompressed to a Solana account, the compressed account
/// stores this discriminator and the PDA pubkey (hashed) to preserve the address.
pub const DECOMPRESSED_PDA_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 0];

#[cfg(feature = "anchor")]
use anchor_lang::{AnchorDeserialize, AnchorSerialize};
#[cfg(not(feature = "anchor"))]
Expand Down
18 changes: 12 additions & 6 deletions program-tests/utils/src/assert_mint_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use std::collections::HashMap;

use anchor_lang::prelude::borsh::BorshDeserialize;
use light_client::indexer::Indexer;
use light_compressed_account::compressed_account::CompressedAccountData;
use light_compressible::compression_info::CompressionInfo;
use light_compressible::{compression_info::CompressionInfo, DECOMPRESSED_PDA_DISCRIMINATOR};
use light_program_test::{LightProgramTest, Rpc};
use light_token_client::instructions::mint_action::MintActionType;
use light_token_interface::state::{extensions::AdditionalMetadata, ExtensionStruct, Mint, Token};
Expand Down Expand Up @@ -183,7 +182,7 @@ pub async fn assert_mint_action(
"Mint state should match expected after applying actions"
);

// Verify compressed account has sentinel values when decompressed
// Verify compressed account has decompressed PDA format when decompressed
if post_decompressed {
let sentinel_account = rpc
.indexer()
Expand All @@ -193,10 +192,17 @@ pub async fn assert_mint_action(
.unwrap()
.value
.expect("Compressed mint account not found");
let sentinel_data = sentinel_account.data.as_ref().unwrap();
// Decompressed PDAs have DECOMPRESSED_PDA_DISCRIMINATOR and data contains PDA pubkey
assert_eq!(
*sentinel_account.data.as_ref().unwrap(),
CompressedAccountData::default(),
"Compressed mint should have sentinel values when Mint is source of truth"
sentinel_data.discriminator, DECOMPRESSED_PDA_DISCRIMINATOR,
"Compressed mint should have DECOMPRESSED_PDA_DISCRIMINATOR when decompressed"
);
let mint_pda = Pubkey::from(pre_compressed_mint.metadata.mint);
assert_eq!(
sentinel_data.data,
mint_pda.to_bytes().to_vec(),
"Compressed mint data should contain the mint PDA pubkey when decompressed"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use borsh::BorshSerialize;
use light_compressed_account::{
compressed_account::PackedMerkleContext, instruction_data::with_readonly::ZInAccountMut,
};
use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR;
use light_hasher::{sha256::Sha256BE, Hasher};
use light_program_profiler::profile;
use light_token_interface::state::Mint;
Expand All @@ -18,7 +19,7 @@ use crate::{
/// but processes existing compressed mint accounts as inputs.
///
/// Steps:
/// 1. Determine if CMint is decompressed (use zero values) or data from instruction
/// 1. Determine if CMint is decompressed (use PDA discriminator and hash) or data from instruction
/// 2. Set InAccount fields (discriminator, merkle hash, address)
#[profile]
pub fn create_input_compressed_mint_account(
Expand All @@ -28,9 +29,13 @@ pub fn create_input_compressed_mint_account(
accounts_config: &AccountsConfig,
compressed_mint: &Mint,
) -> Result<(), ProgramError> {
// When CMint was decompressed (input state BEFORE actions), use zero values
// When CMint was decompressed (input state BEFORE actions), use PDA discriminator and hash of PDA pubkey
let (discriminator, input_data_hash) = if accounts_config.cmint_decompressed {
([0u8; 8], [0u8; 32])
// The mint pubkey is the CMint PDA - hash it for the data_hash
(
DECOMPRESSED_PDA_DISCRIMINATOR,
Sha256BE::hash(&compressed_mint.metadata.mint.to_bytes())?,
)
} else {
// Data from instruction - compute hash
let bytes = compressed_mint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anchor_compressed_token::ErrorCode;
use anchor_lang::prelude::ProgramError;
use borsh::BorshSerialize;
use light_compressed_account::instruction_data::data::ZOutputCompressedAccountWithPackedContextMut;
use light_compressible::rent::get_rent_exemption_lamports;
use light_compressible::{rent::get_rent_exemption_lamports, DECOMPRESSED_PDA_DISCRIMINATOR};
use light_hasher::{sha256::Sha256BE, Hasher};
use light_program_profiler::profile;
use light_token_interface::{
Expand Down Expand Up @@ -50,7 +50,12 @@ pub fn process_output_compressed_account<'a>(
serialize_decompressed_mint(validated_accounts, accounts_config, &mut compressed_mint)?;
}

serialize_compressed_mint(mint_account, compressed_mint, queue_indices)
serialize_compressed_mint(
mint_account,
compressed_mint,
queue_indices,
validated_accounts,
)
}

#[inline(always)]
Expand All @@ -72,6 +77,7 @@ fn serialize_compressed_mint<'a>(
mint_account: &'a mut ZOutputCompressedAccountWithPackedContextMut<'a>,
compressed_mint: Mint,
queue_indices: &QueueIndices,
validated_accounts: &MintActionAccounts,
) -> Result<(), ProgramError> {
let compressed_account_data = mint_account
.compressed_account
Expand All @@ -80,17 +86,25 @@ fn serialize_compressed_mint<'a>(
.ok_or(ErrorCode::MintActionOutputSerializationFailed)?;

let (discriminator, data_hash) = if compressed_mint.metadata.mint_decompressed {
if !compressed_account_data.data.is_empty() {
// When decompressed, store the PDA pubkey (32 bytes) in the data field
if compressed_account_data.data.len() != 32 {
msg!(
"Data allocation for output mint account is wrong: {} (expected) != {} ",
0,
"Data allocation for decompressed mint account is wrong: 32 (expected) != {}",
compressed_account_data.data.len()
);
return Err(ProgramError::InvalidAccountData);
}
// Zeroed discriminator and data hash preserve the address
// of a closed compressed account without any data.
([0u8; 8], [0u8; 32])
let cmint_account = validated_accounts
.get_cmint()
.ok_or(ErrorCode::CMintNotFound)?;
// Store the PDA pubkey in the data field and hash it
compressed_account_data
.data
.copy_from_slice(cmint_account.key());
(
DECOMPRESSED_PDA_DISCRIMINATOR,
Sha256BE::hash(compressed_account_data.data)?,
)
} else {
let data = compressed_mint
.try_to_vec()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ pub fn get_zero_copy_configs(
output_accounts: {
let mut outputs = ArrayVec::new();
// First output is always the mint account
// When CMint is decompressed, use data_len=0 (zero discriminator & hash)
// When CMint is decompressed, use data_len=32 (PDA pubkey stored in data)
let mint_data_len = if cmint_is_decompressed {
0
32
} else {
mint_data_len(&output_mint_config)
};
Expand Down
34 changes: 30 additions & 4 deletions sdk-libs/sdk/src/interface/compress_account.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo;
use light_compressible::rent::AccountRentState;
use light_hasher::DataHasher;
use light_compressed_account::instruction_data::with_account_info::{
CompressedAccountInfo, InAccountInfo,
};
use light_compressible::{rent::AccountRentState, DECOMPRESSED_PDA_DISCRIMINATOR};
use light_hasher::{sha256::Sha256BE, DataHasher, Hasher};
use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress;
use solana_account_info::AccountInfo;
use solana_clock::Clock;
Expand All @@ -16,6 +18,21 @@ use crate::{
instruction::account_meta::CompressedAccountMeta,
AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError,
};

/// Set input for decompressed PDA format.
/// Isolated in separate function to reduce stack usage.
#[inline(never)]
#[cfg(feature = "v2")]
fn set_decompressed_pda_input(
input: &mut InAccountInfo,
pda_pubkey_bytes: &[u8; 32],
) -> Result<(), ProgramError> {
input.data_hash = Sha256BE::hash(pda_pubkey_bytes)
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
input.discriminator = DECOMPRESSED_PDA_DISCRIMINATOR;
Ok(())
}
/// Prepare account for compression.
///
/// # Arguments
Expand Down Expand Up @@ -133,5 +150,14 @@ where
}
}

compressed_account.to_account_info()
let mut account_info_result = compressed_account.to_account_info()?;

// Fix input to use the decompressed PDA format:
// - discriminator: DECOMPRESSED_PDA_DISCRIMINATOR
// - data_hash: Sha256BE(pda_pubkey)
if let Some(input) = account_info_result.input.as_mut() {
set_decompressed_pda_input(input, &account_info.key.to_bytes())?;
}

Ok(account_info_result)
}
34 changes: 31 additions & 3 deletions sdk-libs/sdk/src/interface/compress_account_on_init.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use light_compressed_account::instruction_data::{
data::NewAddressParamsAssignedPacked, with_account_info::CompressedAccountInfo,
data::NewAddressParamsAssignedPacked,
with_account_info::{CompressedAccountInfo, OutAccountInfo},
};
use light_hasher::DataHasher;
use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR;
use light_hasher::{sha256::Sha256BE, DataHasher, Hasher};
use solana_account_info::AccountInfo;
use solana_msg::msg;
use solana_pubkey::Pubkey;
Expand All @@ -12,6 +14,22 @@ use crate::{
AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError,
};

/// Set output for decompressed PDA format.
/// Isolated in separate function to reduce stack usage.
#[inline(never)]
#[cfg(feature = "v2")]
fn set_decompressed_pda_output(
output: &mut OutAccountInfo,
pda_pubkey_bytes: &[u8; 32],
) -> Result<(), ProgramError> {
output.data = pda_pubkey_bytes.to_vec();
output.data_hash = Sha256BE::hash(pda_pubkey_bytes)
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
output.discriminator = DECOMPRESSED_PDA_DISCRIMINATOR;
Ok(())
}

/// Prepare a compressed account on init.
///
/// Does NOT close the PDA, does NOT invoke CPI.
Expand Down Expand Up @@ -112,5 +130,15 @@ where
compressed_account.remove_data();
}

compressed_account.to_account_info()
let mut account_info_result = compressed_account.to_account_info()?;

// For decompressed PDAs (with_data = false), store the PDA pubkey in data
// and set the decompressed discriminator
if !with_data {
if let Some(output) = account_info_result.output.as_mut() {
set_decompressed_pda_output(output, &account_info.key())?;
}
}

Ok(account_info_result)
}
32 changes: 30 additions & 2 deletions sdk-libs/sdk/src/interface/decompress_idempotent.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#![allow(clippy::all)] // TODO: Remove.

use light_compressed_account::address::derive_address;
use light_compressed_account::{
address::derive_address, instruction_data::with_account_info::OutAccountInfo,
};
use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR;
use light_hasher::{sha256::Sha256BE, Hasher};
use light_sdk_types::instruction::account_meta::{
CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress,
};
Expand All @@ -19,6 +23,20 @@ use crate::{
AnchorDeserialize, AnchorSerialize, LightDiscriminator,
};

/// Set output for decompressed PDA format.
/// Isolated in separate function to reduce stack usage.
#[inline(never)]
#[cfg(feature = "v2")]
fn set_decompressed_pda_output(
output: &mut OutAccountInfo,
pda_pubkey_bytes: &[u8; 32],
) -> Result<(), LightSdkError> {
output.data = pda_pubkey_bytes.to_vec();
output.data_hash = Sha256BE::hash(pda_pubkey_bytes)?;
output.discriminator = DECOMPRESSED_PDA_DISCRIMINATOR;
Ok(())
}

/// Convert a `CompressedAccountMetaNoLamportsNoAddress` to a
/// `CompressedAccountMeta` by deriving the compressed address from the solana
/// account's pubkey.
Expand Down Expand Up @@ -141,5 +159,15 @@ where
LightSdkError::Borsh
})?;

Ok(Some(light_account.to_account_info()?))
let mut account_info_result = light_account.to_account_info()?;

// Set output to use decompressed PDA format:
// - discriminator: DECOMPRESSED_PDA_DISCRIMINATOR
// - data: PDA pubkey (32 bytes)
// - data_hash: Sha256BE(pda_pubkey)
if let Some(output) = account_info_result.output.as_mut() {
set_decompressed_pda_output(output, &solana_account.key.to_bytes())?;
}

Ok(Some(account_info_result))
}
25 changes: 15 additions & 10 deletions sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use light_client::interface::{
get_create_accounts_proof, AccountInterfaceExt, CreateAccountsProofInput,
InitializeRentFreeConfig,
};
use light_compressible::rent::SLOTS_PER_EPOCH;
use light_compressible::{rent::SLOTS_PER_EPOCH, DECOMPRESSED_PDA_DISCRIMINATOR};
use light_program_test::{
program_test::{setup_mock_program_data, LightProgramTest, TestRpc},
Indexer, ProgramTestConfig, Rpc,
Expand Down Expand Up @@ -235,15 +235,18 @@ async fn test_create_pdas_and_mint_auto() {
assert_eq!(user_ata_data.owner, payer.pubkey().to_bytes());
assert_eq!(user_ata_data.amount, user_ata_mint_amount);

// Verify compressed addresses registered (empty data - decompressed to on-chain)
// Verify compressed addresses registered (decompressed PDA: data contains PDA pubkey)
let compressed_cmint = rpc
.get_compressed_account(mint_compressed_address, None)
.await
.unwrap()
.value
.unwrap();
assert_eq!(compressed_cmint.address.unwrap(), mint_compressed_address);
assert!(compressed_cmint.data.as_ref().unwrap().data.is_empty());
// Decompressed PDAs have DECOMPRESSED_PDA_DISCRIMINATOR and data contains PDA pubkey
let cmint_data = compressed_cmint.data.as_ref().unwrap();
assert_eq!(cmint_data.discriminator, DECOMPRESSED_PDA_DISCRIMINATOR);
assert_eq!(cmint_data.data, mint_pda.to_bytes().to_vec());

// Verify GameSession initial state before compression
// Fields with compress_as overrides should have their original values
Expand Down Expand Up @@ -694,14 +697,16 @@ async fn test_create_two_mints() {
"Mint B compressed address should be registered"
);

// Verify both compressed mint accounts have empty data (decompressed to on-chain)
assert!(
compressed_mint_a.data.as_ref().unwrap().data.is_empty(),
"Mint A compressed data should be empty (decompressed)"
// Verify both compressed mint accounts have decompressed PDA format (data contains PDA pubkey)
assert_eq!(
compressed_mint_a.data.as_ref().unwrap().data,
cmint_a_pda.to_bytes(),
"Mint A decompressed PDA data should contain the PDA pubkey"
);
assert!(
compressed_mint_b.data.as_ref().unwrap().data.is_empty(),
"Mint B compressed data should be empty (decompressed)"
assert_eq!(
compressed_mint_b.data.as_ref().unwrap().data,
cmint_b_pda.to_bytes(),
"Mint B decompressed PDA data should contain the PDA pubkey"
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use light_client::interface::{
decompress_mint::decompress_mint, get_create_accounts_proof, AccountInterfaceExt,
CreateAccountsProofInput, InitializeRentFreeConfig,
};
use light_compressible::rent::SLOTS_PER_EPOCH;
use light_compressible::{rent::SLOTS_PER_EPOCH, DECOMPRESSED_PDA_DISCRIMINATOR};
use light_program_test::{
program_test::{setup_mock_program_data, LightProgramTest, TestRpc},
Indexer, ProgramTestConfig, Rpc,
Expand Down Expand Up @@ -210,10 +210,16 @@ async fn test_create_mint_with_metadata() {
"Mint compressed address should be registered"
);

// Verify compressed mint account has empty data (decompressed to on-chain)
assert!(
compressed_mint.data.as_ref().unwrap().data.is_empty(),
"Mint compressed data should be empty (decompressed)"
// Verify compressed mint account has decompressed PDA format
let cmint_data = compressed_mint.data.as_ref().unwrap();
assert_eq!(
cmint_data.discriminator, DECOMPRESSED_PDA_DISCRIMINATOR,
"Decompressed PDA should have DECOMPRESSED_PDA_DISCRIMINATOR"
);
assert_eq!(
cmint_data.data,
cmint_pda.to_bytes().to_vec(),
"Decompressed PDA data should contain the PDA pubkey"
);

// Helper functions for lifecycle assertions
Expand Down