Files
SHiNE-server/shine-solana/shine/programs/shine_users/src/lib.rs
T

1256 lines
61 KiB
Rust

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<ShineUsersError> 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<ForkRecordV12>,
pub server_addresses: Vec<ServerAddressRecordV12>,
pub access_servers: Vec<String>,
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<ServerAddressRecordV12>,
pub access_servers: Vec<String>,
pub signature: [u8; 64],
pub promo_seller_login: Option<String>,
}
#[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<Pubkey>,
pub server_addresses: Vec<ServerAddressRecordV12>,
pub access_servers: Vec<String>,
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<u8, ProgramError> {
let v = *self.data.get(self.cursor).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
self.cursor += 1;
Ok(v)
}
fn read_u16(&mut self) -> Result<u16, ProgramError> {
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<u32, ProgramError> {
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<u64, ProgramError> {
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<Pubkey, ProgramError> {
Ok(Pubkey::new_from_array(self.read_fixed_32()?))
}
fn read_string_u8(&mut self) -> Result<String, ProgramError> {
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<Vec<ServerAddressRecordV12>, 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<Vec<String>, 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<CreateUserPdaArgs, ProgramError> {
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<UpdateUserPdaArgs, ProgramError> {
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<CreateUserPdaArgs>) -> 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<UpdateUserPdaArgs>) -> 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<PromoRegistrationContext<'a>, 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<u8> {
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<u8> {
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<UsersEconomyConfigState, ProgramError> {
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<PromoSellerState, ProgramError> {
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<u8, ProgramError> {
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<UserRecordV12, ProgramError> {
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<Vec<ForkRecordV12>> = 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<Vec<u8>, 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<Vec<u8>, 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<F>(out: &mut Vec<u8>, block_type: u8, block_version: u8, writer: F) -> Result<(), ProgramError>
where
F: FnOnce(&mut Vec<u8>) -> 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<u8>, 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<u8>,
}
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<ParsedEd25519Data, ProgramError> {
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<ParsedEd25519Data, ProgramError> {
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<u16, ProgramError> {
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<u64, ProgramError> {
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<u64, ProgramError> { (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<u8>, target_size: usize) -> Result<Vec<u8>, ProgramError> { require!(bytes.len() <= target_size, ShineUsersError::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) }
fn read_pda_all(pda: &AccountInfo) -> Result<Vec<u8>, 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<u8, ProgramError> { 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<u16, ProgramError> { 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<u32, ProgramError> { 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<u64, ProgramError> { 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<String, ProgramError> { 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)) }