Отключить репосты и добавить Solana-модуль

This commit is contained in:
AidarKC
2026-05-24 12:16:39 +03:00
parent abdce05136
commit 56cd90a197
95 changed files with 24261 additions and 64 deletions
@@ -0,0 +1,32 @@
use anchor_lang::prelude::*;
pub mod settings;
pub mod users;
use users::*;
declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
#[program]
pub mod shine {
use super::*;
pub fn init_users_economy_config(ctx: Context<InitUsersEconomyConfig>) -> Result<()> {
users::init_users_economy_config(ctx)
}
pub fn update_users_economy_config(
ctx: Context<UpdateUsersEconomyConfig>,
args: UpdateUsersEconomyConfigArgs,
) -> Result<()> {
users::update_users_economy_config(ctx, args)
}
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
users::create_user_pda(ctx, args)
}
pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) -> Result<()> {
users::update_user_pda(ctx, args)
}
}
@@ -0,0 +1,30 @@
use common::deploy_config;
/// `USER_PDA_SEED_PREFIX` — префикс seed для пользовательского PDA (`login=<...>`).
pub const USER_PDA_SEED_PREFIX: &str = "login=";
/// `USERS_ECONOMY_CONFIG_SEED` — seed PDA с экономическими параметрами программы `shine_users`.
pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_economy_config";
/// `USER_PDA_SPACE` — стартовый размер PDA пользователя, дальше запись может расширяться через realloc.
pub const USER_PDA_SPACE: usize = 768;
/// `USERS_ECONOMY_CONFIG_SPACE` — размер PDA с экономическими параметрами `shine_users`.
pub const USERS_ECONOMY_CONFIG_SPACE: usize = 8 + 96;
/// `DAO_AUTHORITY` — адрес DAO-авторити, который имеет право обновлять economy-конфиг.
pub const DAO_AUTHORITY: &str = deploy_config::DAO_AUTHORITY;
/// `SHINE_PAYMENTS_PROGRAM_ID` — адрес программы `shine_payments`, от которой вычисляется PDA inflow-вольта.
pub const SHINE_PAYMENTS_PROGRAM_ID: &str = deploy_config::SHINE_PAYMENTS_PROGRAM_ID;
/// `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — seed inflow-вольта в программе `shine_payments` (должен совпадать с payments settings).
pub const SHINE_PAYMENTS_INFLOW_VAULT_SEED: &[u8] = b"shine_payments_inflow_vault";
/// `SHINE_LOGIN_GUARD_PROGRAM_ID` — адрес отдельной программы проверки премиальности логина.
pub const SHINE_LOGIN_GUARD_PROGRAM_ID: &str = deploy_config::SHINE_LOGIN_GUARD_PROGRAM_ID;
/// `START_REGISTRATION_FEE_LAMPORTS` — стартовая комиссия регистрации (0.01 SOL) для initial economy-конфига.
pub const START_REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000;
/// `LIMIT_STEP` — шаг пополнения лимита; `additional_limit` должен быть кратен этому значению.
pub const LIMIT_STEP: u64 = 10_000;
/// `START_LAMPORTS_PER_LIMIT_STEP` — стартовая цена одного шага лимита (0.0001 SOL за 10_000 лимита).
pub const START_LAMPORTS_PER_LIMIT_STEP: u64 = 100_000;
/// `START_BONUS_LIMIT` — стартовый бонус лимита, выдаваемый пользователю при создании записи.
pub const START_BONUS_LIMIT: u64 = 100_000;
@@ -0,0 +1,993 @@
use crate::settings;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
ed25519_program,
hash::hashv,
instruction::Instruction,
program::{get_return_data, invoke},
system_instruction,
sysvar::instructions::{load_current_index_checked, load_instruction_at_checked},
};
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
use std::str::FromStr;
const MAGIC: &[u8; 5] = b"SHiNE";
const FORMAT_MAJOR: u8 = 1;
const FORMAT_MINOR: u8 = 0;
const KEY_STATUS_CREATED: u8 = 0;
const MAX_SYNC_SERVERS: usize = 32;
const MAX_AUTO_REALLOC_INCREASE: usize = 10_000;
const RESERVED_BYTES: [u8; 5] = [0, 0, 0, 0, 0];
const ZERO_HASH: [u8; 32] = [0; 32];
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UserMutableFields {
pub blockchain_key: Pubkey,
pub device_key: Pubkey,
pub chain_number: u16,
pub is_server: bool,
pub server_key: Pubkey,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub trusted_count: u8,
}
#[derive(AnchorSerialize, AnchorDeserialize, 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: Vec<u8>,
}
#[derive(AnchorSerialize, AnchorDeserialize, 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: Vec<u8>,
pub additional_limit: u64,
pub fields: UserMutableFields,
pub signature: Vec<u8>,
}
pub struct UserRecord {
pub created_at_ms: u64,
pub updated_at_ms: u64,
pub version: u32,
pub prev_hash: [u8; 32],
pub login: String,
pub root_key_status: u8,
pub root_key: Pubkey,
pub blockchain_key_status: u8,
pub blockchain_key: Pubkey,
pub device_key_status: u8,
pub device_key: Pubkey,
pub chain_number: u16,
pub balance: u64,
pub is_server: bool,
pub server_key: Pubkey,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub trusted_count: u8,
pub signature: [u8; 64],
}
#[derive(AnchorSerialize, AnchorDeserialize, 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(Accounts)]
pub struct CreateUserPda<'info> {
/// CHECK: подписант транзакции, валидируется Anchor как signer и mut.
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике.
#[account(mut)]
pub user_pda: AccountInfo<'info>,
pub system_program: Program<'info, System>,
/// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA.
#[account(mut)]
pub inflow_vault: AccountInfo<'info>,
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
pub instructions: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
pub users_economy_config_pda: AccountInfo<'info>,
pub login_guard_program: Program<'info, shine_login_guard::program::ShineLoginGuard>,
}
#[derive(Accounts)]
pub struct UpdateUserPda<'info> {
/// CHECK: подписант транзакции, валидируется Anchor как signer и mut.
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA пользователя, адрес проверяется вручную через seed в обработчике.
#[account(mut)]
pub user_pda: AccountInfo<'info>,
pub system_program: Program<'info, System>,
/// CHECK: inflow-вольт shine_payments, адрес проверяется вручную как PDA.
#[account(mut)]
pub inflow_vault: AccountInfo<'info>,
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
pub instructions: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
pub users_economy_config_pda: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct InitUsersEconomyConfig<'info> {
/// CHECK: подписант и плательщик, валидируется Anchor как signer и mut.
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
#[account(mut)]
pub users_economy_config_pda: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateUsersEconomyConfig<'info> {
/// CHECK: подписант (должен быть DAO authority из settings).
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
#[account(mut)]
pub users_economy_config_pda: AccountInfo<'info>,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UpdateUsersEconomyConfigArgs {
pub registration_fee_lamports: u64,
pub lamports_per_limit_step: u64,
pub start_bonus_limit: u64,
}
pub fn init_users_economy_config(ctx: Context<InitUsersEconomyConfig>) -> Result<()> {
let (expected_pda, bump) = find_users_economy_config_pda(ctx.program_id);
require_keys_eq!(
expected_pda,
ctx.accounts.users_economy_config_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.users_economy_config_pda.owner == &Pubkey::default(),
ErrCode::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 bytes = state
.try_to_vec()
.map_err(|_| error!(ErrCode::DeserializationError))?;
let seeds: &[&[u8]] = &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]];
create_pda(
&ctx.accounts.users_economy_config_pda,
&ctx.accounts.signer,
&ctx.accounts.system_program.to_account_info(),
ctx.program_id,
seeds,
settings::USERS_ECONOMY_CONFIG_SPACE as u64,
)?;
write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?;
Ok(())
}
pub fn update_users_economy_config(
ctx: Context<UpdateUsersEconomyConfig>,
args: UpdateUsersEconomyConfigArgs,
) -> Result<()> {
let dao_authority =
Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| error!(ErrCode::InvalidSigner))?;
require_keys_eq!(dao_authority, ctx.accounts.signer.key(), ErrCode::InvalidSigner);
let (expected_pda, _) = find_users_economy_config_pda(ctx.program_id);
require_keys_eq!(
expected_pda,
ctx.accounts.users_economy_config_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.users_economy_config_pda.owner == ctx.program_id,
ErrCode::InvalidPdaAddress
);
require!(args.lamports_per_limit_step > 0, ErrCode::InvalidRecordData);
let mut state = read_users_economy_config(&ctx.accounts.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;
let bytes = state
.try_to_vec()
.map_err(|_| error!(ErrCode::DeserializationError))?;
write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?;
Ok(())
}
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
validate_login(&args.login)?;
require_keys_eq!(
ctx.accounts.login_guard_program.key(),
Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID)
.map_err(|_| error!(ErrCode::InvalidLoginGuardResponse))?,
ErrCode::InvalidLoginGuardResponse
);
classify_login_or_fail(
&ctx.accounts.login_guard_program.to_account_info(),
&ctx.accounts.signer,
&args.login,
)?;
validate_fields(&args.fields)?;
validate_inflow_vault(&ctx.accounts.inflow_vault)?;
require!(
args.additional_limit % settings::LIMIT_STEP == 0,
ErrCode::InvalidLimitIncrement
);
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let login_seed = login_seed_normalized(&args.login);
let (expected_pda, bump) = find_user_pda(ctx.program_id, &login_seed);
require_keys_eq!(
expected_pda,
ctx.accounts.user_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.user_pda.owner == &Pubkey::default(),
ErrCode::UserAlreadyExists
);
let start_balance = economy
.start_bonus_limit
.checked_add(args.additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?;
let mut record = UserRecord {
created_at_ms: args.created_at_ms,
updated_at_ms: args.created_at_ms,
version: 0,
prev_hash: ZERO_HASH,
login: args.login.clone(),
root_key_status: KEY_STATUS_CREATED,
root_key: args.root_key,
blockchain_key_status: KEY_STATUS_CREATED,
blockchain_key: args.fields.blockchain_key,
device_key_status: KEY_STATUS_CREATED,
device_key: args.fields.device_key,
chain_number: args.fields.chain_number,
balance: start_balance,
is_server: args.fields.is_server,
server_key: args.fields.server_key,
server_address: args.fields.server_address.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
trusted_count: args.fields.trusted_count,
signature: [0; 64],
};
let unsigned = serialize_unsigned_record(&record)?;
record.signature = verify_record_signature(
&ctx.accounts.instructions,
&record.root_key,
&args.signature,
&unsigned,
)?;
let serialized = serialize_full_record(&record)?;
require!(
serialized.len() <= settings::USER_PDA_SPACE,
ErrCode::RecordTooLarge
);
let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?;
let pda_seeds: &[&[u8]] = &[
settings::USER_PDA_SEED_PREFIX.as_bytes(),
login_seed.as_bytes(),
&[bump],
];
create_pda(
&ctx.accounts.user_pda,
&ctx.accounts.signer,
&ctx.accounts.system_program.to_account_info(),
ctx.program_id,
pda_seeds,
settings::USER_PDA_SPACE as u64,
)?;
write_to_pda(&ctx.accounts.user_pda, &padded)?;
let total_fee = economy
.registration_fee_lamports
.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?)
.ok_or(error!(ErrCode::MathOverflow))?;
transfer_lamports(
&ctx.accounts.signer,
&ctx.accounts.inflow_vault,
&ctx.accounts.system_program.to_account_info(),
total_fee,
)?;
Ok(())
}
fn classify_login_or_fail<'info>(
login_guard_program: &AccountInfo<'info>,
signer: &AccountInfo<'info>,
login: &str,
) -> Result<()> {
let cpi_ctx = CpiContext::new(
login_guard_program.clone(),
shine_login_guard::cpi::accounts::ClassifyLogin {
signer: signer.to_account_info(),
},
);
shine_login_guard::cpi::classify_login(cpi_ctx, login.to_string())?;
let (program_id, raw) = get_return_data().ok_or(error!(ErrCode::InvalidLoginGuardResponse))?;
require_keys_eq!(
program_id,
*login_guard_program.key,
ErrCode::InvalidLoginGuardResponse
);
require!(raw.len() == 4, ErrCode::InvalidLoginGuardResponse);
let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
match class {
0 => Ok(()),
1 => Err(error!(ErrCode::PremiumLogin)),
2 => Err(error!(ErrCode::TrademarkLoginRequiresReview)),
_ => Err(error!(ErrCode::InvalidLoginGuardResponse)),
}
}
pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) -> Result<()> {
validate_login(&args.login)?;
validate_fields(&args.fields)?;
validate_inflow_vault(&ctx.accounts.inflow_vault)?;
require!(
args.additional_limit % settings::LIMIT_STEP == 0,
ErrCode::InvalidLimitIncrement
);
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let login_seed = login_seed_normalized(&args.login);
let (expected_pda, _) = find_user_pda(ctx.program_id, &login_seed);
require_keys_eq!(
expected_pda,
ctx.accounts.user_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.user_pda.owner == ctx.program_id,
ErrCode::InvalidPdaAddress
);
let raw = safe_read_pda(&ctx.accounts.user_pda);
require!(!raw.is_empty(), ErrCode::EmptyPdaData);
let old_record = deserialize_record_from_pda(&raw)?;
require!(
old_record.login == args.login,
ErrCode::ImmutableFieldChanged
);
require!(
old_record.created_at_ms == args.created_at_ms,
ErrCode::ImmutableFieldChanged
);
require_keys_eq!(
old_record.root_key,
args.root_key,
ErrCode::ImmutableFieldChanged
);
require!(
old_record.root_key_status == KEY_STATUS_CREATED
&& old_record.blockchain_key_status == KEY_STATUS_CREATED
&& old_record.device_key_status == KEY_STATUS_CREATED,
ErrCode::InvalidRecordData
);
require!(
args.version == old_record.version.saturating_add(1),
ErrCode::InvalidVersion
);
let expected_prev_hash = hash_unsigned_record(&old_record)?;
let provided_prev_hash = vec_to_hash32(&args.prev_hash)?;
require!(
expected_prev_hash == provided_prev_hash,
ErrCode::InvalidPrevHash
);
let new_balance = old_record
.balance
.checked_add(args.additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?;
require!(new_balance >= old_record.balance, ErrCode::BalanceDecrease);
let mut new_record = UserRecord {
created_at_ms: old_record.created_at_ms,
updated_at_ms: args.updated_at_ms,
version: args.version,
prev_hash: provided_prev_hash,
login: old_record.login.clone(),
root_key_status: old_record.root_key_status,
root_key: old_record.root_key,
blockchain_key_status: old_record.blockchain_key_status,
blockchain_key: args.fields.blockchain_key,
device_key_status: old_record.device_key_status,
device_key: args.fields.device_key,
chain_number: args.fields.chain_number,
balance: new_balance,
is_server: args.fields.is_server,
server_key: args.fields.server_key,
server_address: args.fields.server_address.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
trusted_count: args.fields.trusted_count,
signature: [0; 64],
};
let unsigned = serialize_unsigned_record(&new_record)?;
new_record.signature = verify_record_signature(
&ctx.accounts.instructions,
&new_record.root_key,
&args.signature,
&unsigned,
)?;
let serialized = serialize_full_record(&new_record)?;
ensure_pda_size_and_rent(
&ctx.accounts.user_pda,
&ctx.accounts.signer,
&ctx.accounts.system_program.to_account_info(),
serialized.len(),
)?;
write_to_pda(&ctx.accounts.user_pda, &serialized)?;
let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?;
if topup_fee > 0 {
transfer_lamports(
&ctx.accounts.signer,
&ctx.accounts.inflow_vault,
&ctx.accounts.system_program.to_account_info(),
topup_fee,
)?;
}
Ok(())
}
fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
let login_bytes = record.login.as_bytes();
require!(login_bytes.len() <= u8::MAX as usize, ErrCode::InvalidLogin);
let server_address_bytes = record.server_address.as_bytes();
require!(
server_address_bytes.len() <= u8::MAX as usize,
ErrCode::InvalidRecordData
);
require!(record.access_servers.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
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.version.to_le_bytes());
out.extend_from_slice(&record.prev_hash);
out.push(login_bytes.len() as u8);
out.extend_from_slice(login_bytes);
out.push(record.root_key_status);
out.extend_from_slice(record.root_key.as_ref());
out.push(record.blockchain_key_status);
out.extend_from_slice(record.blockchain_key.as_ref());
out.push(record.device_key_status);
out.extend_from_slice(record.device_key.as_ref());
out.extend_from_slice(&record.chain_number.to_le_bytes());
out.extend_from_slice(&record.balance.to_le_bytes());
out.push(if record.is_server { 1 } else { 0 });
if record.is_server {
out.extend_from_slice(record.server_key.as_ref());
out.push(server_address_bytes.len() as u8);
out.extend_from_slice(server_address_bytes);
require!(
record.sync_servers.len() <= MAX_SYNC_SERVERS,
ErrCode::InvalidRecordData
);
out.push(record.sync_servers.len() as u8);
for login in &record.sync_servers {
let bytes = login.as_bytes();
require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
out.push(bytes.len() as u8);
out.extend_from_slice(bytes);
}
}
out.push(record.access_servers.len() as u8);
for login in &record.access_servers {
let bytes = login.as_bytes();
require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData);
out.push(bytes.len() as u8);
out.extend_from_slice(bytes);
}
out.push(record.trusted_count);
out.extend_from_slice(&RESERVED_BYTES);
let record_len = out
.len()
.checked_add(64)
.ok_or(error!(ErrCode::MathOverflow))?;
require!(record_len <= u16::MAX as usize, ErrCode::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>> {
let mut out = serialize_unsigned_record(record)?;
out.extend_from_slice(&record.signature);
Ok(out)
}
fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
require!(raw.len() >= 9, ErrCode::InvalidRecordData);
require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic);
require!(
raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR,
ErrCode::InvalidRecordFormat
);
let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize;
require!(record_len >= 9 + 64, ErrCode::InvalidRecordLength);
require!(record_len <= raw.len(), ErrCode::InvalidRecordLength);
let useful = &raw[..record_len];
let mut cursor = 9usize;
let created_at_ms = read_u64(useful, &mut cursor)?;
let updated_at_ms = read_u64(useful, &mut cursor)?;
let version = read_u32(useful, &mut cursor)?;
let prev_hash = read_fixed_32(useful, &mut cursor)?;
let login = read_len_prefixed_string(useful, &mut cursor)?;
let root_key_status = read_u8(useful, &mut cursor)?;
let root_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
let blockchain_key_status = read_u8(useful, &mut cursor)?;
let blockchain_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
let device_key_status = read_u8(useful, &mut cursor)?;
let device_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?);
let chain_number = read_u16(useful, &mut cursor)?;
let balance = read_u64(useful, &mut cursor)?;
let is_server = read_u8(useful, &mut cursor)? == 1;
let (server_key, server_address) = if is_server {
(
Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?),
read_len_prefixed_string(useful, &mut cursor)?,
)
} else {
(Pubkey::default(), String::new())
};
let sync_servers = if is_server {
let sync_count = read_u8(useful, &mut cursor)? as usize;
require!(sync_count <= MAX_SYNC_SERVERS, ErrCode::InvalidRecordData);
let mut out = Vec::with_capacity(sync_count);
for _ in 0..sync_count {
out.push(read_len_prefixed_string(useful, &mut cursor)?);
}
out
} else {
Vec::new()
};
let access_count = read_u8(useful, &mut cursor)? as usize;
let mut access_servers = Vec::with_capacity(access_count);
for _ in 0..access_count {
access_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
}
let trusted_count = read_u8(useful, &mut cursor)?;
require!(
useful.get(cursor..cursor + 5) == Some(&RESERVED_BYTES),
ErrCode::InvalidRecordData
);
cursor += 5;
let signature = read_fixed_64(useful, &mut cursor)?;
require!(cursor == useful.len(), ErrCode::InvalidRecordLength);
Ok(UserRecord {
created_at_ms,
updated_at_ms,
version,
prev_hash,
login,
root_key_status,
root_key,
blockchain_key_status,
blockchain_key,
device_key_status,
device_key,
chain_number,
balance,
is_server,
server_key,
server_address,
sync_servers,
access_servers,
trusted_count,
signature,
})
}
fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> {
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 verify_record_signature(
instructions_sysvar: &AccountInfo,
root_key: &Pubkey,
signature: &[u8],
unsigned: &[u8],
) -> Result<[u8; 64]> {
require_keys_eq!(
*instructions_sysvar.key,
anchor_lang::solana_program::sysvar::instructions::id(),
ErrCode::InvalidSignature
);
let provided_sig = vec_to_signature(signature)?;
let msg_hash = hashv(&[unsigned]);
let current_ix_index = load_current_index_checked(instructions_sysvar)
.map_err(|_| error!(ErrCode::InvalidSignature))?;
require!(current_ix_index > 0, ErrCode::InvalidSignature);
let ed_ix = load_instruction_at_checked((current_ix_index - 1) as usize, instructions_sysvar)
.map_err(|_| error!(ErrCode::InvalidSignature))?;
let parsed = parse_ed25519_ix(&ed_ix)?;
require_keys_eq!(parsed.pubkey, *root_key, ErrCode::InvalidSignature);
require!(
parsed.message == msg_hash.as_ref(),
ErrCode::InvalidSignature
);
require!(parsed.signature == provided_sig, ErrCode::InvalidSignature);
Ok(parsed.signature)
}
struct ParsedEd25519 {
pub pubkey: Pubkey,
pub signature: [u8; 64],
pub message: Vec<u8>,
}
fn parse_ed25519_ix(ix: &Instruction) -> Result<ParsedEd25519> {
require_keys_eq!(
ix.program_id,
ed25519_program::id(),
ErrCode::InvalidSignature
);
let data = &ix.data;
require!(data.len() >= 16, ErrCode::InvalidSignature);
require!(data[0] == 1, ErrCode::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, ErrCode::InvalidSignature);
require!(pubkey_ix_index == u16::MAX, ErrCode::InvalidSignature);
require!(message_ix_index == u16::MAX, ErrCode::InvalidSignature);
let signature_end = signature_offset
.checked_add(64)
.ok_or(error!(ErrCode::InvalidSignature))?;
let pubkey_end = pubkey_offset
.checked_add(32)
.ok_or(error!(ErrCode::InvalidSignature))?;
let message_end = message_offset
.checked_add(message_size)
.ok_or(error!(ErrCode::InvalidSignature))?;
let signature_slice = data
.get(signature_offset..signature_end)
.ok_or(error!(ErrCode::InvalidSignature))?;
let pubkey_slice = data
.get(pubkey_offset..pubkey_end)
.ok_or(error!(ErrCode::InvalidSignature))?;
let message = data
.get(message_offset..message_end)
.ok_or(error!(ErrCode::InvalidSignature))?
.to_vec();
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(|_| error!(ErrCode::InvalidSignature))?,
);
Ok(ParsedEd25519 {
pubkey,
signature,
message,
})
}
fn le_u16(data: &[u8], offset: usize) -> Result<u16> {
let end = offset
.checked_add(2)
.ok_or(error!(ErrCode::InvalidSignature))?;
let s = data
.get(offset..end)
.ok_or(error!(ErrCode::InvalidSignature))?;
Ok(u16::from_le_bytes([s[0], s[1]]))
}
fn validate_login(login: &str) -> Result<()> {
require!(!login.is_empty(), ErrCode::InvalidLogin);
require!(login.len() <= 20, ErrCode::InvalidLogin);
for ch in login.chars() {
if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') {
return Err(error!(ErrCode::InvalidLogin));
}
}
Ok(())
}
fn login_seed_normalized(login: &str) -> String {
login.to_ascii_lowercase()
}
fn validate_fields(fields: &UserMutableFields) -> Result<()> {
if fields.is_server {
require!(
!fields.server_address.is_empty(),
ErrCode::InvalidRecordData
);
require!(
fields.server_address.as_bytes().len() <= u8::MAX as usize,
ErrCode::InvalidRecordData
);
require!(
fields.sync_servers.len() <= MAX_SYNC_SERVERS,
ErrCode::InvalidRecordData
);
for login in &fields.sync_servers {
require!(!login.is_empty(), ErrCode::InvalidRecordData);
require!(
login.as_bytes().len() <= u8::MAX as usize,
ErrCode::InvalidRecordData
);
}
} else {
require!(fields.server_address.is_empty(), ErrCode::InvalidRecordData);
require!(fields.sync_servers.is_empty(), ErrCode::InvalidRecordData);
}
require!(
fields.access_servers.len() <= u8::MAX as usize,
ErrCode::InvalidRecordData
);
for login in &fields.access_servers {
require!(!login.is_empty(), ErrCode::InvalidRecordData);
require!(
login.as_bytes().len() <= u8::MAX as usize,
ErrCode::InvalidRecordData
);
}
Ok(())
}
fn validate_inflow_vault(inflow_vault: &AccountInfo) -> Result<()> {
let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID)
.map_err(|_| error!(ErrCode::InvalidFeeReceiver))?;
let (expected, _) = Pubkey::find_program_address(
&[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED],
&payments_program_id,
);
require_keys_eq!(expected, *inflow_vault.key, ErrCode::InvalidFeeReceiver);
Ok(())
}
fn transfer_lamports<'info>(
payer: &AccountInfo<'info>,
recipient: &AccountInfo<'info>,
system_program: &AccountInfo<'info>,
lamports: u64,
) -> Result<()> {
if lamports == 0 {
return Ok(());
}
let ix = system_instruction::transfer(payer.key, recipient.key, lamports);
invoke(
&ix,
&[payer.clone(), recipient.clone(), system_program.clone()],
)?;
Ok(())
}
fn ensure_pda_size_and_rent<'info>(
pda: &AccountInfo<'info>,
payer: &AccountInfo<'info>,
system_program: &AccountInfo<'info>,
required_len: usize,
) -> Result<()> {
let current_len = pda.data_len();
if required_len <= current_len {
return Ok(());
}
let increase = required_len
.checked_sub(current_len)
.ok_or(error!(ErrCode::MathOverflow))?;
require!(increase <= MAX_AUTO_REALLOC_INCREASE, ErrCode::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, top_up)?;
}
pda.realloc(required_len, false)?;
Ok(())
}
fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result<u64> {
let units = limit_delta / settings::LIMIT_STEP;
units
.checked_mul(lamports_per_limit_step)
.ok_or(error!(ErrCode::MathOverflow))
}
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 read_users_economy_config(pda: &AccountInfo) -> Result<UsersEconomyConfigState> {
let raw = safe_read_pda(pda);
require!(!raw.is_empty(), ErrCode::EmptyPdaData);
let mut slice: &[u8] = &raw;
UsersEconomyConfigState::deserialize(&mut slice)
.map_err(|_| error!(ErrCode::DeserializationError))
}
fn pad_to_fixed_size(mut bytes: Vec<u8>, target_size: usize) -> Result<Vec<u8>> {
require!(bytes.len() <= target_size, ErrCode::RecordTooLarge);
bytes.resize(target_size, 0);
Ok(bytes)
}
fn vec_to_signature(input: &[u8]) -> Result<[u8; 64]> {
require!(input.len() == 64, ErrCode::InvalidSignature);
let mut out = [0u8; 64];
out.copy_from_slice(input);
Ok(out)
}
fn vec_to_hash32(input: &[u8]) -> Result<[u8; 32]> {
require!(input.len() == 32, ErrCode::InvalidPrevHash);
let mut out = [0u8; 32];
out.copy_from_slice(input);
Ok(out)
}
fn read_u8(data: &[u8], cursor: &mut usize) -> Result<u8> {
let v = *data
.get(*cursor)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor += 1;
Ok(v)
}
fn read_u16(data: &[u8], cursor: &mut usize) -> Result<u16> {
let end = cursor
.checked_add(2)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
Ok(u16::from_le_bytes([slice[0], slice[1]]))
}
fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32> {
let end = cursor
.checked_add(4)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
}
fn read_u64(data: &[u8], cursor: &mut usize) -> Result<u64> {
let end = cursor
.checked_add(8)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
Ok(u64::from_le_bytes([
slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
]))
}
fn read_fixed_32(data: &[u8], cursor: &mut usize) -> Result<[u8; 32]> {
let end = cursor
.checked_add(32)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
let mut out = [0u8; 32];
out.copy_from_slice(slice);
Ok(out)
}
fn read_fixed_64(data: &[u8], cursor: &mut usize) -> Result<[u8; 64]> {
let end = cursor
.checked_add(64)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
let mut out = [0u8; 64];
out.copy_from_slice(slice);
Ok(out)
}
fn read_len_prefixed_string(data: &[u8], cursor: &mut usize) -> Result<String> {
let len = read_u8(data, cursor)? as usize;
let end = cursor
.checked_add(len)
.ok_or(error!(ErrCode::InvalidRecordData))?;
let slice = data
.get(*cursor..end)
.ok_or(error!(ErrCode::InvalidRecordData))?;
*cursor = end;
let value = std::str::from_utf8(slice).map_err(|_| error!(ErrCode::InvalidRecordData))?;
Ok(value.to_string())
}