use solana_program::{ account_info::{next_account_info, AccountInfo}, clock::Clock, entrypoint, entrypoint::ProgramResult, hash::hashv, instruction::Instruction, program::{get_return_data, invoke, invoke_signed}, program_error::ProgramError, program_memory::sol_memcmp, pubkey::Pubkey, rent::Rent, system_instruction, system_program, sysvar::{instructions::{load_current_index_checked, load_instruction_at_checked}, Sysvar}, }; use std::{convert::TryFrom, str::FromStr}; pub mod settings; solana_program::declare_id!("SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6"); entrypoint!(process_instruction); const MAGIC: &[u8; 5] = b"SHiNE"; const FORMAT_MAJOR: u8 = 1; const FORMAT_MINOR: u8 = 2; const LEGACY_FORMAT_MINOR: u8 = 0; const MAX_AUTO_REALLOC_INCREASE: usize = 10_000; const ZERO_HASH: [u8; 32] = [0; 32]; const BLOCK_TYPE_ROOT_KEY: u8 = 1; const BLOCK_TYPE_CLIENT_KEY: u8 = 2; const BLOCK_TYPE_BLOCKCHAIN_REGISTRY: u8 = 3; const BLOCK_TYPE_SERVER_PROFILE: u8 = 30; const BLOCK_TYPE_ACCESS_SERVERS: u8 = 40; const BLOCK_VERSION_0: u8 = 0; const AUTH_MODE_BLOCKCHAIN: u8 = 0; const AUTH_MODE_ROOT: u8 = 1; const DAY_MS: u64 = 86_400_000; const FORK_COOLDOWN_MS: u64 = 3 * DAY_MS; const MAX_CLIENT_CLOCK_SKEW_MS: u64 = 5 * 60 * 1000; const IX_INIT_USERS_ECONOMY_CONFIG: u8 = 1; const IX_UPDATE_USERS_ECONOMY_CONFIG: u8 = 2; const IX_CREATE_USER_PDA: u8 = 3; const IX_UPDATE_USER_PDA: u8 = 4; const IX_UPSERT_PROMO_SELLER: u8 = 5; const IX_CLOSE_LEGACY_USER_PDA: u8 = 6; const LOGIN_GUARD_IX_CLASSIFY_LOGIN: u8 = 1; #[repr(u32)] #[derive(Clone, Copy, Debug)] enum ShineUsersError { InvalidInstruction = 1, InvalidSigner = 2, InvalidPdaAddress = 3, UserAlreadyExists = 4, EmptyPdaData = 5, InvalidRecordData = 6, InvalidRecordMagic = 7, InvalidRecordFormat = 8, InvalidRecordLength = 9, InvalidLogin = 10, InvalidLimitIncrement = 11, InvalidFeeReceiver = 12, InvalidLoginGuardResponse = 13, PremiumLogin = 14, TrademarkLoginRequiresReview = 15, InvalidVersion = 16, InvalidPrevHash = 17, ImmutableFieldChanged = 18, BalanceDecrease = 19, InvalidSignature = 20, RecordTooLarge = 21, MathOverflow = 22, SystemAlreadyInitialized = 23, MissingRequiredSignature = 24, InvalidSystemProgram = 25, InvalidAccountOwner = 26, InvalidAccountData = 27, InvalidPromoCode = 28, PromoSellerNotFound = 29, PromoSalesExhausted = 30, PromoLoginTooShort = 31, InvalidPromoSellerState = 32, PromoSignatureMismatch = 33, ForkCooldown = 34, DuplicateBlockchainKey = 35, PaidLimitTooLarge = 36, CannotCloseCurrentFormat = 37, RootChangeRequiresRoot = 38, AmbiguousAuthorityRotation = 39, } impl From for ProgramError { fn from(value: ShineUsersError) -> Self { ProgramError::Custom(value as u32) } } macro_rules! require { ($cond:expr, $err:expr) => { if !($cond) { return Err(ProgramError::from($err)); } }; } macro_rules! require_keys_eq { ($left:expr, $right:expr, $err:expr) => { if $left != $right { return Err(ProgramError::from($err)); } }; } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ForkRecordV12 { pub blockchain_key: Pubkey, pub created_at_ms: u64, pub paid_limit_bytes: u32, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ServerAddressRecordV12 { pub address_format_type: u8, pub address_format_version: u8, pub address: String, } #[derive(Clone, Debug)] pub struct UserRecordV12 { pub created_at_ms: u64, pub updated_at_ms: u64, pub record_number: u32, pub prev_record_hash: [u8; 32], pub login: String, pub root_key: Pubkey, pub client_key: Pubkey, pub forks: Vec, pub server_addresses: Vec, pub access_servers: Vec, pub signature: [u8; 64], } impl UserRecordV12 { fn active_fork(&self) -> Result<&ForkRecordV12, ProgramError> { self.forks.last().ok_or(ProgramError::from(ShineUsersError::InvalidRecordData)) } } #[derive(Clone, Debug)] pub struct CreateUserPdaArgs { pub login: String, pub root_key: Pubkey, pub created_at_ms: u64, pub additional_limit: u64, pub client_key: Pubkey, pub blockchain_key: Pubkey, pub server_addresses: Vec, pub access_servers: Vec, pub signature: [u8; 64], pub promo_seller_login: Option, } #[derive(Clone, Debug)] pub struct UpdateUserPdaArgs { pub login: String, pub root_key: Pubkey, pub updated_at_ms: u64, pub additional_limit: u64, pub client_key: Pubkey, pub auth_mode: u8, pub new_blockchain_key: Option, pub server_addresses: Vec, pub access_servers: Vec, pub signature: [u8; 64], } #[derive(Clone, Debug)] pub struct CloseLegacyUserPdaArgs { pub login: String, } #[derive(Clone, Debug)] pub struct UpdateUsersEconomyConfigArgs { pub registration_fee_lamports: u64, pub lamports_per_limit_step: u64, pub start_bonus_limit: u64, } #[derive(Clone, Debug)] pub struct UsersEconomyConfigState { pub version: u8, pub registration_fee_lamports: u64, pub lamports_per_limit_step: u64, pub start_bonus_limit: u64, } #[derive(Clone, Debug)] pub struct UpsertPromoSellerArgs { pub seller_login: String, pub remaining_sales: u64, pub min_login_length: u8, pub signer_pubkey: Pubkey, } #[derive(Clone, Debug)] pub struct PromoSellerState { pub version: u8, pub remaining_sales: u64, pub min_login_length: u8, pub signer_pubkey: Pubkey, } struct Reader<'a> { data: &'a [u8], cursor: usize, } impl<'a> Reader<'a> { fn new(data: &'a [u8]) -> Self { Self { data, cursor: 0 } } fn read_u8(&mut self) -> Result { let v = *self.data.get(self.cursor).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor += 1; Ok(v) } fn read_u16(&mut self) -> Result { let end = self.cursor.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; Ok(u16::from_le_bytes([s[0], s[1]])) } fn read_u32(&mut self) -> Result { let end = self.cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) } fn read_u64(&mut self) -> Result { let end = self.cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) } fn read_fixed_32(&mut self) -> Result<[u8; 32], ProgramError> { let end = self.cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) } fn read_fixed_64(&mut self) -> Result<[u8; 64], ProgramError> { let end = self.cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) } fn read_pubkey(&mut self) -> Result { Ok(Pubkey::new_from_array(self.read_fixed_32()?)) } fn read_string_u8(&mut self) -> Result { let len = self.read_u8()? as usize; let end = self.cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; self.cursor = end; std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction)) } fn remaining(&self) -> usize { self.data.len().saturating_sub(self.cursor) } fn finish(self) -> Result<(), ProgramError> { require!(self.cursor == self.data.len(), ShineUsersError::InvalidInstruction); Ok(()) } } fn process_instruction<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], instruction_data: &[u8]) -> ProgramResult { let mut r = Reader::new(instruction_data); let tag = r.read_u8()?; match tag { IX_INIT_USERS_ECONOMY_CONFIG => { r.finish()?; process_init_users_economy_config(program_id, accounts) } IX_UPDATE_USERS_ECONOMY_CONFIG => { let args = UpdateUsersEconomyConfigArgs { registration_fee_lamports: r.read_u64()?, lamports_per_limit_step: r.read_u64()?, start_bonus_limit: r.read_u64()?, }; r.finish()?; process_update_users_economy_config(program_id, accounts, args) } IX_CREATE_USER_PDA => dispatch_create_user_pda(program_id, accounts, r), IX_UPDATE_USER_PDA => dispatch_update_user_pda(program_id, accounts, r), IX_UPSERT_PROMO_SELLER => dispatch_upsert_promo_seller(program_id, accounts, r), IX_CLOSE_LEGACY_USER_PDA => dispatch_close_legacy_user_pda(program_id, accounts, r), _ => Err(ProgramError::from(ShineUsersError::InvalidInstruction)), } } fn dispatch_create_user_pda<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], mut r: Reader<'_>) -> ProgramResult { let args = Box::new(parse_create_args(&mut r)?); r.finish()?; process_create_user_pda(program_id, accounts, args) } fn dispatch_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult { let args = Box::new(parse_update_args(&mut r)?); r.finish()?; process_update_user_pda(program_id, accounts, args) } fn dispatch_upsert_promo_seller(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult { let args = UpsertPromoSellerArgs { seller_login: r.read_string_u8()?, remaining_sales: r.read_u64()?, min_login_length: r.read_u8()?, signer_pubkey: r.read_pubkey()?, }; r.finish()?; process_upsert_promo_seller(program_id, accounts, args) } fn parse_server_addresses(r: &mut Reader<'_>) -> Result, ProgramError> { let count = r.read_u8()? as usize; let mut addresses = Vec::with_capacity(count); for _ in 0..count { addresses.push(ServerAddressRecordV12 { address_format_type: r.read_u8()?, address_format_version: r.read_u8()?, address: r.read_string_u8()?, }); } Ok(addresses) } fn parse_access_servers(r: &mut Reader<'_>) -> Result, ProgramError> { let count = r.read_u8()? as usize; let mut servers = Vec::with_capacity(count); for _ in 0..count { servers.push(r.read_string_u8()?); } Ok(servers) } fn parse_create_args(r: &mut Reader<'_>) -> Result { Ok(CreateUserPdaArgs { login: r.read_string_u8()?, root_key: r.read_pubkey()?, created_at_ms: r.read_u64()?, additional_limit: r.read_u64()?, client_key: r.read_pubkey()?, blockchain_key: r.read_pubkey()?, server_addresses: parse_server_addresses(r)?, access_servers: parse_access_servers(r)?, signature: r.read_fixed_64()?, promo_seller_login: if r.remaining() > 0 { Some(r.read_string_u8()?) } else { None }, }) } fn parse_update_args(r: &mut Reader<'_>) -> Result { let login = r.read_string_u8()?; let root_key = r.read_pubkey()?; let updated_at_ms = r.read_u64()?; let additional_limit = r.read_u64()?; let client_key = r.read_pubkey()?; let auth_mode = r.read_u8()?; let new_blockchain_key = match r.read_u8()? { 0 => None, 1 => Some(r.read_pubkey()?), _ => return Err(ProgramError::from(ShineUsersError::InvalidInstruction)), }; let server_addresses = parse_server_addresses(r)?; let access_servers = parse_access_servers(r)?; let signature = r.read_fixed_64()?; Ok(UpdateUserPdaArgs { login, root_key, updated_at_ms, additional_limit, client_key, auth_mode, new_blockchain_key, server_addresses, access_servers, signature, }) } fn dispatch_close_legacy_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], mut r: Reader<'_>) -> ProgramResult { let args = CloseLegacyUserPdaArgs { login: r.read_string_u8()? }; r.finish()?; process_close_legacy_user_pda(program_id, accounts, args) } fn process_init_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; let system_program_ai = next_account_info(&mut it)?; require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); let (expected_pda, bump) = find_users_economy_config_pda(program_id); require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress); require!(users_economy_config_pda.owner == &system_program::id(), ShineUsersError::SystemAlreadyInitialized); require!(users_economy_config_pda.data_is_empty(), ShineUsersError::SystemAlreadyInitialized); let state = UsersEconomyConfigState { version: 1, registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS, lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP, start_bonus_limit: settings::START_BONUS_LIMIT, }; let data = serialize_users_economy_config(&state); create_pda_account( signer, users_economy_config_pda, system_program_ai, program_id, &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]], data.len(), )?; write_pda_exact(users_economy_config_pda, &data)?; Ok(()) } fn process_update_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUsersEconomyConfigArgs) -> ProgramResult { let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?; require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner); let (expected_pda, _) = find_users_economy_config_pda(program_id); require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress); require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); require!(args.lamports_per_limit_step > 0, ShineUsersError::InvalidRecordData); let mut state = read_users_economy_config(users_economy_config_pda)?; state.registration_fee_lamports = args.registration_fee_lamports; state.lamports_per_limit_step = args.lamports_per_limit_step; state.start_bonus_limit = args.start_bonus_limit; write_pda_exact(users_economy_config_pda, &serialize_users_economy_config(&state))?; Ok(()) } fn process_upsert_promo_seller(program_id: &Pubkey, accounts: &[AccountInfo], args: UpsertPromoSellerArgs) -> ProgramResult { let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let promo_seller_pda = next_account_info(&mut it)?; let system_program_ai = next_account_info(&mut it)?; require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); validate_login(&args.seller_login)?; require!((1..=20).contains(&args.min_login_length), ShineUsersError::InvalidPromoSellerState); let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?; require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner); let seller_seed = login_seed_normalized(&args.seller_login); let (expected_pda, bump) = find_promo_seller_pda(program_id, &seller_seed); require_keys_eq!(expected_pda, *promo_seller_pda.key, ShineUsersError::InvalidPdaAddress); if promo_seller_pda.owner == &system_program::id() { create_pda_account( signer, promo_seller_pda, system_program_ai, program_id, &[settings::PROMO_SELLER_PDA_SEED_PREFIX.as_bytes(), seller_seed.as_bytes(), &[bump]], settings::PROMO_SELLER_PDA_SPACE, )?; } else { require!(promo_seller_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); ensure_pda_size_and_rent(promo_seller_pda, signer, system_program_ai, settings::PROMO_SELLER_PDA_SPACE)?; } let state = PromoSellerState { version: 1, remaining_sales: args.remaining_sales, min_login_length: args.min_login_length, signer_pubkey: args.signer_pubkey, }; write_pda_exact(promo_seller_pda, &serialize_promo_seller_state(&state))?; Ok(()) } fn process_create_user_pda<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: Box) -> ProgramResult { let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let user_pda = next_account_info(&mut it)?; let system_program_ai = next_account_info(&mut it)?; let inflow_vault = next_account_info(&mut it)?; let instructions_sysvar = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; let login_guard_program = next_account_info(&mut it)?; let promo_seller_pda = it.next(); require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); validate_login(&args.login)?; validate_server_addresses(&args.server_addresses)?; validate_access_servers(&args.access_servers)?; validate_inflow_vault(inflow_vault)?; require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement); require_keys_eq!(*login_guard_program.key, Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?, ShineUsersError::InvalidLoginGuardResponse); validate_users_economy_config_pda(program_id, users_economy_config_pda)?; let promo_context = if let Some(seller_login) = args.promo_seller_login.as_deref().filter(|value| !value.trim().is_empty()) { let seller_pda = promo_seller_pda.ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?; Some(verify_promo_registration(program_id, instructions_sysvar, seller_pda, &args.login, seller_login)?) } else { require!(promo_seller_pda.is_none(), ShineUsersError::InvalidInstruction); None }; if promo_context.is_none() { classify_login_or_fail(login_guard_program, &args.login)?; } let economy = read_users_economy_config(users_economy_config_pda)?; let login_seed = login_seed_normalized(&args.login); let (expected_pda, bump) = find_user_pda(program_id, &login_seed); require_keys_eq!(expected_pda, *user_pda.key, ShineUsersError::InvalidPdaAddress); require!(user_pda.owner == &system_program::id(), ShineUsersError::UserAlreadyExists); require!(user_pda.data_is_empty(), ShineUsersError::UserAlreadyExists); let start_balance_u64 = economy.start_bonus_limit.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; let start_balance = u32::try_from(start_balance_u64).map_err(|_| ProgramError::from(ShineUsersError::PaidLimitTooLarge))?; let now_ms = current_time_ms()?; validate_client_time_ms(args.created_at_ms, now_ms)?; let fork_created_at_ms = args.created_at_ms; let mut record = UserRecordV12 { created_at_ms: args.created_at_ms, updated_at_ms: args.created_at_ms, record_number: 0, prev_record_hash: ZERO_HASH, login: args.login, root_key: args.root_key, client_key: args.client_key, forks: vec![ForkRecordV12 { blockchain_key: args.blockchain_key, created_at_ms: fork_created_at_ms, paid_limit_bytes: start_balance, }], server_addresses: args.server_addresses, access_servers: args.access_servers, signature: [0; 64], }; let unsigned = serialize_unsigned_record_v12(&record)?; let unsigned_hash = hashv(&[&unsigned]); // На create root доказывает владение recovery-ключом, а сама новая запись // подписывается активным blockchain-key (новым authority состояния). verify_ed25519_instruction_pubkey_message(instructions_sysvar, -2, &record.root_key, unsigned_hash.as_ref())?; record.signature = verify_record_signature_hash_at(instructions_sysvar, -1, &record.active_fork()?.blockchain_key, &args.signature, unsigned_hash.as_ref())?; let serialized = pad_to_fixed_size(serialize_full_record_v12(&record)?, settings::USER_PDA_SPACE)?; create_pda_account( signer, user_pda, system_program_ai, program_id, &[settings::USER_PDA_SEED_PREFIX.as_bytes(), login_seed.as_bytes(), &[bump]], settings::USER_PDA_SPACE, )?; write_pda_exact(user_pda, &serialized)?; if let Some(promo) = promo_context { write_pda_exact(promo.promo_seller_pda, &serialize_promo_seller_state(&promo.updated_state))?; } let total_fee = economy.registration_fee_lamports.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; transfer_lamports(signer, inflow_vault, system_program_ai, total_fee)?; Ok(()) } fn process_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: Box) -> ProgramResult { let mut it = accounts.iter(); let signer = next_account_info(&mut it)?; let user_pda = next_account_info(&mut it)?; let system_program_ai = next_account_info(&mut it)?; let inflow_vault = next_account_info(&mut it)?; let instructions_sysvar = next_account_info(&mut it)?; let users_economy_config_pda = next_account_info(&mut it)?; require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(signer.is_signer, ShineUsersError::InvalidSigner); require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram); validate_login(&args.login)?; validate_server_addresses(&args.server_addresses)?; validate_access_servers(&args.access_servers)?; validate_inflow_vault(inflow_vault)?; require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement); validate_users_economy_config_pda(program_id, users_economy_config_pda)?; let economy = read_users_economy_config(users_economy_config_pda)?; let normalized_login = login_seed_normalized(&args.login); require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress); require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); let raw = read_pda_all(user_pda)?; require!(record_format_minor(&raw)? == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat); let old_record = deserialize_record_v12_from_pda(&raw)?; require!(old_record.login == args.login, ShineUsersError::ImmutableFieldChanged); require!(args.updated_at_ms >= old_record.updated_at_ms, ShineUsersError::InvalidRecordData); let now_ms = current_time_ms()?; validate_client_time_ms(args.updated_at_ms, now_ms)?; require!(args.auth_mode == AUTH_MODE_BLOCKCHAIN || args.auth_mode == AUTH_MODE_ROOT, ShineUsersError::InvalidInstruction); let root_changed = args.root_key != old_record.root_key; if args.auth_mode == AUTH_MODE_BLOCKCHAIN { require!(!root_changed, ShineUsersError::RootChangeRequiresRoot); } if root_changed && args.new_blockchain_key.is_some() { return Err(ProgramError::from(ShineUsersError::AmbiguousAuthorityRotation)); } let old_active_key = old_record.active_fork()?.blockchain_key; let old_authority = if args.auth_mode == AUTH_MODE_ROOT { old_record.root_key } else { old_active_key }; let mut forks = old_record.forks.clone(); let additional_limit_u32 = u32::try_from(args.additional_limit).map_err(|_| ProgramError::from(ShineUsersError::PaidLimitTooLarge))?; let mut appended_key = None; if let Some(new_key) = args.new_blockchain_key { for fork in &forks { require!(fork.blockchain_key != new_key, ShineUsersError::DuplicateBlockchainKey); } if args.auth_mode == AUTH_MODE_BLOCKCHAIN { enforce_fork_cooldown(old_record.active_fork()?.created_at_ms, args.updated_at_ms)?; } let current_limit = old_record.active_fork()?.paid_limit_bytes; let new_limit = current_limit.checked_add(additional_limit_u32).ok_or(ProgramError::from(ShineUsersError::PaidLimitTooLarge))?; forks.push(ForkRecordV12 { blockchain_key: new_key, created_at_ms: args.updated_at_ms, paid_limit_bytes: new_limit }); appended_key = Some(new_key); } else if additional_limit_u32 > 0 { let active = forks.last_mut().ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; active.paid_limit_bytes = active.paid_limit_bytes.checked_add(additional_limit_u32).ok_or(ProgramError::from(ShineUsersError::PaidLimitTooLarge))?; } let prev_hash = hash_unsigned_record_v12(&old_record)?; let mut new_record = UserRecordV12 { created_at_ms: old_record.created_at_ms, updated_at_ms: args.updated_at_ms, record_number: old_record.record_number.checked_add(1).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?, prev_record_hash: prev_hash, login: old_record.login.clone(), root_key: args.root_key, client_key: args.client_key, forks, server_addresses: args.server_addresses.clone(), access_servers: args.access_servers.clone(), signature: [0; 64], }; let record_signer = if root_changed { new_record.root_key } else if let Some(new_key) = appended_key { new_key } else if args.auth_mode == AUTH_MODE_ROOT { new_record.root_key } else { new_record.active_fork()?.blockchain_key }; let unsigned = serialize_unsigned_record_v12(&new_record)?; let unsigned_hash = hashv(&[&unsigned]); // Первая подпись разрешает переход старым authority. Вторая подпись хранится // в PDA и принадлежит authority уже нового состояния. verify_ed25519_instruction_pubkey_message(instructions_sysvar, -2, &old_authority, unsigned_hash.as_ref())?; new_record.signature = verify_record_signature_hash_at(instructions_sysvar, -1, &record_signer, &args.signature, unsigned_hash.as_ref())?; let serialized = serialize_full_record_v12(&new_record)?; ensure_pda_size_and_rent(user_pda, signer, system_program_ai, serialized.len())?; write_pda_exact(user_pda, &serialized)?; let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?; if topup_fee > 0 { transfer_lamports(signer, inflow_vault, system_program_ai, topup_fee)?; } Ok(()) } fn process_close_legacy_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: CloseLegacyUserPdaArgs) -> ProgramResult { let mut it = accounts.iter(); let caller = next_account_info(&mut it)?; let user_pda = next_account_info(&mut it)?; require!(it.next().is_none(), ShineUsersError::InvalidInstruction); require!(caller.is_signer, ShineUsersError::InvalidSigner); require!(caller.is_writable, ShineUsersError::InvalidAccountData); require!(user_pda.is_writable, ShineUsersError::InvalidAccountData); validate_login(&args.login)?; let normalized_login = login_seed_normalized(&args.login); require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress); require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress); let raw = read_pda_all(user_pda)?; require!(record_format_minor(&raw)? == LEGACY_FORMAT_MINOR, ShineUsersError::CannotCloseCurrentFormat); let lamports = user_pda.lamports(); { let mut caller_lamports = caller.try_borrow_mut_lamports().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; **caller_lamports = (**caller_lamports).checked_add(lamports).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; } { let mut pda_lamports = user_pda.try_borrow_mut_lamports().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; **pda_lamports = 0; } user_pda.realloc(0, false)?; Ok(()) } struct PromoRegistrationContext<'a> { promo_seller_pda: &'a AccountInfo<'a>, updated_state: PromoSellerState, } fn verify_promo_registration<'a>( program_id: &Pubkey, instructions_sysvar: &AccountInfo<'a>, promo_seller_pda: &'a AccountInfo<'a>, target_login: &str, seller_login: &str, ) -> Result, ProgramError> { validate_login(seller_login)?; let normalized_seller_login = login_seed_normalized(&seller_login); let expected_pda = find_promo_seller_pda(program_id, &normalized_seller_login).0; require_keys_eq!(expected_pda, *promo_seller_pda.key, ShineUsersError::InvalidPdaAddress); require!(promo_seller_pda.owner == program_id, ShineUsersError::PromoSellerNotFound); let state = read_promo_seller_state(promo_seller_pda)?; require!(state.remaining_sales > 0, ShineUsersError::PromoSalesExhausted); require!(target_login.len() >= state.min_login_length as usize, ShineUsersError::PromoLoginTooShort); let message = build_promo_sign_message(target_login); verify_ed25519_instruction_pubkey_message(instructions_sysvar, -3, &state.signer_pubkey, message.as_bytes()) .map_err(|_| ProgramError::from(ShineUsersError::PromoSignatureMismatch))?; let updated_state = PromoSellerState { version: state.version, remaining_sales: state.remaining_sales.saturating_sub(1), min_login_length: state.min_login_length, signer_pubkey: state.signer_pubkey, }; Ok(PromoRegistrationContext { promo_seller_pda, updated_state, }) } fn classify_login_or_fail(login_guard_program: &AccountInfo, login: &str) -> ProgramResult { let login_guard_program_id = Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID) .map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?; require_keys_eq!(*login_guard_program.key, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse); let mut data = Vec::with_capacity(1 + 4 + login.len()); data.push(LOGIN_GUARD_IX_CLASSIFY_LOGIN); data.extend_from_slice(&(login.len() as u32).to_le_bytes()); data.extend_from_slice(login.as_bytes()); let ix = Instruction { program_id: login_guard_program_id, accounts: vec![], data }; invoke(&ix, &[login_guard_program.clone()])?; let (program_id, raw) = get_return_data().ok_or(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?; require_keys_eq!(program_id, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse); require!(raw.len() == 4, ShineUsersError::InvalidLoginGuardResponse); let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); match class { 0 => Ok(()), 1 => Err(ProgramError::from(ShineUsersError::PremiumLogin)), 2 => Err(ProgramError::from(ShineUsersError::TrademarkLoginRequiresReview)), _ => Err(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse)), } } fn serialize_users_economy_config(state: &UsersEconomyConfigState) -> Vec { let mut out = Vec::with_capacity(1 + 8 + 8 + 8); out.push(state.version); out.extend_from_slice(&state.registration_fee_lamports.to_le_bytes()); out.extend_from_slice(&state.lamports_per_limit_step.to_le_bytes()); out.extend_from_slice(&state.start_bonus_limit.to_le_bytes()); out } fn serialize_promo_seller_state(state: &PromoSellerState) -> Vec { let mut out = Vec::with_capacity(1 + 8 + 1 + 32); out.push(state.version); out.extend_from_slice(&state.remaining_sales.to_le_bytes()); out.push(state.min_login_length); out.extend_from_slice(state.signer_pubkey.as_ref()); out } fn validate_users_economy_config_pda(program_id: &Pubkey, pda: &AccountInfo) -> ProgramResult { let (expected_pda, _) = find_users_economy_config_pda(program_id); require_keys_eq!(expected_pda, *pda.key, ShineUsersError::InvalidPdaAddress); require!(pda.owner == program_id, ShineUsersError::InvalidPdaAddress); Ok(()) } fn read_users_economy_config(pda: &AccountInfo) -> Result { let raw = read_pda_all(pda)?; require!(!raw.is_empty(), ShineUsersError::EmptyPdaData); require!(raw.len() >= 25, ShineUsersError::InvalidAccountData); Ok(UsersEconomyConfigState { version: raw[0], registration_fee_lamports: u64::from_le_bytes(raw[1..9].try_into().unwrap()), lamports_per_limit_step: u64::from_le_bytes(raw[9..17].try_into().unwrap()), start_bonus_limit: u64::from_le_bytes(raw[17..25].try_into().unwrap()), }) } fn read_promo_seller_state(pda: &AccountInfo) -> Result { let raw = read_pda_all(pda)?; require!(!raw.is_empty(), ShineUsersError::PromoSellerNotFound); require!(raw.len() >= 42, ShineUsersError::InvalidPromoSellerState); let signer_pubkey = Pubkey::new_from_array(raw[10..42].try_into().map_err(|_| ProgramError::from(ShineUsersError::InvalidPromoSellerState))?); Ok(PromoSellerState { version: raw[0], remaining_sales: u64::from_le_bytes(raw[1..9].try_into().unwrap()), min_login_length: raw[9], signer_pubkey, }) } fn record_format_minor(raw: &[u8]) -> Result { require!(raw.len() >= 9, ShineUsersError::InvalidRecordData); require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic); require!(raw[5] == FORMAT_MAJOR, ShineUsersError::InvalidRecordFormat); Ok(raw[6]) } fn deserialize_record_v12_from_pda(raw: &[u8]) -> Result { require!(raw.len() >= 9, ShineUsersError::InvalidRecordData); require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic); require!(raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat); let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize; require!(record_len >= 9 + 64, ShineUsersError::InvalidRecordLength); require!(record_len <= raw.len(), ShineUsersError::InvalidRecordLength); let useful = &raw[..record_len]; let mut cursor = 9usize; let created_at_ms = read_u64_from(useful, &mut cursor)?; let updated_at_ms = read_u64_from(useful, &mut cursor)?; let record_number = read_u32_from(useful, &mut cursor)?; let prev_record_hash = read_fixed_32_from(useful, &mut cursor)?; let login = read_len_prefixed_string_from(useful, &mut cursor)?; let blocks_count = read_u8_from(useful, &mut cursor)? as usize; let mut root_key = None; let mut client_key = None; let mut forks: Option> = None; let mut server_addresses = Vec::new(); let mut access_servers = Vec::new(); for _ in 0..blocks_count { let block_type = read_u8_from(useful, &mut cursor)?; let block_version = read_u8_from(useful, &mut cursor)?; match block_type { BLOCK_TYPE_ROOT_KEY => { require!(block_version == BLOCK_VERSION_0 && root_key.is_none(), ShineUsersError::InvalidRecordFormat); root_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?)); } BLOCK_TYPE_CLIENT_KEY => { require!(block_version == BLOCK_VERSION_0 && client_key.is_none(), ShineUsersError::InvalidRecordFormat); client_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?)); } _ => { let payload_len = read_u16_from(useful, &mut cursor)? as usize; let payload_end = cursor.checked_add(payload_len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordLength))?; require!(payload_end <= useful.len().saturating_sub(64), ShineUsersError::InvalidRecordLength); if block_version == BLOCK_VERSION_0 { match block_type { BLOCK_TYPE_BLOCKCHAIN_REGISTRY => { require!(forks.is_none(), ShineUsersError::InvalidRecordData); let mut p = cursor; let count = read_u16_from(useful, &mut p)? as usize; require!(count > 0, ShineUsersError::InvalidRecordData); let mut list = Vec::with_capacity(count); for _ in 0..count { list.push(ForkRecordV12 { blockchain_key: Pubkey::new_from_array(read_fixed_32_from(useful, &mut p)?), created_at_ms: read_u64_from(useful, &mut p)?, paid_limit_bytes: read_u32_from(useful, &mut p)?, }); } for pair in list.windows(2) { require!(pair[0].created_at_ms <= pair[1].created_at_ms, ShineUsersError::InvalidRecordData); } require!(p == payload_end, ShineUsersError::InvalidRecordLength); forks = Some(list); } BLOCK_TYPE_SERVER_PROFILE => { require!(server_addresses.is_empty(), ShineUsersError::InvalidRecordData); let mut p = cursor; let count = read_u8_from(useful, &mut p)? as usize; for _ in 0..count { server_addresses.push(ServerAddressRecordV12 { address_format_type: read_u8_from(useful, &mut p)?, address_format_version: read_u8_from(useful, &mut p)?, address: read_len_prefixed_string_from(useful, &mut p)?, }); } require!(p == payload_end, ShineUsersError::InvalidRecordLength); } BLOCK_TYPE_ACCESS_SERVERS => { require!(access_servers.is_empty(), ShineUsersError::InvalidRecordData); let mut p = cursor; let count = read_u8_from(useful, &mut p)? as usize; for _ in 0..count { access_servers.push(read_len_prefixed_string_from(useful, &mut p)?); } require!(p == payload_end, ShineUsersError::InvalidRecordLength); } _ => { // Неизвестный variable block 1.2 можно безопасно пропустить по payload_len. } } } cursor = payload_end; } } } let signature = read_fixed_64_from(useful, &mut cursor)?; require!(cursor == useful.len(), ShineUsersError::InvalidRecordLength); let record = UserRecordV12 { created_at_ms, updated_at_ms, record_number, prev_record_hash, login, root_key: root_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, client_key: client_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, forks: forks.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?, server_addresses, access_servers, signature, }; validate_server_addresses(&record.server_addresses)?; validate_access_servers(&record.access_servers)?; Ok(record) } fn serialize_unsigned_record_v12(record: &UserRecordV12) -> Result, ProgramError> { let login_bytes = record.login.as_bytes(); require!(login_bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidLogin); require!(!record.forks.is_empty() && record.forks.len() <= u16::MAX as usize, ShineUsersError::InvalidRecordData); validate_server_addresses(&record.server_addresses)?; validate_access_servers(&record.access_servers)?; let mut out = Vec::new(); out.extend_from_slice(MAGIC); out.push(FORMAT_MAJOR); out.push(FORMAT_MINOR); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&record.created_at_ms.to_le_bytes()); out.extend_from_slice(&record.updated_at_ms.to_le_bytes()); out.extend_from_slice(&record.record_number.to_le_bytes()); out.extend_from_slice(&record.prev_record_hash); out.push(login_bytes.len() as u8); out.extend_from_slice(login_bytes); let mut blocks_count = 3usize; if !record.server_addresses.is_empty() { blocks_count += 1; } if !record.access_servers.is_empty() { blocks_count += 1; } require!(blocks_count <= u8::MAX as usize, ShineUsersError::RecordTooLarge); out.push(blocks_count as u8); // Fixed-size core blocks intentionally omit payload_len. out.push(BLOCK_TYPE_ROOT_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.root_key.as_ref()); out.push(BLOCK_TYPE_CLIENT_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.client_key.as_ref()); write_variable_block(&mut out, BLOCK_TYPE_BLOCKCHAIN_REGISTRY, BLOCK_VERSION_0, |payload| { payload.extend_from_slice(&(record.forks.len() as u16).to_le_bytes()); for fork in &record.forks { payload.extend_from_slice(fork.blockchain_key.as_ref()); payload.extend_from_slice(&fork.created_at_ms.to_le_bytes()); payload.extend_from_slice(&fork.paid_limit_bytes.to_le_bytes()); } Ok(()) })?; if !record.server_addresses.is_empty() { write_variable_block(&mut out, BLOCK_TYPE_SERVER_PROFILE, BLOCK_VERSION_0, |payload| { require!(record.server_addresses.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); payload.push(record.server_addresses.len() as u8); for address in &record.server_addresses { payload.push(address.address_format_type); payload.push(address.address_format_version); write_len_prefixed_string(payload, &address.address)?; } Ok(()) })?; } if !record.access_servers.is_empty() { write_variable_block(&mut out, BLOCK_TYPE_ACCESS_SERVERS, BLOCK_VERSION_0, |payload| { require!(record.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); payload.push(record.access_servers.len() as u8); for login in &record.access_servers { write_len_prefixed_string(payload, login)?; } Ok(()) })?; } let record_len = out.len().checked_add(64).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; require!(record_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge); let len_bytes = (record_len as u16).to_le_bytes(); out[7] = len_bytes[0]; out[8] = len_bytes[1]; Ok(out) } fn serialize_full_record_v12(record: &UserRecordV12) -> Result, ProgramError> { let mut out = serialize_unsigned_record_v12(record)?; out.extend_from_slice(&record.signature); Ok(out) } fn hash_unsigned_record_v12(record: &UserRecordV12) -> Result<[u8; 32], ProgramError> { let unsigned = serialize_unsigned_record_v12(record)?; let digest = hashv(&[&unsigned]); let mut out = [0u8; 32]; out.copy_from_slice(digest.as_ref()); Ok(out) } fn write_variable_block(out: &mut Vec, block_type: u8, block_version: u8, writer: F) -> Result<(), ProgramError> where F: FnOnce(&mut Vec) -> Result<(), ProgramError>, { out.push(block_type); out.push(block_version); let len_pos = out.len(); out.extend_from_slice(&0u16.to_le_bytes()); let payload_start = out.len(); writer(out)?; let payload_len = out.len().checked_sub(payload_start).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; require!(payload_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge); let len = (payload_len as u16).to_le_bytes(); out[len_pos] = len[0]; out[len_pos + 1] = len[1]; Ok(()) } fn write_len_prefixed_string(out: &mut Vec, value: &str) -> Result<(), ProgramError> { let bytes = value.as_bytes(); require!(bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(bytes.len() as u8); out.extend_from_slice(bytes); Ok(()) } fn verify_record_signature_hash_at(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, key: &Pubkey, signature: &[u8; 64], message_hash: &[u8]) -> Result<[u8; 64], ProgramError> { verify_ed25519_signature_instruction(instructions_sysvar, index_relative_to_current, key, signature, message_hash)?; Ok(*signature) } struct ParsedEd25519Data { pubkey: Pubkey, signature: [u8; 64], message: Vec, } fn verify_ed25519_signature_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_signature: &[u8; 64], expected_message: &[u8]) -> ProgramResult { let parsed = load_parsed_ed25519_instruction(instructions_sysvar, index_relative_to_current)?; require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature); require!(parsed.signature == *expected_signature, ShineUsersError::InvalidSignature); require!(parsed.message == expected_message, ShineUsersError::InvalidSignature); Ok(()) } fn verify_ed25519_instruction_pubkey_message(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_message: &[u8]) -> ProgramResult { let parsed = load_parsed_ed25519_instruction(instructions_sysvar, index_relative_to_current)?; require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature); require!(parsed.message == expected_message, ShineUsersError::InvalidSignature); Ok(()) } fn load_parsed_ed25519_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64) -> Result { require_keys_eq!(*instructions_sysvar.key, solana_program::sysvar::instructions::id(), ShineUsersError::InvalidSignature); let current_index = load_current_index_checked(instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))? as i64; let target_index = current_index.checked_add(index_relative_to_current).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; require!(target_index >= 0, ShineUsersError::InvalidSignature); let ed_ix = load_instruction_at_checked(target_index as usize, instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?; require_keys_eq!(ed_ix.program_id, solana_program::ed25519_program::id(), ShineUsersError::InvalidSignature); parse_ed25519_ix(ed_ix.data.as_slice()) } fn parse_ed25519_ix(data: &[u8]) -> Result { require!(data.len() >= 16, ShineUsersError::InvalidSignature); require!(data[0] == 1, ShineUsersError::InvalidSignature); let signature_offset = le_u16(data, 2)? as usize; let signature_ix_index = le_u16(data, 4)?; let pubkey_offset = le_u16(data, 6)? as usize; let pubkey_ix_index = le_u16(data, 8)?; let message_offset = le_u16(data, 10)? as usize; let message_size = le_u16(data, 12)? as usize; let message_ix_index = le_u16(data, 14)?; require!(signature_ix_index == u16::MAX, ShineUsersError::InvalidSignature); require!(pubkey_ix_index == u16::MAX, ShineUsersError::InvalidSignature); require!(message_ix_index == u16::MAX, ShineUsersError::InvalidSignature); let signature_end = signature_offset.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let pubkey_end = pubkey_offset.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let message_end = message_offset.checked_add(message_size).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let signature_slice = data.get(signature_offset..signature_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let pubkey_slice = data.get(pubkey_offset..pubkey_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let message = data.get(message_offset..message_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let mut signature = [0u8; 64]; signature.copy_from_slice(signature_slice); let pubkey = Pubkey::new_from_array(<[u8; 32]>::try_from(pubkey_slice).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?); Ok(ParsedEd25519Data { pubkey, signature, message: message.to_vec() }) } fn le_u16(data: &[u8], offset: usize) -> Result { let end = offset.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; let s = data.get(offset..end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?; Ok(u16::from_le_bytes([s[0], s[1]])) } fn build_promo_sign_message(login: &str) -> String { let mut message = String::with_capacity(settings::PROMO_SIGN_PREFIX.len() + login.len()); message.push_str(settings::PROMO_SIGN_PREFIX); message.push_str(login); message } fn validate_login(login: &str) -> ProgramResult { require!(!login.is_empty(), ShineUsersError::InvalidLogin); require!(login.len() <= 20, ShineUsersError::InvalidLogin); for ch in login.chars() { if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') { return Err(ProgramError::from(ShineUsersError::InvalidLogin)); } } Ok(()) } fn login_seed_normalized(login: &str) -> String { login.to_ascii_lowercase() } fn validate_server_addresses(addresses: &[ServerAddressRecordV12]) -> ProgramResult { require!(addresses.len() <= 1, ShineUsersError::InvalidRecordData); for address in addresses { let bytes = address.address.as_bytes(); require!(!bytes.is_empty() && bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); } Ok(()) } fn validate_access_servers(servers: &[String]) -> ProgramResult { require!(servers.len() <= 1, ShineUsersError::InvalidRecordData); for login in servers { validate_login(login)?; } Ok(()) } fn current_time_ms() -> Result { let seconds = Clock::get()?.unix_timestamp; require!(seconds >= 0, ShineUsersError::InvalidRecordData); (seconds as u64).checked_mul(1000).ok_or(ProgramError::from(ShineUsersError::MathOverflow)) } fn validate_client_time_ms(value_ms: u64, now_ms: u64) -> ProgramResult { let delta = if value_ms >= now_ms { value_ms - now_ms } else { now_ms - value_ms }; require!(delta <= MAX_CLIENT_CLOCK_SKEW_MS, ShineUsersError::InvalidRecordData); Ok(()) } fn enforce_fork_cooldown(last_fork_ms: u64, now_ms: u64) -> ProgramResult { require!(now_ms >= last_fork_ms, ShineUsersError::InvalidRecordData); require!(now_ms.saturating_sub(last_fork_ms) >= FORK_COOLDOWN_MS, ShineUsersError::ForkCooldown); Ok(()) } fn validate_inflow_vault(inflow_vault: &AccountInfo) -> ProgramResult { let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidFeeReceiver))?; let (expected, _) = Pubkey::find_program_address(&[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], &payments_program_id); require_keys_eq!(expected, *inflow_vault.key, ShineUsersError::InvalidFeeReceiver); Ok(()) } fn transfer_lamports<'a>(payer: &AccountInfo<'a>, recipient: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, lamports: u64) -> ProgramResult { if lamports == 0 { return Ok(()); } let ix = system_instruction::transfer(payer.key, recipient.key, lamports); invoke(&ix, &[payer.clone(), recipient.clone(), system_program_ai.clone()]) } // Создание PDA, устойчивое к «минированию» детерминированного адреса. // Адрес будущей записи логина выводится из самого логина, поэтому злоумышленник // может заранее вычислить адрес и перевести на него немного лампортов обычным // system-переводом. Тогда обычный system_instruction::create_account упал бы // («account already in use») и заблокировал бы регистрацию этого логина навсегда. // Чтобы это исключить, при уже существующих на адресе лампортах создаём аккаунт // «поверх предзаполненного»: доводим ренту переводом, затем allocate + assign // под подписью PDA. Подсев чужих лампортов больше ничего не ломает. fn create_pda_account<'a>(payer: &AccountInfo<'a>, pda: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, owner: &Pubkey, seeds: &[&[u8]], space: usize) -> ProgramResult { let rent = Rent::get()?; let required_lamports = rent.minimum_balance(space); let current_lamports = pda.lamports(); if current_lamports == 0 { // Быстрый путь: адрес пуст — обычное создание аккаунта одной инструкцией. let ix = system_instruction::create_account(payer.key, pda.key, required_lamports, space as u64, owner); return invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds]); } // На адресе уже лежат лампорты (вероятно, «подсев» атакующим). Доводим баланс // до рент-экземпта, выделяем место и назначаем владельцем нашу программу. let top_up = required_lamports.saturating_sub(current_lamports); transfer_lamports(payer, pda, system_program_ai, top_up)?; let allocate_ix = system_instruction::allocate(pda.key, space as u64); invoke_signed(&allocate_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])?; let assign_ix = system_instruction::assign(pda.key, owner); invoke_signed(&assign_ix, &[pda.clone(), system_program_ai.clone()], &[seeds]) } fn ensure_pda_size_and_rent<'a>(pda: &AccountInfo<'a>, payer: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, required_len: usize) -> ProgramResult { let current_len = pda.data_len(); if required_len <= current_len { return Ok(()); } let increase = required_len.checked_sub(current_len).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?; require!(increase <= MAX_AUTO_REALLOC_INCREASE, ShineUsersError::RecordTooLarge); let rent = Rent::get()?; let required_lamports = rent.minimum_balance(required_len); let current_lamports = pda.lamports(); let top_up = required_lamports.saturating_sub(current_lamports); if top_up > 0 { transfer_lamports(payer, pda, system_program_ai, top_up)?; } pda.realloc(required_len, false) } fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login.as_bytes()], program_id) } fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id) } fn find_promo_seller_pda(program_id: &Pubkey, seller_login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::PROMO_SELLER_PDA_SEED_PREFIX.as_bytes(), seller_login.as_bytes()], program_id) } fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result { (limit_delta / settings::LIMIT_STEP).checked_mul(lamports_per_limit_step).ok_or(ProgramError::from(ShineUsersError::MathOverflow)) } fn pad_to_fixed_size(mut bytes: Vec, target_size: usize) -> Result, ProgramError> { require!(bytes.len() <= target_size, ShineUsersError::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) } fn read_pda_all(pda: &AccountInfo) -> Result, ProgramError> { Ok(pda.try_borrow_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?.to_vec()) } fn write_pda_exact(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); for b in &mut dst[data.len()..] { *b = 0; } Ok(()) } fn write_pda_prefix(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); Ok(()) } fn read_u8_from(data: &[u8], cursor: &mut usize) -> Result { let v = *data.get(*cursor).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor += 1; Ok(v) } fn read_u16_from(data: &[u8], cursor: &mut usize) -> Result { let end = cursor.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u16::from_le_bytes([s[0], s[1]])) } fn read_u32_from(data: &[u8], cursor: &mut usize) -> Result { let end = cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) } fn read_u64_from(data: &[u8], cursor: &mut usize) -> Result { let end = cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) } fn read_fixed_32_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 32], ProgramError> { let end = cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) } fn read_fixed_64_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 64], ProgramError> { let end = cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) } fn read_len_prefixed_string_from(data: &[u8], cursor: &mut usize) -> Result { let len = read_u8_from(data, cursor)? as usize; let end = cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }