light_sdk/compressible/
config.rs

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// TODO: add rent_authority + rent_func like in ctoken.
20/// Global configuration for compressible accounts
21#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
22pub struct CompressibleConfig {
23    /// Config version for future upgrades
24    pub version: u8,
25    /// Lamports to top up on each write (heuristic)
26    pub write_top_up: u32,
27    /// Authority that can update the config
28    pub update_authority: Pubkey,
29    /// Account that receives rent from compressed PDAs
30    pub rent_sponsor: Pubkey,
31    /// Authority that can compress/close PDAs (distinct from rent_sponsor)
32    pub compression_authority: Pubkey,
33    /// Rent function parameters for compressibility and distribution
34    pub rent_config: RentConfig,
35    /// Config bump seed (0)
36    pub config_bump: u8,
37    /// PDA bump seed
38    pub bump: u8,
39    /// Address space for compressed accounts (currently 1 address_tree allowed)
40    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    /// Calculate the exact size needed for a CompressibleConfig with the given
56    /// number of address spaces
57    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    /// Derives the config PDA address with config bump
70    pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) {
71        // Convert u8 to u16 to match program-libs derivation (uses u16 with to_le_bytes)
72        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    /// Derives the default config PDA address (config_bump = 0)
80    pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) {
81        Self::derive_pda(program_id, 0)
82    }
83
84    /// Checks the config account
85    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        // For now, only allow config_bump = 0 to keep it simple
101        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    /// Loads and validates config from account, checking owner and PDA derivation
112    #[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        // CHECK: PDA derivation
136        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/// Creates a new compressible config PDA
151///
152/// # Security - Solana Best Practice
153/// This function follows the standard Solana pattern where only the program's
154/// upgrade authority can create the initial config. This prevents unauthorized
155/// parties from hijacking the config system.
156///
157/// # Arguments
158/// * `config_account` - The config PDA account to initialize
159/// * `update_authority` - Authority that can update the config after creation
160/// * `rent_sponsor` - Account that receives rent from compressed PDAs
161/// * `compression_authority` - Authority that can compress/close PDAs
162/// * `rent_config` - Rent function parameters
163/// * `write_top_up` - Lamports to top up on each write
164/// * `address_space` - Address space for compressed accounts (currently 1 address_tree allowed)
165/// * `config_bump` - Config bump seed (must be 0 for now)
166/// * `payer` - Account paying for the PDA creation
167/// * `system_program` - System program
168/// * `program_id` - The program that owns the config
169///
170/// # Required Validation (must be done by caller)
171/// The caller MUST validate that the signer is the program's upgrade authority
172/// by checking against the program data account. This cannot be done in the SDK
173/// due to dependency constraints.
174///
175/// # Returns
176/// * `Ok(())` if config was created successfully
177/// * `Err(ProgramError)` if there was an error
178#[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    // CHECK: only 1 address_space
193    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    // CHECK: not already initialized
199    if config_account.data_len() > 0 {
200        msg!("Config account already initialized");
201        return Err(LightSdkError::ConstraintViolation.into());
202    }
203
204    // CHECK: only 1 address_space
205    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    // CHECK: unique pubkeys in address_space
214    validate_address_space_no_duplicates(&address_space)?;
215
216    // CHECK: signer
217    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    // CHECK: pda derivation
223    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    // Use u16 to_le_bytes to match derive_pda (2 bytes instead of 1)
234    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/// Updates an existing compressible config
282///
283/// # Arguments
284/// * `config_account` - The config PDA account to update
285/// * `authority` - Current update authority (must match config)
286/// * `new_update_authority` - Optional new update authority
287/// * `new_rent_sponsor` - Optional new rent recipient
288/// * `new_compression_authority` - Optional new compression authority
289/// * `new_rent_config` - Optional new rent function parameters
290/// * `new_write_top_up` - Optional new write top-up amount
291/// * `new_address_space` - Optional new address space (currently 1 address_tree allowed)
292/// * `owner_program_id` - The program that owns the config
293///
294/// # Returns
295/// * `Ok(())` if config was updated successfully
296/// * `Err(ProgramError)` if there was an error
297#[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    // CHECK: PDA derivation
310    let mut config = CompressibleConfig::load_checked(config_account, owner_program_id)?;
311
312    // CHECK: signer
313    if !authority.is_signer {
314        msg!("Update authority must be signer");
315        return Err(LightSdkError::ConstraintViolation.into());
316    }
317    // CHECK: authority
318    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        // CHECK: address space length
340        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
367/// Checks that the signer is the program's upgrade authority
368///
369/// # Arguments
370/// * `program_id` - The program to check
371/// * `program_data_account` - The program's data account (ProgramData)
372/// * `authority` - The authority to verify
373///
374/// # Returns
375/// * `Ok(())` if authority is valid
376/// * `Err(LightSdkError)` if authority is invalid or verification fails
377pub fn check_program_upgrade_authority(
378    program_id: &Pubkey,
379    program_data_account: &AccountInfo,
380    authority: &AccountInfo,
381) -> Result<(), crate::ProgramError> {
382    // CHECK: program data PDA
383    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    // Extract upgrade authority
397    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                    // Check for invalid zero authority when authority exists
405                    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    // CHECK: upgrade authority is signer
424    if !authority.is_signer {
425        msg!("Authority must be signer");
426        return Err(LightSdkError::ConstraintViolation.into());
427    }
428
429    // CHECK: upgrade authority is program's upgrade authority
430    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/// Creates a new compressible config PDA.
443///
444/// # Arguments
445/// * `config_account` - The config PDA account to initialize
446/// * `update_authority` - Must be the program's upgrade authority
447/// * `program_data_account` - The program's data account for validation
448/// * `rent_sponsor` - Account that receives rent from compressed PDAs
449/// * `compression_authority` - Authority that can compress/close PDAs
450/// * `rent_config` - Rent function parameters
451/// * `write_top_up` - Lamports to top up on each write
452/// * `address_space` - Address spaces for compressed accounts (exactly 1
453///   allowed)
454/// * `config_bump` - Config bump seed (must be 0 for now)
455/// * `payer` - Account paying for the PDA creation
456/// * `system_program` - System program
457/// * `program_id` - The program that owns the config
458///
459/// # Returns
460/// * `Ok(())` if config was created successfully
461/// * `Err(ProgramError)` if there was an error or authority validation fails
462#[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    // Verify the signer is the program's upgrade authority
486    check_program_upgrade_authority(program_id, program_data_account, update_authority)?;
487
488    // Create the config with validated authority
489    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
504/// Validates that address_space contains no duplicate pubkeys
505fn 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
516/// Validates that new_address_space only adds to existing address_space (no removals)
517fn validate_address_space_only_adds(
518    existing_address_space: &[Pubkey],
519    new_address_space: &[Pubkey],
520) -> Result<(), LightSdkError> {
521    // Check that all existing pubkeys are still present in new address space
522    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}