Skip to content

Commit baf768f

Browse files
wip
1 parent 9d78913 commit baf768f

File tree

2 files changed

+209
-71
lines changed

2 files changed

+209
-71
lines changed

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,76 @@ impl crate::sdk::compress_pda::PdaTimingData for MyPdaAccount {
104104
self.last_written_slot = slot;
105105
}
106106
}
107+
108+
/// Example: Decompresses multiple compressed accounts into PDAs in a single transaction.
109+
pub fn decompress_multiple_to_pda(
110+
accounts: &[AccountInfo],
111+
instruction_data: &[u8],
112+
) -> Result<(), LightSdkError> {
113+
use crate::sdk::decompress_idempotent::decompress_multiple_idempotent;
114+
115+
#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)]
116+
pub struct DecompressMultipleInstructionData {
117+
pub proof: ValidityProof,
118+
pub compressed_accounts: Vec<MyCompressedAccount>,
119+
pub system_accounts_offset: u8,
120+
}
121+
122+
let mut instruction_data = instruction_data;
123+
let instruction_data = DecompressMultipleInstructionData::deserialize(&mut instruction_data)
124+
.map_err(|_| LightSdkError::Borsh)?;
125+
126+
// Get fixed accounts
127+
let fee_payer = &accounts[0];
128+
let rent_payer = &accounts[1];
129+
let system_program = &accounts[2];
130+
131+
// Calculate where PDA accounts start
132+
let pda_accounts_start = 3;
133+
let num_accounts = instruction_data.compressed_accounts.len();
134+
135+
// Get PDA accounts
136+
let pda_accounts = &accounts[pda_accounts_start..pda_accounts_start + num_accounts];
137+
138+
// Cpi accounts
139+
let cpi_accounts = CpiAccounts::new_with_config(
140+
fee_payer,
141+
&accounts[instruction_data.system_accounts_offset as usize..],
142+
CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER),
143+
);
144+
145+
// Custom seeds for PDA derivation (same for all accounts in this example)
146+
let custom_seeds: Vec<&[u8]> = vec![b"decompressed_pda"];
147+
148+
// Build inputs for batch decompression
149+
let mut compressed_accounts = Vec::new();
150+
let mut seeds_list = Vec::new();
151+
let mut pda_account_refs = Vec::new();
152+
153+
for (i, compressed_account_data) in instruction_data.compressed_accounts.into_iter().enumerate()
154+
{
155+
let compressed_account = LightAccount::<'_, MyPdaAccount>::new_mut(
156+
&crate::ID,
157+
&compressed_account_data.meta,
158+
compressed_account_data.data,
159+
)?;
160+
161+
compressed_accounts.push(compressed_account);
162+
seeds_list.push(custom_seeds.clone());
163+
pda_account_refs.push(&pda_accounts[i]);
164+
}
165+
166+
// Decompress all accounts in one CPI call
167+
decompress_multiple_idempotent::<MyPdaAccount>(
168+
&pda_account_refs,
169+
compressed_accounts,
170+
&seeds_list,
171+
instruction_data.proof,
172+
cpi_accounts,
173+
&crate::ID,
174+
rent_payer,
175+
system_program,
176+
)?;
177+
178+
Ok(())
179+
}

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

Lines changed: 136 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100;
2323
///
2424
/// # Arguments
2525
/// * `pda_account` - The PDA account to decompress into
26-
/// * `compressed_account_meta` - Optional metadata for the compressed account (None if PDA already exists)
27-
/// * `compressed_account_data` - The data to write to the PDA
28-
/// * `proof` - Optional validity proof (None if PDA already exists)
26+
/// * `compressed_account` - The compressed account to decompress
27+
/// * `proof` - Validity proof
2928
/// * `cpi_accounts` - Accounts needed for CPI
3029
/// * `owner_program` - The program that will own the PDA
3130
/// * `rent_payer` - The account to pay for PDA rent
@@ -38,7 +37,7 @@ pub const SLOTS_UNTIL_COMPRESSION: u64 = 100;
3837
/// * `Err(LightSdkError)` if there was an error
3938
pub fn decompress_idempotent<'info, A>(
4039
pda_account: &AccountInfo<'info>,
41-
mut compressed_account: LightAccount<'_, A>,
40+
compressed_account: LightAccount<'_, A>,
4241
proof: ValidityProof,
4342
cpi_accounts: CpiAccounts<'_, 'info>,
4443
owner_program: &Pubkey,
@@ -55,81 +54,147 @@ where
5554
+ Clone
5655
+ PdaTimingData,
5756
{
58-
// Check if PDA is already initialized
59-
if pda_account.data_len() > 0 {
60-
msg!("PDA already initialized, skipping decompression");
61-
return Ok(());
62-
}
57+
decompress_multiple_idempotent(
58+
&[pda_account],
59+
vec![compressed_account],
60+
&[custom_seeds.to_vec()],
61+
proof,
62+
cpi_accounts,
63+
owner_program,
64+
rent_payer,
65+
system_program,
66+
)
67+
}
6368

64-
// Get compressed address
65-
let compressed_address = compressed_account
66-
.address()
67-
.ok_or(LightSdkError::ConstraintViolation)?;
69+
/// Helper function to decompress multiple compressed accounts into PDAs idempotently.
70+
///
71+
/// This function is idempotent, meaning it can be called multiple times with the same compressed accounts
72+
/// and it will only decompress them once. If a PDA already exists and is initialized, it skips that account.
73+
///
74+
/// # Arguments
75+
/// * `decompress_inputs` - Vector of tuples containing (pda_account, compressed_account, custom_seeds, additional_seed)
76+
/// * `proof` - Single validity proof for all accounts
77+
/// * `cpi_accounts` - Accounts needed for CPI
78+
/// * `owner_program` - The program that will own the PDAs
79+
/// * `rent_payer` - The account to pay for PDA rent
80+
/// * `system_program` - The system program
81+
///
82+
/// # Returns
83+
/// * `Ok(())` if all compressed accounts were decompressed successfully or PDAs already exist
84+
/// * `Err(LightSdkError)` if there was an error
85+
pub fn decompress_multiple_idempotent<'info, A>(
86+
pda_accounts: &[&AccountInfo<'info>],
87+
compressed_accounts: Vec<LightAccount<'_, A>>,
88+
custom_seeds_list: &[Vec<&[u8]>],
89+
proof: ValidityProof,
90+
cpi_accounts: CpiAccounts<'_, 'info>,
91+
owner_program: &Pubkey,
92+
rent_payer: &AccountInfo<'info>,
93+
system_program: &AccountInfo<'info>,
94+
) -> Result<(), LightSdkError>
95+
where
96+
A: DataHasher
97+
+ LightDiscriminator
98+
+ BorshSerialize
99+
+ BorshDeserialize
100+
+ Default
101+
+ Clone
102+
+ PdaTimingData,
103+
{
104+
// Get current slot and rent once for all accounts
105+
let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?;
106+
let current_slot = clock.slot;
107+
let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?;
68108

69-
// Derive onchain PDA
70-
// CHECK: PDA is derived from compressed account address.
71-
let mut seeds: Vec<&[u8]> = custom_seeds.to_vec();
72-
seeds.push(&compressed_address);
109+
// Calculate space needed for PDA (same for all accounts of type A)
110+
let space = std::mem::size_of::<A>() + 8; // +8 for discriminator
111+
let minimum_balance = rent.minimum_balance(space);
73112

74-
let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program); // TODO: consider passing the bump.
113+
// Collect compressed accounts for CPI
114+
let mut compressed_accounts_for_cpi = Vec::new();
115+
116+
for ((pda_account, mut compressed_account), custom_seeds) in pda_accounts
117+
.iter()
118+
.zip(compressed_accounts.into_iter())
119+
.zip(custom_seeds_list.iter())
120+
.map(|((pda, ca), seeds)| ((pda, ca), seeds.clone()))
121+
{
122+
// Check if PDA is already initialized
123+
if pda_account.data_len() > 0 {
124+
msg!(
125+
"PDA {} already initialized, skipping decompression",
126+
pda_account.key
127+
);
128+
continue;
129+
}
75130

76-
// Verify PDA matches
77-
if pda_pubkey != *pda_account.key {
78-
msg!("Invalid PDA pubkey");
79-
return Err(LightSdkError::ConstraintViolation);
80-
}
131+
// Get compressed address
132+
let compressed_address = compressed_account
133+
.address()
134+
.ok_or(LightSdkError::ConstraintViolation)?;
81135

82-
// Get current slot
83-
let clock = Clock::get().map_err(|_| LightSdkError::Borsh)?;
84-
let current_slot = clock.slot;
136+
// Derive onchain PDA
137+
let mut seeds: Vec<&[u8]> = custom_seeds;
138+
seeds.push(&compressed_address);
85139

86-
// Calculate space needed for PDA
87-
let space = std::mem::size_of::<A>() + 8; // +8 for discriminator
140+
let (pda_pubkey, pda_bump) = Pubkey::find_program_address(&seeds, owner_program);
88141

89-
// Get minimum rent
90-
let rent = Rent::get().map_err(|_| LightSdkError::Borsh)?;
91-
let minimum_balance = rent.minimum_balance(space);
142+
// Verify PDA matches
143+
if pda_pubkey != *pda_account.key {
144+
msg!("Invalid PDA pubkey for account {}", pda_account.key);
145+
return Err(LightSdkError::ConstraintViolation);
146+
}
92147

93-
// Create PDA account
94-
let create_account_ix = system_instruction::create_account(
95-
rent_payer.key,
96-
pda_account.key,
97-
minimum_balance,
98-
space as u64,
99-
owner_program,
100-
);
101-
102-
// Add bump to seeds for signing
103-
let bump_seed = [pda_bump];
104-
let mut signer_seeds = seeds.clone();
105-
signer_seeds.push(&bump_seed);
106-
let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect();
107-
108-
invoke_signed(
109-
&create_account_ix,
110-
&[
111-
rent_payer.clone(),
112-
pda_account.clone(),
113-
system_program.clone(),
114-
],
115-
&[&signer_seeds_refs],
116-
)?;
117-
118-
// Initialize PDA with decompressed data and update slot
119-
let mut decompressed_pda = compressed_account.account.clone();
120-
decompressed_pda.set_last_written_slot(current_slot);
121-
// Write data to PDA
122-
decompressed_pda
123-
.serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..])
124-
.map_err(|_| LightSdkError::Borsh)?;
125-
126-
// Zero the compressed account
127-
compressed_account.account = A::default();
128-
129-
let cpi_inputs = CpiInputs::new(proof, vec![compressed_account.to_account_info()?]);
130-
cpi_inputs.invoke_light_system_program(cpi_accounts)?;
131-
132-
drop(pda_account.try_borrow_mut_data()?); // todo: check if this is needed.
148+
// Create PDA account
149+
let create_account_ix = system_instruction::create_account(
150+
rent_payer.key,
151+
pda_account.key,
152+
minimum_balance,
153+
space as u64,
154+
owner_program,
155+
);
156+
157+
// Add bump to seeds for signing
158+
let bump_seed = [pda_bump];
159+
let mut signer_seeds = seeds.clone();
160+
signer_seeds.push(&bump_seed);
161+
let signer_seeds_refs: Vec<&[u8]> = signer_seeds.iter().map(|s| *s).collect();
162+
163+
invoke_signed(
164+
&create_account_ix,
165+
&[
166+
rent_payer.clone(),
167+
(*pda_account).clone(),
168+
system_program.clone(),
169+
],
170+
&[&signer_seeds_refs],
171+
)?;
172+
173+
// Initialize PDA with decompressed data and update slot
174+
let mut decompressed_pda = compressed_account.account.clone();
175+
decompressed_pda.set_last_written_slot(current_slot);
176+
177+
// Write discriminator
178+
let discriminator = A::LIGHT_DISCRIMINATOR;
179+
pda_account.try_borrow_mut_data()?[..8].copy_from_slice(&discriminator);
180+
181+
// Write data to PDA
182+
decompressed_pda
183+
.serialize(&mut &mut pda_account.try_borrow_mut_data()?[8..])
184+
.map_err(|_| LightSdkError::Borsh)?;
185+
186+
// Zero the compressed account
187+
compressed_account.account = A::default();
188+
189+
// Add to CPI batch
190+
compressed_accounts_for_cpi.push(compressed_account.to_account_info()?);
191+
}
192+
193+
// Make single CPI call with all compressed accounts
194+
if !compressed_accounts_for_cpi.is_empty() {
195+
let cpi_inputs = CpiInputs::new(proof, compressed_accounts_for_cpi);
196+
cpi_inputs.invoke_light_system_program(cpi_accounts)?;
197+
}
133198

134199
Ok(())
135200
}

0 commit comments

Comments
 (0)