diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index 4cf42f5b2d..7382b1c400 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -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"))] diff --git a/program-tests/utils/src/assert_mint_action.rs b/program-tests/utils/src/assert_mint_action.rs index 680063e28e..10b0439c25 100644 --- a/program-tests/utils/src/assert_mint_action.rs +++ b/program-tests/utils/src/assert_mint_action.rs @@ -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}; @@ -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() @@ -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" ); } diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs index 4b4ef31689..d56bf62a55 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs @@ -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; @@ -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( @@ -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 diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index ad6f84c389..29a9e9cb2d 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -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::{ @@ -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)] @@ -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 @@ -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() diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs index 7d2d803023..26d19466a3 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs @@ -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) }; diff --git a/sdk-libs/sdk/src/interface/compress_account.rs b/sdk-libs/sdk/src/interface/compress_account.rs index 1272c46083..6b4b7cebf6 100644 --- a/sdk-libs/sdk/src/interface/compress_account.rs +++ b/sdk-libs/sdk/src/interface/compress_account.rs @@ -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; @@ -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 @@ -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) } diff --git a/sdk-libs/sdk/src/interface/compress_account_on_init.rs b/sdk-libs/sdk/src/interface/compress_account_on_init.rs index d4122db070..1518250f0c 100644 --- a/sdk-libs/sdk/src/interface/compress_account_on_init.rs +++ b/sdk-libs/sdk/src/interface/compress_account_on_init.rs @@ -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; @@ -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. @@ -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) } diff --git a/sdk-libs/sdk/src/interface/decompress_idempotent.rs b/sdk-libs/sdk/src/interface/decompress_idempotent.rs index d4512bfacb..aa41ec0c98 100644 --- a/sdk-libs/sdk/src/interface/decompress_idempotent.rs +++ b/sdk-libs/sdk/src/interface/decompress_idempotent.rs @@ -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, }; @@ -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. @@ -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)) } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs index 8f1b1dde98..7e96df7a81 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs @@ -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, @@ -235,7 +235,7 @@ 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 @@ -243,7 +243,10 @@ async fn test_create_pdas_and_mint_auto() { .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 @@ -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" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs index 938500f4bb..dfd8d98e61 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs @@ -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, @@ -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