SHiNE-server/shine-solana/shine/programs/shine_users/src/lib.rs
AidarKC 42dcf6970d homeserver: рендейм subserver→homeserver, документ деривации ключей, запрет пустого пароля
Основное (наша работа в этой сессии):
- Переименование «subserver» → «homeserver» по всему проекту: основной ESP32-скетч
  (папка shine_subserver_ui → shine_homeserver_ui, .ino, flash-скрипт, режим burn.sh
  homeserver-ui), скетч lvgl_nav_minimal_test (ключ homeserver.key:<имя>), spec-доки
  reference/*, формат PDA (терминология session_type=100 «Homeserver пользователя»),
  константа SESSION_TYPE_HOMESERVER в JS и Rust (значение 100 не менялось, формат не затронут),
  pending/future доки, AGENTS.md, DAO-док. Сохранены отдельный lvgl_subserver_touch_test и
  историческая пометка о рендейме в DERIVATION.md.
- Новый источник истины по деривации ключей: Dev_Docs/Keys/DERIVATION.md (Argon2id-секрет из
  пароля, формула Ed25519(SHA-256(base64(secret)|suffix)), суффиксы root/bch/dev/homeserver.key,
  Solana-ключ = dev.key). Уточнены роли root (главный/master) и dev (пополняемый кошелёк) в
  Dev_Docs/Keys/README.md.
- UI: убран легаси-путь пустого пароля (derivePasswordSeed и др.), deriveMasterSecretFromPassword
  бросает ошибку на пустом пароле, register-view блокирует пустой пароль; экран пополнения
  переведён на канонический device-адрес из preGeneratedKeyBundle (удалён расходящийся
  deriveWalletFromPassword).

Включены также параллельные правки Solana-аудита №3 (были в рабочем дереве, переплетены в lib.rs):
- shine_users: defense-in-depth «строгий список аккаунтов» (require!(it.next().is_none()))
  в init/update economy config и create/update user PDA, плюс описание в doc/programs/shine_users.md;
- Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:16:12 +04:00

1073 lines
55 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use solana_program::{
account_info::{next_account_info, AccountInfo},
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!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
entrypoint!(process_instruction);
const MAGIC: &[u8; 5] = b"SHiNE";
const FORMAT_MAJOR: u8 = 1;
const FORMAT_MINOR: u8 = 0;
const MAX_SYNC_SERVERS: usize = 32;
const MAX_SESSIONS: usize = 64;
const MAX_SESSION_NAME_LEN: usize = 64;
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_DEVICE_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_TYPE_SESSIONS: u8 = 50;
const BLOCK_TYPE_TRUSTED_STATE: u8 = 70;
const BLOCK_VERSION_0: u8 = 0;
const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1;
const SESSIONS_MODE_MIXED: u8 = 1;
const SESSIONS_MODE_PDA_ONLY: u8 = 10;
const SESSION_TYPE_USER: u8 = 1;
const SESSION_TYPE_HOMESERVER: u8 = 100;
const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK";
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 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,
}
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 SessionRecord {
pub session_type: u8,
pub session_version: u8,
pub session_name: String,
pub session_pub_key: Pubkey,
}
#[derive(Clone, Debug)]
pub struct UserMutableFields {
pub device_key: Pubkey,
pub blockchain_public_key: Pubkey,
pub blockchain_name: String,
pub used_bytes: u64,
pub last_block_number: u32,
pub last_block_hash: [u8; 32],
pub last_block_signature: [u8; 64],
pub arweave_tx_id: String,
pub is_server: bool,
pub address_format_type: u8,
pub address_format_version: u8,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub sessions_mode: u8,
pub sessions: Vec<SessionRecord>,
pub trusted_count: u8,
}
#[derive(Clone, Debug)]
pub struct CreateUserPdaArgs {
pub login: String,
pub root_key: Pubkey,
pub created_at_ms: u64,
pub additional_limit: u64,
pub fields: UserMutableFields,
pub signature: [u8; 64],
}
#[derive(Clone, Debug)]
pub struct UpdateUserPdaArgs {
pub login: String,
pub root_key: Pubkey,
pub created_at_ms: u64,
pub updated_at_ms: u64,
pub version: u32,
pub prev_hash: [u8; 32],
pub additional_limit: u64,
pub fields: UserMutableFields,
pub signature: [u8; 64],
}
#[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 BlockchainRecord {
pub blockchain_type: u8,
pub blockchain_name: String,
pub blockchain_public_key: Pubkey,
pub paid_limit_bytes: u64,
pub used_bytes: u64,
pub last_block_number: u32,
pub last_block_hash: [u8; 32],
pub last_block_signature: [u8; 64],
pub arweave_tx_id: String,
}
#[derive(Clone, Debug)]
pub struct UserRecord {
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 device_key: Pubkey,
pub blockchain: BlockchainRecord,
pub is_server: bool,
pub address_format_type: u8,
pub address_format_version: u8,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub sessions_mode: u8,
pub sessions: Vec<SessionRecord>,
pub trusted_count: u8,
pub signature: [u8; 64],
}
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_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 finish(self) -> Result<(), ProgramError> {
require!(self.cursor == self.data.len(), ShineUsersError::InvalidInstruction);
Ok(())
}
}
fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], 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 => {
let args = parse_create_args(&mut r)?;
r.finish()?;
process_create_user_pda(program_id, accounts, args)
}
IX_UPDATE_USER_PDA => {
let args = parse_update_args(&mut r)?;
r.finish()?;
process_update_user_pda(program_id, accounts, args)
}
_ => Err(ProgramError::from(ShineUsersError::InvalidInstruction)),
}
}
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()?,
fields: parse_fields(r)?,
signature: r.read_fixed_64()?,
})
}
fn parse_update_args(r: &mut Reader<'_>) -> Result<UpdateUserPdaArgs, ProgramError> {
Ok(UpdateUserPdaArgs {
login: r.read_string_u8()?,
root_key: r.read_pubkey()?,
created_at_ms: r.read_u64()?,
updated_at_ms: r.read_u64()?,
version: r.read_u32()?,
prev_hash: r.read_fixed_32()?,
additional_limit: r.read_u64()?,
fields: parse_fields(r)?,
signature: r.read_fixed_64()?,
})
}
fn parse_fields(r: &mut Reader<'_>) -> Result<UserMutableFields, ProgramError> {
let device_key = r.read_pubkey()?;
let blockchain_public_key = r.read_pubkey()?;
let blockchain_name = r.read_string_u8()?;
let used_bytes = r.read_u64()?;
let last_block_number = r.read_u32()?;
let last_block_hash = r.read_fixed_32()?;
let last_block_signature = r.read_fixed_64()?;
let arweave_tx_id = r.read_string_u8()?;
let is_server = r.read_u8()? == 1;
let (address_format_type, address_format_version, server_address, sync_servers) = if is_server {
let aft = r.read_u8()?;
let afv = r.read_u8()?;
let addr = r.read_string_u8()?;
let cnt = r.read_u8()? as usize;
let mut list = Vec::with_capacity(cnt);
for _ in 0..cnt { list.push(r.read_string_u8()?); }
(aft, afv, addr, list)
} else {
(0, 0, String::new(), Vec::new())
};
let access_count = r.read_u8()? as usize;
let mut access_servers = Vec::with_capacity(access_count);
for _ in 0..access_count { access_servers.push(r.read_string_u8()?); }
let sessions_mode = r.read_u8()?;
let sessions_count = r.read_u8()? as usize;
let mut sessions = Vec::with_capacity(sessions_count);
for _ in 0..sessions_count {
sessions.push(SessionRecord {
session_type: r.read_u8()?,
session_version: r.read_u8()?,
session_name: r.read_string_u8()?,
session_pub_key: r.read_pubkey()?,
});
}
let trusted_count = r.read_u8()?;
Ok(UserMutableFields {
device_key,
blockchain_public_key,
blockchain_name,
used_bytes,
last_block_number,
last_block_hash,
last_block_signature,
arweave_tx_id,
is_server,
address_format_type,
address_format_version,
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
})
}
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_create_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: 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)?;
require!(it.next().is_none(), ShineUsersError::InvalidInstruction);
require!(signer.is_signer, ShineUsersError::InvalidSigner);
require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner);
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
validate_login(&args.login)?;
validate_fields(&args.fields)?;
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);
classify_login_or_fail(login_guard_program, &args.login)?;
validate_users_economy_config_pda(program_id, users_economy_config_pda)?;
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 = economy.start_bonus_limit.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
let mut record = UserRecord {
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,
device_key: args.fields.device_key,
blockchain: BlockchainRecord {
blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER,
blockchain_name: args.fields.blockchain_name,
blockchain_public_key: args.fields.blockchain_public_key,
paid_limit_bytes: start_balance,
used_bytes: args.fields.used_bytes,
last_block_number: args.fields.last_block_number,
last_block_hash: args.fields.last_block_hash,
last_block_signature: args.fields.last_block_signature,
arweave_tx_id: args.fields.arweave_tx_id,
},
is_server: args.fields.is_server,
address_format_type: args.fields.address_format_type,
address_format_version: args.fields.address_format_version,
server_address: args.fields.server_address,
sync_servers: args.fields.sync_servers,
access_servers: args.fields.access_servers,
sessions_mode: args.fields.sessions_mode,
sessions: args.fields.sessions,
trusted_count: args.fields.trusted_count,
signature: [0; 64],
};
validate_blockchain_limits(&record.blockchain, 0, 0, true)?;
verify_last_block_state_signature(instructions_sysvar, &record)?;
let unsigned = serialize_unsigned_record(&record)?;
let unsigned_hash = hashv(&[&unsigned]);
record.signature = verify_record_signature_hash(instructions_sysvar, &record.root_key, &args.signature, unsigned_hash.as_ref())?;
let serialized = pad_to_fixed_size(serialize_full_record(&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)?;
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: 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!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner);
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
validate_login(&args.login)?;
validate_fields(&args.fields)?;
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 old_record = deserialize_record_from_pda(&read_pda_all(user_pda)?)?;
require!(old_record.login == args.login, ShineUsersError::ImmutableFieldChanged);
require!(old_record.created_at_ms == args.created_at_ms, ShineUsersError::ImmutableFieldChanged);
require_keys_eq!(old_record.root_key, args.root_key, ShineUsersError::ImmutableFieldChanged);
require!(args.version == old_record.record_number.saturating_add(1), ShineUsersError::InvalidVersion);
require!(hash_unsigned_record(&old_record)? == args.prev_hash, ShineUsersError::InvalidPrevHash);
require!(args.fields.blockchain_name == old_record.blockchain.blockchain_name, ShineUsersError::ImmutableFieldChanged);
require_keys_eq!(args.fields.blockchain_public_key, old_record.blockchain.blockchain_public_key, ShineUsersError::ImmutableFieldChanged);
let new_balance = old_record.blockchain.paid_limit_bytes.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
require!(new_balance >= old_record.blockchain.paid_limit_bytes, ShineUsersError::BalanceDecrease);
let blockchain_state_unchanged = old_record.blockchain.used_bytes == args.fields.used_bytes
&& old_record.blockchain.last_block_number == args.fields.last_block_number
&& old_record.blockchain.last_block_hash == args.fields.last_block_hash
&& old_record.blockchain.last_block_signature == args.fields.last_block_signature
&& old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id;
let mut new_record = build_update_record(&old_record, &args, new_balance)?;
validate_blockchain_limits(&new_record.blockchain, old_record.blockchain.used_bytes, old_record.blockchain.last_block_number, false)?;
if !blockchain_state_unchanged {
verify_last_block_state_signature(instructions_sysvar, &new_record)?;
}
let unsigned = serialize_unsigned_record(&new_record)?;
let unsigned_hash = hashv(&[&unsigned]);
new_record.signature = verify_record_signature_hash(instructions_sysvar, &old_record.root_key, &args.signature, unsigned_hash.as_ref())?;
let serialized = serialize_full_record(&new_record)?;
ensure_pda_size_and_rent(user_pda, signer, system_program_ai, serialized.len())?;
write_pda_prefix(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 build_update_record(old_record: &UserRecord, args: &UpdateUserPdaArgs, new_balance: u64) -> Result<UserRecord, ProgramError> {
Ok(UserRecord {
created_at_ms: old_record.created_at_ms,
updated_at_ms: args.updated_at_ms,
record_number: args.version,
prev_record_hash: args.prev_hash,
login: old_record.login.clone(),
root_key: old_record.root_key,
device_key: args.fields.device_key,
blockchain: BlockchainRecord {
blockchain_type: old_record.blockchain.blockchain_type,
blockchain_name: args.fields.blockchain_name.clone(),
blockchain_public_key: args.fields.blockchain_public_key,
paid_limit_bytes: new_balance,
used_bytes: args.fields.used_bytes,
last_block_number: args.fields.last_block_number,
last_block_hash: args.fields.last_block_hash,
last_block_signature: args.fields.last_block_signature,
arweave_tx_id: args.fields.arweave_tx_id.clone(),
},
is_server: args.fields.is_server,
address_format_type: args.fields.address_format_type,
address_format_version: args.fields.address_format_version,
server_address: args.fields.server_address.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
sessions_mode: args.fields.sessions_mode,
sessions: args.fields.sessions.clone(),
trusted_count: args.fields.trusted_count,
signature: [0; 64],
})
}
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 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 deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord, 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 device_key = None;
let mut blockchain = None;
let mut is_server = false;
let mut address_format_type = 0u8;
let mut address_format_version = 0u8;
let mut server_address = String::new();
let mut sync_servers = Vec::new();
let mut access_servers = Vec::new();
let mut sessions_mode = SESSIONS_MODE_MIXED;
let mut sessions = Vec::new();
let mut trusted_count = 0u8;
for _ in 0..blocks_count {
let block_type = read_u8_from(useful, &mut cursor)?;
let block_version = read_u8_from(useful, &mut cursor)?;
require!(block_version == BLOCK_VERSION_0, ShineUsersError::InvalidRecordFormat);
match block_type {
BLOCK_TYPE_ROOT_KEY => {
require!(root_key.is_none(), ShineUsersError::InvalidRecordData);
root_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
}
BLOCK_TYPE_DEVICE_KEY => {
require!(device_key.is_none(), ShineUsersError::InvalidRecordData);
device_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
}
BLOCK_TYPE_BLOCKCHAIN_REGISTRY => {
require!(blockchain.is_none(), ShineUsersError::InvalidRecordData);
let count = read_u8_from(useful, &mut cursor)?;
require!(count == 1, ShineUsersError::InvalidRecordData);
blockchain = Some(read_blockchain_record(useful, &mut cursor)?);
}
BLOCK_TYPE_SERVER_PROFILE => {
require!(!is_server, ShineUsersError::InvalidRecordData);
is_server = read_u8_from(useful, &mut cursor)? == 1;
require!(is_server, ShineUsersError::InvalidRecordData);
address_format_type = read_u8_from(useful, &mut cursor)?;
address_format_version = read_u8_from(useful, &mut cursor)?;
server_address = read_len_prefixed_string_from(useful, &mut cursor)?;
let sync_count = read_u8_from(useful, &mut cursor)? as usize;
require!(sync_count <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData);
for _ in 0..sync_count { sync_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); }
}
BLOCK_TYPE_ACCESS_SERVERS => {
require!(access_servers.is_empty(), ShineUsersError::InvalidRecordData);
let access_count = read_u8_from(useful, &mut cursor)? as usize;
for _ in 0..access_count { access_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); }
}
BLOCK_TYPE_SESSIONS => {
require!(sessions.is_empty(), ShineUsersError::InvalidRecordData);
sessions_mode = read_u8_from(useful, &mut cursor)?;
let sessions_count = read_u8_from(useful, &mut cursor)? as usize;
require!(sessions_count <= MAX_SESSIONS, ShineUsersError::InvalidRecordData);
for _ in 0..sessions_count { sessions.push(read_session_record(useful, &mut cursor)?); }
}
BLOCK_TYPE_TRUSTED_STATE => {
trusted_count = read_u8_from(useful, &mut cursor)?;
}
_ => return Err(ProgramError::from(ShineUsersError::InvalidRecordFormat)),
}
}
validate_sessions_fields(sessions_mode, &sessions)?;
let signature = read_fixed_64_from(useful, &mut cursor)?;
require!(cursor == useful.len(), ShineUsersError::InvalidRecordLength);
Ok(UserRecord {
created_at_ms,
updated_at_ms,
record_number,
prev_record_hash,
login,
root_key: root_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
device_key: device_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
blockchain: blockchain.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
is_server,
address_format_type,
address_format_version,
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
signature,
})
}
fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result<BlockchainRecord, ProgramError> {
let blockchain_type = read_u8_from(data, cursor)?;
require!(blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData);
let blockchain_name = read_len_prefixed_string_from(data, cursor)?;
let blockchain_public_key = Pubkey::new_from_array(read_fixed_32_from(data, cursor)?);
let paid_limit_bytes = read_u64_from(data, cursor)?;
let used_bytes = read_u64_from(data, cursor)?;
let last_block_number = read_u32_from(data, cursor)?;
let last_block_hash = read_fixed_32_from(data, cursor)?;
let last_block_signature = read_fixed_64_from(data, cursor)?;
let arweave_present = read_u8_from(data, cursor)?;
let arweave_tx_id = match arweave_present { 0 => String::new(), 1 => read_len_prefixed_string_from(data, cursor)?, _ => return Err(ProgramError::from(ShineUsersError::InvalidRecordData)) };
Ok(BlockchainRecord {
blockchain_type,
blockchain_name,
blockchain_public_key,
paid_limit_bytes,
used_bytes,
last_block_number,
last_block_hash,
last_block_signature,
arweave_tx_id,
})
}
fn read_session_record(data: &[u8], cursor: &mut usize) -> Result<SessionRecord, ProgramError> {
Ok(SessionRecord {
session_type: read_u8_from(data, cursor)?,
session_version: read_u8_from(data, cursor)?,
session_name: read_len_prefixed_string_from(data, cursor)?,
session_pub_key: Pubkey::new_from_array(read_fixed_32_from(data, cursor)?),
})
}
fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
let login_bytes = record.login.as_bytes();
require!(login_bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidLogin);
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 blocks_count = if record.is_server { 7 } else { 6 };
out.push(blocks_count);
write_root_key_block(&mut out, record);
write_device_key_block(&mut out, record);
write_blockchain_registry_block(&mut out, &record.blockchain)?;
if record.is_server { write_server_profile_block(&mut out, record)?; }
write_access_servers_block(&mut out, record)?;
write_sessions_block(&mut out, record)?;
write_trusted_state_block(&mut out, record);
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(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
let mut out = serialize_unsigned_record(record)?;
out.extend_from_slice(&record.signature);
Ok(out)
}
fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32], ProgramError> {
let unsigned = serialize_unsigned_record(record)?;
let digest = hashv(&[&unsigned]);
let mut out = [0u8; 32];
out.copy_from_slice(digest.as_ref());
Ok(out)
}
fn write_root_key_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_ROOT_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.root_key.as_ref()); }
fn write_device_key_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_DEVICE_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.device_key.as_ref()); }
fn write_blockchain_registry_block(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<(), ProgramError> { out.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY); out.push(BLOCK_VERSION_0); out.push(1); write_blockchain_record(out, blockchain) }
fn write_blockchain_record(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<(), ProgramError> {
out.push(blockchain.blockchain_type);
write_len_prefixed_string(out, &blockchain.blockchain_name)?;
out.extend_from_slice(blockchain.blockchain_public_key.as_ref());
out.extend_from_slice(&blockchain.paid_limit_bytes.to_le_bytes());
out.extend_from_slice(&blockchain.used_bytes.to_le_bytes());
out.extend_from_slice(&blockchain.last_block_number.to_le_bytes());
out.extend_from_slice(&blockchain.last_block_hash);
out.extend_from_slice(&blockchain.last_block_signature);
if blockchain.arweave_tx_id.is_empty() { out.push(0); } else { out.push(1); write_len_prefixed_string(out, &blockchain.arweave_tx_id)?; }
Ok(())
}
fn write_server_profile_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
out.push(BLOCK_TYPE_SERVER_PROFILE); out.push(BLOCK_VERSION_0); out.push(1); out.push(record.address_format_type); out.push(record.address_format_version); write_len_prefixed_string(out, &record.server_address)?; require!(record.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData); out.push(record.sync_servers.len() as u8); for login in &record.sync_servers { write_len_prefixed_string(out, login)?; } Ok(())
}
fn write_access_servers_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
out.push(BLOCK_TYPE_ACCESS_SERVERS); out.push(BLOCK_VERSION_0); require!(record.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(record.access_servers.len() as u8); for login in &record.access_servers { write_len_prefixed_string(out, login)?; } Ok(())
}
fn write_sessions_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
out.push(BLOCK_TYPE_SESSIONS); out.push(BLOCK_VERSION_0); out.push(record.sessions_mode); require!(record.sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData); out.push(record.sessions.len() as u8); for session in &record.sessions { out.push(session.session_type); out.push(session.session_version); write_len_prefixed_string(out, &session.session_name)?; out.extend_from_slice(session.session_pub_key.as_ref()); } Ok(())
}
fn write_trusted_state_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_TRUSTED_STATE); out.push(BLOCK_VERSION_0); out.push(record.trusted_count); }
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(instructions_sysvar: &AccountInfo, root_key: &Pubkey, signature: &[u8; 64], message_hash: &[u8]) -> Result<[u8; 64], ProgramError> {
verify_ed25519_signature_instruction(instructions_sysvar, -2, root_key, signature, message_hash)?;
Ok(*signature)
}
fn verify_last_block_state_signature(instructions_sysvar: &AccountInfo, record: &UserRecord) -> ProgramResult {
let message = serialize_last_block_state(record)?;
let msg_hash = hashv(&[&message]);
verify_ed25519_signature_instruction(instructions_sysvar, -1, &record.blockchain.blockchain_public_key, &record.blockchain.last_block_signature, msg_hash.as_ref())
}
fn serialize_last_block_state(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
let mut out = Vec::new();
out.extend_from_slice(LAST_BLOCK_STATE_PREFIX);
write_len_prefixed_string(&mut out, &record.login)?;
write_len_prefixed_string(&mut out, &record.blockchain.blockchain_name)?;
out.extend_from_slice(&record.blockchain.last_block_number.to_le_bytes());
out.extend_from_slice(&record.blockchain.last_block_hash);
out.extend_from_slice(&record.blockchain.used_bytes.to_le_bytes());
Ok(out)
}
struct ParsedEd25519Ref<'a> {
pubkey: Pubkey,
signature: [u8; 64],
message: &'a [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 {
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);
let parsed = parse_ed25519_ix(ed_ix.data.as_slice())?;
require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature);
require!(parsed.signature == *expected_signature, ShineUsersError::InvalidSignature);
require!(parsed.message == expected_message, ShineUsersError::InvalidSignature);
Ok(())
}
fn parse_ed25519_ix<'a>(data: &'a [u8]) -> Result<ParsedEd25519Ref<'a>, 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(ParsedEd25519Ref { pubkey, signature, message })
}
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 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_fields(fields: &UserMutableFields) -> ProgramResult {
require!(!fields.blockchain_name.is_empty(), ShineUsersError::InvalidRecordData);
require!(fields.blockchain_name.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
require!(fields.arweave_tx_id.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
if fields.is_server {
require!(!fields.server_address.is_empty(), ShineUsersError::InvalidRecordData);
require!(fields.server_address.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
require!(fields.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData);
for login in &fields.sync_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); }
} else {
require!(fields.server_address.is_empty(), ShineUsersError::InvalidRecordData);
require!(fields.sync_servers.is_empty(), ShineUsersError::InvalidRecordData);
}
require!(fields.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
for login in &fields.access_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); }
validate_sessions_fields(fields.sessions_mode, &fields.sessions)
}
fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> ProgramResult {
require!(mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY, ShineUsersError::InvalidRecordData);
require!(sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData);
for i in 0..sessions.len() {
validate_session_record(&sessions[i])?;
for j in (i + 1)..sessions.len() {
require!(sessions[i].session_name != sessions[j].session_name, ShineUsersError::InvalidRecordData);
require!(sessions[i].session_pub_key != sessions[j].session_pub_key, ShineUsersError::InvalidRecordData);
}
}
Ok(())
}
fn validate_session_record(session: &SessionRecord) -> ProgramResult {
require!(session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_HOMESERVER, ShineUsersError::InvalidRecordData);
require!(session.session_version == 1, ShineUsersError::InvalidRecordData);
let bytes = session.session_name.as_bytes();
require!(!bytes.is_empty(), ShineUsersError::InvalidRecordData);
require!(bytes.len() <= MAX_SESSION_NAME_LEN, ShineUsersError::InvalidRecordData);
for &b in bytes { require!(b.is_ascii_alphanumeric() || b == b'_', ShineUsersError::InvalidRecordData); }
Ok(())
}
fn validate_blockchain_limits(blockchain: &BlockchainRecord, old_used_bytes: u64, old_last_block_number: u32, is_create: bool) -> ProgramResult {
require!(blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData);
require!(blockchain.used_bytes <= blockchain.paid_limit_bytes, ShineUsersError::InvalidRecordData);
if !is_create {
require!(blockchain.used_bytes >= old_used_bytes && blockchain.last_block_number >= old_last_block_number, ShineUsersError::InvalidRecordData);
}
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 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_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)) }