Skip to content

Commit 551f0f1

Browse files
wip
1 parent db37aa3 commit 551f0f1

File tree

4 files changed

+432
-0
lines changed

4 files changed

+432
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use borsh::{BorshDeserialize, BorshSerialize};
2+
use light_sdk::{
3+
account::LightAccount,
4+
cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs},
5+
error::LightSdkError,
6+
instruction::ValidityProof,
7+
};
8+
use solana_program::{
9+
account_info::AccountInfo, clock::Clock, msg, program_error::ProgramError, pubkey::Pubkey,
10+
sysvar::Sysvar,
11+
};
12+
13+
use crate::{create_pda::MyCompressedAccount, decompress_to_pda::DecompressedPdaAccount};
14+
15+
/// Compresses a PDA back into a compressed account
16+
/// Anyone can call this after the timeout period has elapsed
17+
pub fn compress_from_pda(
18+
accounts: &[AccountInfo],
19+
instruction_data: &[u8],
20+
) -> Result<(), LightSdkError> {
21+
msg!("Compressing PDA back to compressed account");
22+
23+
let mut instruction_data = instruction_data;
24+
let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data)
25+
.map_err(|_| LightSdkError::Borsh)?;
26+
27+
// Get accounts
28+
let fee_payer = &accounts[0];
29+
let pda_account = &accounts[1];
30+
let rent_recipient = &accounts[2]; // Hardcoded by caller program
31+
let _system_program = &accounts[3];
32+
33+
// Verify the PDA account is owned by our program
34+
if pda_account.owner != &crate::ID {
35+
msg!("PDA account not owned by this program");
36+
return Err(LightSdkError::ConstraintViolation);
37+
}
38+
39+
// Read and deserialize PDA data
40+
let pda_data = pda_account.try_borrow_data()?;
41+
42+
// Check discriminator
43+
if &pda_data[..8] != b"decomppd" {
44+
msg!("Invalid PDA discriminator");
45+
return Err(LightSdkError::ConstraintViolation);
46+
}
47+
48+
let decompressed_pda = DecompressedPdaAccount::deserialize(&mut &pda_data[8..])
49+
.map_err(|_| LightSdkError::Borsh)?;
50+
51+
// Check if enough time has passed
52+
let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?;
53+
let current_slot = clock.slot;
54+
let slots_elapsed = current_slot.saturating_sub(decompressed_pda.last_written_slot);
55+
56+
if slots_elapsed < decompressed_pda.slots_until_compression {
57+
msg!(
58+
"Cannot compress yet. {} slots remaining",
59+
decompressed_pda
60+
.slots_until_compression
61+
.saturating_sub(slots_elapsed)
62+
);
63+
return Err(LightSdkError::ConstraintViolation);
64+
}
65+
66+
// Derive PDA to verify it matches
67+
let (pda_pubkey, _pda_bump) = Pubkey::find_program_address(
68+
&[
69+
b"decompressed_pda",
70+
&decompressed_pda.compressed_address,
71+
&instruction_data.additional_seed,
72+
],
73+
&crate::ID,
74+
);
75+
76+
if pda_pubkey != *pda_account.key {
77+
msg!("PDA derivation mismatch");
78+
return Err(LightSdkError::ConstraintViolation);
79+
}
80+
81+
// Drop the borrow before we close the account
82+
drop(pda_data);
83+
84+
// Close the PDA account and send rent to recipient
85+
let pda_lamports = pda_account.lamports();
86+
**pda_account.try_borrow_mut_lamports()? = 0;
87+
**rent_recipient.try_borrow_mut_lamports()? = rent_recipient
88+
.lamports()
89+
.checked_add(pda_lamports)
90+
.ok_or(ProgramError::ArithmeticOverflow)?;
91+
92+
// Clear the PDA data
93+
pda_account.try_borrow_mut_data()?.fill(0);
94+
95+
// Now create the compressed account with the latest data
96+
let mut compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init(
97+
&crate::ID,
98+
Some(decompressed_pda.compressed_address),
99+
instruction_data.output_merkle_tree_index,
100+
);
101+
102+
compressed_account.data = decompressed_pda.data;
103+
104+
// Set up CPI accounts for light system program
105+
let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER);
106+
config.sol_pool_pda = true; // We're compressing SOL
107+
108+
let cpi_accounts = CpiAccounts::new_with_config(
109+
fee_payer,
110+
&accounts[instruction_data.system_accounts_offset as usize..],
111+
config,
112+
);
113+
114+
// Create CPI inputs
115+
let mut cpi_inputs = CpiInputs::new_with_address(
116+
instruction_data.proof,
117+
vec![compressed_account.to_account_info()?],
118+
vec![instruction_data.new_address_params],
119+
);
120+
121+
// Set compression parameters
122+
// We're compressing the lamports that were in the PDA
123+
cpi_inputs.compress_or_decompress_lamports = Some(instruction_data.lamports_to_compress);
124+
cpi_inputs.is_compress = true;
125+
126+
// Invoke light system program
127+
cpi_inputs.invoke_light_system_program(cpi_accounts)?;
128+
129+
msg!("Successfully compressed PDA back to compressed account");
130+
Ok(())
131+
}
132+
133+
#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)]
134+
pub struct CompressFromPdaInstructionData {
135+
pub proof: ValidityProof,
136+
pub new_address_params: light_sdk::address::PackedNewAddressParams,
137+
pub output_merkle_tree_index: u8,
138+
pub additional_seed: [u8; 32], // Must match the seed used in decompression
139+
pub lamports_to_compress: u64,
140+
pub system_accounts_offset: u8,
141+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use borsh::{BorshDeserialize, BorshSerialize};
2+
use light_sdk::{
3+
account::LightAccount,
4+
cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs},
5+
error::LightSdkError,
6+
instruction::{
7+
account_meta::{CompressedAccountMeta, CompressedAccountMetaTrait},
8+
ValidityProof,
9+
},
10+
LightDiscriminator, LightHasher,
11+
};
12+
use solana_program::{
13+
account_info::AccountInfo, clock::Clock, msg, program::invoke_signed, pubkey::Pubkey,
14+
rent::Rent, system_instruction, sysvar::Sysvar,
15+
};
16+
17+
pub const SLOTS_UNTIL_COMPRESSION: u64 = 100;
18+
19+
/// Account structure for the decompressed PDA
20+
#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)]
21+
pub struct DecompressedPdaAccount {
22+
/// The compressed account address this PDA was derived from
23+
pub compressed_address: [u8; 32],
24+
/// Slot when this account was last written
25+
pub last_written_slot: u64,
26+
/// Number of slots until this account can be compressed again
27+
pub slots_until_compression: u64,
28+
/// The actual account data
29+
pub data: [u8; 31],
30+
/// Flag to indicate if this is a decompressed account
31+
pub is_decompressed: bool,
32+
}
33+
34+
/// Compressed account structure with decompression flag
35+
#[derive(
36+
Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize,
37+
)]
38+
pub struct DecompressedMarkerAccount {
39+
/// Flag to indicate this account has been decompressed
40+
pub is_decompressed: bool,
41+
}
42+
43+
/// Decompresses a compressed account into a PDA
44+
/// The PDA is derived from the compressed account's address and other seeds
45+
pub fn decompress_to_pda(
46+
accounts: &[AccountInfo],
47+
instruction_data: &[u8],
48+
) -> Result<(), LightSdkError> {
49+
msg!("Decompressing compressed account to PDA");
50+
51+
let mut instruction_data = instruction_data;
52+
let instruction_data = DecompressToPdaInstructionData::deserialize(&mut instruction_data)
53+
.map_err(|_| LightSdkError::Borsh)?;
54+
55+
// Get accounts
56+
let fee_payer = &accounts[0];
57+
let pda_account = &accounts[1];
58+
let rent_payer = &accounts[2]; // Account that pays for PDA rent
59+
let system_program = &accounts[3];
60+
61+
// Derive PDA from compressed address
62+
let compressed_address = instruction_data.compressed_account.meta.address;
63+
let (pda_pubkey, pda_bump) = Pubkey::find_program_address(
64+
&[
65+
b"decompressed_pda",
66+
&compressed_address,
67+
&instruction_data.additional_seed,
68+
],
69+
&crate::ID,
70+
);
71+
72+
// Verify PDA matches
73+
if pda_pubkey != *pda_account.key {
74+
msg!("Invalid PDA pubkey");
75+
return Err(LightSdkError::ConstraintViolation);
76+
}
77+
78+
// Get current slot
79+
let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?;
80+
let current_slot = clock.slot;
81+
82+
// Calculate space needed for PDA
83+
let space = std::mem::size_of::<DecompressedPdaAccount>() + 8; // +8 for discriminator
84+
85+
// Get minimum rent
86+
let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?;
87+
let minimum_balance = rent.minimum_balance(space);
88+
89+
// Create PDA account (rent payer pays for the PDA creation)
90+
let create_account_ix = system_instruction::create_account(
91+
rent_payer.key,
92+
pda_account.key,
93+
minimum_balance,
94+
space as u64,
95+
&crate::ID,
96+
);
97+
98+
let signer_seeds = &[
99+
b"decompressed_pda".as_ref(),
100+
compressed_address.as_ref(),
101+
instruction_data.additional_seed.as_ref(),
102+
&[pda_bump],
103+
];
104+
105+
invoke_signed(
106+
&create_account_ix,
107+
&[
108+
rent_payer.clone(),
109+
pda_account.clone(),
110+
system_program.clone(),
111+
],
112+
&[signer_seeds],
113+
)?;
114+
115+
// Initialize PDA with decompressed data
116+
let decompressed_pda = DecompressedPdaAccount {
117+
compressed_address,
118+
last_written_slot: current_slot,
119+
slots_until_compression: SLOTS_UNTIL_COMPRESSION,
120+
data: instruction_data.compressed_account.data,
121+
is_decompressed: true,
122+
};
123+
124+
// Write data to PDA
125+
decompressed_pda
126+
.serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..])
127+
.map_err(|_| LightSdkError::Borsh)?;
128+
129+
// Write discriminator
130+
pda_account.try_borrow_mut_data()?[..8].copy_from_slice(b"decomppd");
131+
132+
// Now handle the compressed account side
133+
// Create a marker account that indicates this compressed account has been decompressed
134+
let marker_account = LightAccount::<'_, DecompressedMarkerAccount>::new_mut(
135+
&crate::ID,
136+
&instruction_data.compressed_account.meta,
137+
DecompressedMarkerAccount {
138+
is_decompressed: true,
139+
},
140+
)?;
141+
142+
// Set up CPI accounts for light system program
143+
let mut config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER);
144+
config.sol_pool_pda = false;
145+
config.sol_compression_recipient = true; // We need to decompress SOL to the PDA
146+
147+
let cpi_accounts = CpiAccounts::new_with_config(
148+
fee_payer,
149+
&accounts[instruction_data.system_accounts_offset as usize..],
150+
config,
151+
);
152+
153+
// Create CPI inputs with decompression
154+
let mut cpi_inputs = CpiInputs::new(
155+
instruction_data.proof,
156+
vec![marker_account.to_account_info()?],
157+
);
158+
159+
// Set decompression parameters
160+
// Transfer all lamports from compressed account to the PDA
161+
let lamports_to_decompress = instruction_data
162+
.compressed_account
163+
.meta
164+
.get_lamports()
165+
.unwrap_or(0);
166+
167+
cpi_inputs.compress_or_decompress_lamports = Some(lamports_to_decompress);
168+
cpi_inputs.is_compress = false; // This is decompression
169+
170+
// Invoke light system program
171+
cpi_inputs.invoke_light_system_program(cpi_accounts)?;
172+
173+
msg!("Successfully decompressed account to PDA");
174+
Ok(())
175+
}
176+
177+
#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)]
178+
pub struct DecompressToPdaInstructionData {
179+
pub proof: ValidityProof,
180+
pub compressed_account: DecompressMyCompressedAccount,
181+
pub additional_seed: [u8; 32], // Additional seed for PDA derivation
182+
pub system_accounts_offset: u8,
183+
}
184+
185+
#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)]
186+
pub struct DecompressMyCompressedAccount {
187+
pub meta: CompressedAccountMeta,
188+
pub data: [u8; 31],
189+
}

program-tests/sdk-test/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use solana_program::{
44
account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey,
55
};
66

7+
pub mod compress_from_pda;
78
pub mod create_pda;
9+
pub mod decompress_to_pda;
10+
pub mod update_decompressed_pda;
811
pub mod update_pda;
912

1013
pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy");
@@ -17,6 +20,9 @@ entrypoint!(process_instruction);
1720
pub enum InstructionType {
1821
CreatePdaBorsh = 0,
1922
UpdatePdaBorsh = 1,
23+
DecompressToPda = 2,
24+
CompressFromPda = 3,
25+
UpdateDecompressedPda = 4,
2026
}
2127

2228
impl TryFrom<u8> for InstructionType {
@@ -26,6 +32,9 @@ impl TryFrom<u8> for InstructionType {
2632
match value {
2733
0 => Ok(InstructionType::CreatePdaBorsh),
2834
1 => Ok(InstructionType::UpdatePdaBorsh),
35+
2 => Ok(InstructionType::DecompressToPda),
36+
3 => Ok(InstructionType::CompressFromPda),
37+
4 => Ok(InstructionType::UpdateDecompressedPda),
2938
_ => panic!("Invalid instruction discriminator."),
3039
}
3140
}
@@ -44,6 +53,15 @@ pub fn process_instruction(
4453
InstructionType::UpdatePdaBorsh => {
4554
update_pda::update_pda::<false>(accounts, &instruction_data[1..])
4655
}
56+
InstructionType::DecompressToPda => {
57+
decompress_to_pda::decompress_to_pda(accounts, &instruction_data[1..])
58+
}
59+
InstructionType::CompressFromPda => {
60+
compress_from_pda::compress_from_pda(accounts, &instruction_data[1..])
61+
}
62+
InstructionType::UpdateDecompressedPda => {
63+
update_decompressed_pda::update_decompressed_pda(accounts, &instruction_data[1..])
64+
}
4765
}?;
4866
Ok(())
4967
}

0 commit comments

Comments
 (0)