1use std::collections::HashSet;
2
3use light_compressible::rent::RentConfig;
4use solana_account_info::AccountInfo;
5use solana_cpi::invoke_signed;
6use solana_loader_v3_interface::state::UpgradeableLoaderState;
7use solana_msg::msg;
8use solana_pubkey::Pubkey;
9use solana_system_interface::instruction as system_instruction;
10use solana_sysvar::{rent::Rent, Sysvar};
11
12use crate::{error::LightSdkError, AnchorDeserialize, AnchorSerialize};
13
14pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config";
15pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1;
16const BPF_LOADER_UPGRADEABLE_ID: Pubkey =
17 Pubkey::from_str_const("BPFLoaderUpgradeab1e11111111111111111111111");
18
19#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
22pub struct CompressibleConfig {
23 pub version: u8,
25 pub write_top_up: u32,
27 pub update_authority: Pubkey,
29 pub rent_sponsor: Pubkey,
31 pub compression_authority: Pubkey,
33 pub rent_config: RentConfig,
35 pub config_bump: u8,
37 pub bump: u8,
39 pub address_space: Vec<Pubkey>,
41}
42
43impl CompressibleConfig {
44 pub const LEN: usize = 1
45 + 4
46 + 32
47 + 32
48 + 32
49 + core::mem::size_of::<RentConfig>()
50 + 1
51 + 1
52 + 4
53 + (32 * MAX_ADDRESS_TREES_PER_SPACE);
54
55 pub fn size_for_address_space(num_address_trees: usize) -> usize {
58 1 + 4
59 + 32
60 + 32
61 + 32
62 + core::mem::size_of::<RentConfig>()
63 + 1
64 + 1
65 + 4
66 + (32 * num_address_trees)
67 }
68
69 pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) {
71 let config_bump_u16 = config_bump as u16;
73 Pubkey::find_program_address(
74 &[COMPRESSIBLE_CONFIG_SEED, &config_bump_u16.to_le_bytes()],
75 program_id,
76 )
77 }
78
79 pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) {
81 Self::derive_pda(program_id, 0)
82 }
83
84 pub fn validate(&self) -> Result<(), crate::ProgramError> {
86 if self.version != 1 {
87 msg!(
88 "CompressibleConfig validation failed: Unsupported config version: {}",
89 self.version
90 );
91 return Err(LightSdkError::ConstraintViolation.into());
92 }
93 if self.address_space.len() != 1 {
94 msg!(
95 "CompressibleConfig validation failed: Address space must contain exactly 1 pubkey, found: {}",
96 self.address_space.len()
97 );
98 return Err(LightSdkError::ConstraintViolation.into());
99 }
100 if self.config_bump != 0 {
102 msg!(
103 "CompressibleConfig validation failed: Config bump must be 0 for now, found: {}",
104 self.config_bump
105 );
106 return Err(LightSdkError::ConstraintViolation.into());
107 }
108 Ok(())
109 }
110
111 #[inline(never)]
113 pub fn load_checked(
114 account: &AccountInfo,
115 program_id: &Pubkey,
116 ) -> Result<Self, crate::ProgramError> {
117 if account.owner != program_id {
118 msg!(
119 "CompressibleConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.",
120 program_id,
121 account.owner
122 );
123 return Err(LightSdkError::ConstraintViolation.into());
124 }
125 let data = account.try_borrow_data()?;
126 let config = Self::try_from_slice(&data).map_err(|err| {
127 msg!(
128 "CompressibleConfig::load_checked failed: Failed to deserialize config data: {:?}",
129 err
130 );
131 LightSdkError::Borsh
132 })?;
133 config.validate()?;
134
135 let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump);
137 if expected_pda != *account.key {
138 msg!(
139 "CompressibleConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.",
140 expected_pda,
141 account.key
142 );
143 return Err(LightSdkError::ConstraintViolation.into());
144 }
145
146 Ok(config)
147 }
148}
149
150#[allow(clippy::too_many_arguments)]
179pub fn process_initialize_compression_config_account_info<'info>(
180 config_account: &AccountInfo<'info>,
181 update_authority: &AccountInfo<'info>,
182 rent_sponsor: &Pubkey,
183 compression_authority: &Pubkey,
184 rent_config: RentConfig,
185 write_top_up: u32,
186 address_space: Vec<Pubkey>,
187 config_bump: u8,
188 payer: &AccountInfo<'info>,
189 system_program: &AccountInfo<'info>,
190 program_id: &Pubkey,
191) -> Result<(), crate::ProgramError> {
192 if config_bump != 0 {
194 msg!("Config bump must be 0 for now, found: {}", config_bump);
195 return Err(LightSdkError::ConstraintViolation.into());
196 }
197
198 if config_account.data_len() > 0 {
200 msg!("Config account already initialized");
201 return Err(LightSdkError::ConstraintViolation.into());
202 }
203
204 if address_space.len() != 1 {
206 msg!(
207 "Address space must contain exactly 1 pubkey, found: {}",
208 address_space.len()
209 );
210 return Err(LightSdkError::ConstraintViolation.into());
211 }
212
213 validate_address_space_no_duplicates(&address_space)?;
215
216 if !update_authority.is_signer {
218 msg!("Update authority must be signer for initial config creation");
219 return Err(LightSdkError::ConstraintViolation.into());
220 }
221
222 let (derived_pda, bump) = CompressibleConfig::derive_pda(program_id, config_bump);
224 if derived_pda != *config_account.key {
225 msg!("Invalid config PDA");
226 return Err(LightSdkError::ConstraintViolation.into());
227 }
228
229 let rent = Rent::get().map_err(LightSdkError::from)?;
230 let account_size = CompressibleConfig::size_for_address_space(address_space.len());
231 let rent_lamports = rent.minimum_balance(account_size);
232
233 let config_bump_bytes = (config_bump as u16).to_le_bytes();
235 let seeds = &[
236 COMPRESSIBLE_CONFIG_SEED,
237 config_bump_bytes.as_ref(),
238 &[bump],
239 ];
240 let create_account_ix = system_instruction::create_account(
241 payer.key,
242 config_account.key,
243 rent_lamports,
244 account_size as u64,
245 program_id,
246 );
247
248 invoke_signed(
249 &create_account_ix,
250 &[
251 payer.clone(),
252 config_account.clone(),
253 system_program.clone(),
254 ],
255 &[seeds],
256 )
257 .map_err(LightSdkError::from)?;
258
259 let config = CompressibleConfig {
260 version: 1,
261 write_top_up,
262 update_authority: *update_authority.key,
263 rent_sponsor: *rent_sponsor,
264 compression_authority: *compression_authority,
265 rent_config,
266 config_bump,
267 address_space,
268 bump,
269 };
270
271 let mut data = config_account
272 .try_borrow_mut_data()
273 .map_err(LightSdkError::from)?;
274 config
275 .serialize(&mut &mut data[..])
276 .map_err(|_| LightSdkError::Borsh)?;
277
278 Ok(())
279}
280
281#[allow(clippy::too_many_arguments)]
298pub fn process_update_compression_config<'info>(
299 config_account: &AccountInfo<'info>,
300 authority: &AccountInfo<'info>,
301 new_update_authority: Option<&Pubkey>,
302 new_rent_sponsor: Option<&Pubkey>,
303 new_compression_authority: Option<&Pubkey>,
304 new_rent_config: Option<RentConfig>,
305 new_write_top_up: Option<u32>,
306 new_address_space: Option<Vec<Pubkey>>,
307 owner_program_id: &Pubkey,
308) -> Result<(), crate::ProgramError> {
309 let mut config = CompressibleConfig::load_checked(config_account, owner_program_id)?;
311
312 if !authority.is_signer {
314 msg!("Update authority must be signer");
315 return Err(LightSdkError::ConstraintViolation.into());
316 }
317 if *authority.key != config.update_authority {
319 msg!("Invalid update authority");
320 return Err(LightSdkError::ConstraintViolation.into());
321 }
322
323 if let Some(new_authority) = new_update_authority {
324 config.update_authority = *new_authority;
325 }
326 if let Some(new_recipient) = new_rent_sponsor {
327 config.rent_sponsor = *new_recipient;
328 }
329 if let Some(new_auth) = new_compression_authority {
330 config.compression_authority = *new_auth;
331 }
332 if let Some(new_rcfg) = new_rent_config {
333 config.rent_config = new_rcfg;
334 }
335 if let Some(new_top_up) = new_write_top_up {
336 config.write_top_up = new_top_up;
337 }
338 if let Some(new_address_space) = new_address_space {
339 if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE {
341 msg!(
342 "New address space must contain exactly 1 pubkey, found: {}",
343 new_address_space.len()
344 );
345 return Err(LightSdkError::ConstraintViolation.into());
346 }
347
348 validate_address_space_no_duplicates(&new_address_space)?;
349
350 validate_address_space_only_adds(&config.address_space, &new_address_space)?;
351
352 config.address_space = new_address_space;
353 }
354
355 let mut data = config_account.try_borrow_mut_data().map_err(|e| {
356 msg!("Failed to borrow mut data for config_account: {:?}", e);
357 LightSdkError::from(e)
358 })?;
359 config.serialize(&mut &mut data[..]).map_err(|e| {
360 msg!("Failed to serialize updated config: {:?}", e);
361 LightSdkError::Borsh
362 })?;
363
364 Ok(())
365}
366
367pub fn check_program_upgrade_authority(
378 program_id: &Pubkey,
379 program_data_account: &AccountInfo,
380 authority: &AccountInfo,
381) -> Result<(), crate::ProgramError> {
382 let (expected_program_data, _) =
384 Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID);
385 if program_data_account.key != &expected_program_data {
386 msg!("Invalid program data account");
387 return Err(LightSdkError::ConstraintViolation.into());
388 }
389
390 let data = program_data_account.try_borrow_data()?;
391 let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| {
392 msg!("Failed to deserialize program data account");
393 LightSdkError::ConstraintViolation
394 })?;
395
396 let upgrade_authority = match program_state {
398 UpgradeableLoaderState::ProgramData {
399 slot: _,
400 upgrade_authority_address,
401 } => {
402 match upgrade_authority_address {
403 Some(auth) => {
404 if auth == Pubkey::default() {
406 msg!("Invalid state: authority is zero pubkey");
407 return Err(LightSdkError::ConstraintViolation.into());
408 }
409 auth
410 }
411 None => {
412 msg!("Program has no upgrade authority");
413 return Err(LightSdkError::ConstraintViolation.into());
414 }
415 }
416 }
417 _ => {
418 msg!("Account is not ProgramData, found: {:?}", program_state);
419 return Err(LightSdkError::ConstraintViolation.into());
420 }
421 };
422
423 if !authority.is_signer {
425 msg!("Authority must be signer");
426 return Err(LightSdkError::ConstraintViolation.into());
427 }
428
429 if *authority.key != upgrade_authority {
431 msg!(
432 "Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}",
433 authority.key,
434 upgrade_authority
435 );
436 return Err(LightSdkError::ConstraintViolation.into());
437 }
438
439 Ok(())
440}
441
442#[allow(clippy::too_many_arguments)]
463pub fn process_initialize_compression_config_checked<'info>(
464 config_account: &AccountInfo<'info>,
465 update_authority: &AccountInfo<'info>,
466 program_data_account: &AccountInfo<'info>,
467 rent_sponsor: &Pubkey,
468 compression_authority: &Pubkey,
469 rent_config: RentConfig,
470 write_top_up: u32,
471 address_space: Vec<Pubkey>,
472 config_bump: u8,
473 payer: &AccountInfo<'info>,
474 system_program: &AccountInfo<'info>,
475 program_id: &Pubkey,
476) -> Result<(), crate::ProgramError> {
477 msg!(
478 "create_compression_config_checked program_data_account: {:?}",
479 program_data_account.key
480 );
481 msg!(
482 "create_compression_config_checked program_id: {:?}",
483 program_id
484 );
485 check_program_upgrade_authority(program_id, program_data_account, update_authority)?;
487
488 process_initialize_compression_config_account_info(
490 config_account,
491 update_authority,
492 rent_sponsor,
493 compression_authority,
494 rent_config,
495 write_top_up,
496 address_space,
497 config_bump,
498 payer,
499 system_program,
500 program_id,
501 )
502}
503
504fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
506 let mut seen = HashSet::new();
507 for pubkey in address_space {
508 if !seen.insert(pubkey) {
509 msg!("Duplicate pubkey found in address_space: {}", pubkey);
510 return Err(LightSdkError::ConstraintViolation);
511 }
512 }
513 Ok(())
514}
515
516fn validate_address_space_only_adds(
518 existing_address_space: &[Pubkey],
519 new_address_space: &[Pubkey],
520) -> Result<(), LightSdkError> {
521 for existing_pubkey in existing_address_space {
523 if !new_address_space.contains(existing_pubkey) {
524 msg!(
525 "Cannot remove existing pubkey from address_space: {}",
526 existing_pubkey
527 );
528 return Err(LightSdkError::ConstraintViolation);
529 }
530 }
531 Ok(())
532}