use std::collections::HashSet;
use light_compressible::rent::RentConfig;
use solana_account_info::AccountInfo;
use solana_cpi::invoke_signed;
use solana_loader_v3_interface::state::UpgradeableLoaderState;
use solana_msg::msg;
use solana_pubkey::Pubkey;
use solana_system_interface::instruction as system_instruction;
use solana_sysvar::{rent::Rent, Sysvar};
use crate::{error::LightSdkError, AnchorDeserialize, AnchorSerialize};
pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config";
pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1;
const BPF_LOADER_UPGRADEABLE_ID: Pubkey =
Pubkey::from_str_const("BPFLoaderUpgradeab1e11111111111111111111111");
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct LightConfig {
pub version: u8,
pub write_top_up: u32,
pub update_authority: Pubkey,
pub rent_sponsor: Pubkey,
pub compression_authority: Pubkey,
pub rent_config: RentConfig,
pub config_bump: u8,
pub bump: u8,
pub address_space: Vec<Pubkey>,
}
impl LightConfig {
pub const LEN: usize = 1
+ 4
+ 32
+ 32
+ 32
+ core::mem::size_of::<RentConfig>()
+ 1
+ 1
+ 4
+ (32 * MAX_ADDRESS_TREES_PER_SPACE);
pub fn size_for_address_space(num_address_trees: usize) -> usize {
1 + 4
+ 32
+ 32
+ 32
+ core::mem::size_of::<RentConfig>()
+ 1
+ 1
+ 4
+ (32 * num_address_trees)
}
pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) {
let config_bump_u16 = config_bump as u16;
Pubkey::find_program_address(
&[COMPRESSIBLE_CONFIG_SEED, &config_bump_u16.to_le_bytes()],
program_id,
)
}
pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Self::derive_pda(program_id, 0)
}
pub fn validate(&self) -> Result<(), crate::ProgramError> {
if self.version != 1 {
msg!(
"LightConfig validation failed: Unsupported config version: {}",
self.version
);
return Err(LightSdkError::ConstraintViolation.into());
}
if self.address_space.len() != 1 {
msg!(
"LightConfig validation failed: Address space must contain exactly 1 pubkey, found: {}",
self.address_space.len()
);
return Err(LightSdkError::ConstraintViolation.into());
}
if self.config_bump != 0 {
msg!(
"LightConfig validation failed: Config bump must be 0 for now, found: {}",
self.config_bump
);
return Err(LightSdkError::ConstraintViolation.into());
}
Ok(())
}
#[inline(never)]
pub fn load_checked(
account: &AccountInfo,
program_id: &Pubkey,
) -> Result<Self, crate::ProgramError> {
if account.owner != program_id {
msg!(
"LightConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.",
program_id,
account.owner
);
return Err(LightSdkError::ConstraintViolation.into());
}
let data = account.try_borrow_data()?;
let config = Self::try_from_slice(&data).map_err(|err| {
msg!(
"LightConfig::load_checked failed: Failed to deserialize config data: {:?}",
err
);
LightSdkError::Borsh
})?;
config.validate()?;
let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump);
if expected_pda != *account.key {
msg!(
"LightConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.",
expected_pda,
account.key
);
return Err(LightSdkError::ConstraintViolation.into());
}
Ok(config)
}
}
#[allow(clippy::too_many_arguments)]
pub fn process_initialize_light_config<'info>(
config_account: &AccountInfo<'info>,
update_authority: &AccountInfo<'info>,
rent_sponsor: &Pubkey,
compression_authority: &Pubkey,
rent_config: RentConfig,
write_top_up: u32,
address_space: Vec<Pubkey>,
config_bump: u8,
payer: &AccountInfo<'info>,
system_program: &AccountInfo<'info>,
program_id: &Pubkey,
) -> Result<(), crate::ProgramError> {
if config_bump != 0 {
msg!("Config bump must be 0 for now, found: {}", config_bump);
return Err(LightSdkError::ConstraintViolation.into());
}
if config_account.data_len() > 0 {
msg!("Config account already initialized");
return Err(LightSdkError::ConstraintViolation.into());
}
if address_space.len() != 1 {
msg!(
"Address space must contain exactly 1 pubkey, found: {}",
address_space.len()
);
return Err(LightSdkError::ConstraintViolation.into());
}
validate_address_space_no_duplicates(&address_space)?;
if !update_authority.is_signer {
msg!("Update authority must be signer for initial config creation");
return Err(LightSdkError::ConstraintViolation.into());
}
let (derived_pda, bump) = LightConfig::derive_pda(program_id, config_bump);
if derived_pda != *config_account.key {
msg!("Invalid config PDA");
return Err(LightSdkError::ConstraintViolation.into());
}
let rent = Rent::get().map_err(LightSdkError::from)?;
let account_size = LightConfig::size_for_address_space(address_space.len());
let rent_lamports = rent.minimum_balance(account_size);
let config_bump_bytes = (config_bump as u16).to_le_bytes();
let seeds = &[
COMPRESSIBLE_CONFIG_SEED,
config_bump_bytes.as_ref(),
&[bump],
];
let create_account_ix = system_instruction::create_account(
payer.key,
config_account.key,
rent_lamports,
account_size as u64,
program_id,
);
invoke_signed(
&create_account_ix,
&[
payer.clone(),
config_account.clone(),
system_program.clone(),
],
&[seeds],
)
.map_err(LightSdkError::from)?;
let config = LightConfig {
version: 1,
write_top_up,
update_authority: *update_authority.key,
rent_sponsor: *rent_sponsor,
compression_authority: *compression_authority,
rent_config,
config_bump,
address_space,
bump,
};
let mut data = config_account
.try_borrow_mut_data()
.map_err(LightSdkError::from)?;
config
.serialize(&mut &mut data[..])
.map_err(|_| LightSdkError::Borsh)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn process_update_light_config<'info>(
config_account: &AccountInfo<'info>,
authority: &AccountInfo<'info>,
new_update_authority: Option<&Pubkey>,
new_rent_sponsor: Option<&Pubkey>,
new_compression_authority: Option<&Pubkey>,
new_rent_config: Option<RentConfig>,
new_write_top_up: Option<u32>,
new_address_space: Option<Vec<Pubkey>>,
owner_program_id: &Pubkey,
) -> Result<(), crate::ProgramError> {
let mut config = LightConfig::load_checked(config_account, owner_program_id)?;
if !authority.is_signer {
msg!("Update authority must be signer");
return Err(LightSdkError::ConstraintViolation.into());
}
if *authority.key != config.update_authority {
msg!("Invalid update authority");
return Err(LightSdkError::ConstraintViolation.into());
}
if let Some(new_authority) = new_update_authority {
config.update_authority = *new_authority;
}
if let Some(new_recipient) = new_rent_sponsor {
config.rent_sponsor = *new_recipient;
}
if let Some(new_auth) = new_compression_authority {
config.compression_authority = *new_auth;
}
if let Some(new_rcfg) = new_rent_config {
config.rent_config = new_rcfg;
}
if let Some(new_top_up) = new_write_top_up {
config.write_top_up = new_top_up;
}
if let Some(new_address_space) = new_address_space {
if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE {
msg!(
"New address space must contain exactly 1 pubkey, found: {}",
new_address_space.len()
);
return Err(LightSdkError::ConstraintViolation.into());
}
validate_address_space_no_duplicates(&new_address_space)?;
validate_address_space_only_adds(&config.address_space, &new_address_space)?;
config.address_space = new_address_space;
}
let mut data = config_account.try_borrow_mut_data().map_err(|e| {
msg!("Failed to borrow mut data for config_account: {:?}", e);
LightSdkError::from(e)
})?;
config.serialize(&mut &mut data[..]).map_err(|e| {
msg!("Failed to serialize updated config: {:?}", e);
LightSdkError::Borsh
})?;
Ok(())
}
pub fn check_program_upgrade_authority(
program_id: &Pubkey,
program_data_account: &AccountInfo,
authority: &AccountInfo,
) -> Result<(), crate::ProgramError> {
let (expected_program_data, _) =
Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID);
if program_data_account.key != &expected_program_data {
msg!("Invalid program data account");
return Err(LightSdkError::ConstraintViolation.into());
}
let data = program_data_account.try_borrow_data()?;
let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| {
msg!("Failed to deserialize program data account");
LightSdkError::ConstraintViolation
})?;
let upgrade_authority = match program_state {
UpgradeableLoaderState::ProgramData {
slot: _,
upgrade_authority_address,
} => {
match upgrade_authority_address {
Some(auth) => {
if auth == Pubkey::default() {
msg!("Invalid state: authority is zero pubkey");
return Err(LightSdkError::ConstraintViolation.into());
}
auth
}
None => {
msg!("Program has no upgrade authority");
return Err(LightSdkError::ConstraintViolation.into());
}
}
}
_ => {
msg!("Account is not ProgramData, found: {:?}", program_state);
return Err(LightSdkError::ConstraintViolation.into());
}
};
if !authority.is_signer {
msg!("Authority must be signer");
return Err(LightSdkError::ConstraintViolation.into());
}
if *authority.key != upgrade_authority {
msg!(
"Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}",
authority.key,
upgrade_authority
);
return Err(LightSdkError::ConstraintViolation.into());
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn process_initialize_light_config_checked<'info>(
config_account: &AccountInfo<'info>,
update_authority: &AccountInfo<'info>,
program_data_account: &AccountInfo<'info>,
rent_sponsor: &Pubkey,
compression_authority: &Pubkey,
rent_config: RentConfig,
write_top_up: u32,
address_space: Vec<Pubkey>,
config_bump: u8,
payer: &AccountInfo<'info>,
system_program: &AccountInfo<'info>,
program_id: &Pubkey,
) -> Result<(), crate::ProgramError> {
msg!(
"create_compression_config_checked program_data_account: {:?}",
program_data_account.key
);
msg!(
"create_compression_config_checked program_id: {:?}",
program_id
);
check_program_upgrade_authority(program_id, program_data_account, update_authority)?;
process_initialize_light_config(
config_account,
update_authority,
rent_sponsor,
compression_authority,
rent_config,
write_top_up,
address_space,
config_bump,
payer,
system_program,
program_id,
)
}
fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
let mut seen = HashSet::new();
for pubkey in address_space {
if !seen.insert(pubkey) {
msg!("Duplicate pubkey found in address_space: {}", pubkey);
return Err(LightSdkError::ConstraintViolation);
}
}
Ok(())
}
fn validate_address_space_only_adds(
existing_address_space: &[Pubkey],
new_address_space: &[Pubkey],
) -> Result<(), LightSdkError> {
for existing_pubkey in existing_address_space {
if !new_address_space.contains(existing_pubkey) {
msg!(
"Cannot remove existing pubkey from address_space: {}",
existing_pubkey
);
return Err(LightSdkError::ConstraintViolation);
}
}
Ok(())
}