use crate::settings; use anchor_lang::prelude::*; use anchor_lang::solana_program::{ ed25519_program, hash::hashv, program::{get_return_data, invoke}, system_instruction, sysvar::instructions::get_instruction_relative, }; 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 MAX_SYNC_SERVERS: usize = 32; 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_TRUSTED_STATE: u8 = 50; const BLOCK_VERSION_0: u8 = 0; const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1; const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK"; #[derive(AnchorSerialize, AnchorDeserialize, 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: Vec, pub last_block_signature: Vec, 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, pub access_servers: Vec, 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, } #[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, pub additional_limit: u64, pub fields: UserMutableFields, pub signature: Vec, } 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, pub access_servers: Vec, pub trusted_count: u8, pub signature: [u8; 64], } 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(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) -> 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, 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, 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, record_number: 0, prev_record_hash: ZERO_HASH, login: args.login.clone(), 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.clone(), 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: vec_to_hash32(&args.fields.last_block_hash)?, last_block_signature: vec_to_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(), trusted_count: args.fields.trusted_count, signature: [0; 64], }; validate_blockchain_limits(&record.blockchain, 0, 0, true)?; verify_last_block_state_signature(&ctx.accounts.instructions, &record)?; 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, 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.as_slice())?; drop(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!( args.version == old_record.record_number.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 .blockchain .paid_limit_bytes .checked_add(args.additional_limit) .ok_or(error!(ErrCode::MathOverflow))?; require!( new_balance >= old_record.blockchain.paid_limit_bytes, ErrCode::BalanceDecrease ); let old_used_bytes = old_record.blockchain.used_bytes; let old_last_block_number = old_record.blockchain.last_block_number; 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.as_slice() == args.fields.last_block_hash.as_slice() && old_record.blockchain.last_block_signature.as_slice() == args.fields.last_block_signature.as_slice() && old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id; let mut new_record = UserRecord { created_at_ms: old_record.created_at_ms, updated_at_ms: args.updated_at_ms, record_number: args.version, prev_record_hash: provided_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: vec_to_hash32(&args.fields.last_block_hash)?, last_block_signature: vec_to_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(), trusted_count: args.fields.trusted_count, signature: [0; 64], }; require!( new_record.blockchain.blockchain_type == old_record.blockchain.blockchain_type && new_record.blockchain.blockchain_name == old_record.blockchain.blockchain_name && new_record.blockchain.blockchain_public_key == old_record.blockchain.blockchain_public_key, ErrCode::ImmutableFieldChanged ); validate_blockchain_limits( &new_record.blockchain, old_used_bytes, old_last_block_number, false, )?; drop(old_record); if !blockchain_state_unchanged { verify_last_block_state_signature(&ctx.accounts.instructions, &new_record)?; } let unsigned = serialize_unsigned_record(&new_record)?; new_record.signature = verify_record_signature( &ctx.accounts.instructions, &new_record.root_key, &args.signature, &unsigned, )?; drop(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> { let login_bytes = record.login.as_bytes(); require!(login_bytes.len() <= u8::MAX as usize, ErrCode::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 { 6 } else { 5 }; 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_trusted_state_block(&mut out, record); 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> { let mut out = serialize_unsigned_record(record)?; out.extend_from_slice(&record.signature); Ok(out) } fn write_root_key_block(out: &mut Vec, 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, 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, blockchain: &BlockchainRecord) -> Result<()> { out.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY); out.push(BLOCK_VERSION_0); out.push(1); write_blockchain_record(out, blockchain)?; Ok(()) } fn write_blockchain_record(out: &mut Vec, blockchain: &BlockchainRecord) -> Result<()> { 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, record: &UserRecord) -> Result<()> { 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, ErrCode::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, record: &UserRecord) -> Result<()> { out.push(BLOCK_TYPE_ACCESS_SERVERS); out.push(BLOCK_VERSION_0); require!( record.access_servers.len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); out.push(record.access_servers.len() as u8); for login in &record.access_servers { write_len_prefixed_string(out, login)?; } Ok(()) } fn write_trusted_state_block(out: &mut Vec, 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, value: &str) -> Result<()> { let bytes = value.as_bytes(); require!(bytes.len() <= u8::MAX as usize, ErrCode::InvalidRecordData); out.push(bytes.len() as u8); out.extend_from_slice(bytes); Ok(()) } fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result { let blockchain_type = read_u8(data, cursor)?; require!( blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ErrCode::InvalidRecordData ); let blockchain_name = read_len_prefixed_string(data, cursor)?; let blockchain_public_key = Pubkey::new_from_array(read_fixed_32(data, cursor)?); let paid_limit_bytes = read_u64(data, cursor)?; let used_bytes = read_u64(data, cursor)?; let last_block_number = read_u32(data, cursor)?; let last_block_hash = read_fixed_32(data, cursor)?; let last_block_signature = read_fixed_64(data, cursor)?; let arweave_present = read_u8(data, cursor)?; let arweave_tx_id = match arweave_present { 0 => String::new(), 1 => read_len_prefixed_string(data, cursor)?, _ => return Err(error!(ErrCode::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 deserialize_record_from_pda(raw: &[u8]) -> Result { 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 record_number = read_u32(useful, &mut cursor)?; let prev_record_hash = read_fixed_32(useful, &mut cursor)?; let login = read_len_prefixed_string(useful, &mut cursor)?; let blocks_count = read_u8(useful, &mut cursor)? as usize; let mut root_key: Option = None; let mut device_key: Option = None; let mut blockchain: Option = 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 trusted_count = 0u8; for _ in 0..blocks_count { let block_type = read_u8(useful, &mut cursor)?; let block_version = read_u8(useful, &mut cursor)?; require!( block_version == BLOCK_VERSION_0, ErrCode::InvalidRecordFormat ); match block_type { BLOCK_TYPE_ROOT_KEY => { require!(root_key.is_none(), ErrCode::InvalidRecordData); root_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?)); } BLOCK_TYPE_DEVICE_KEY => { require!(device_key.is_none(), ErrCode::InvalidRecordData); device_key = Some(Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?)); } BLOCK_TYPE_BLOCKCHAIN_REGISTRY => { require!(blockchain.is_none(), ErrCode::InvalidRecordData); let count = read_u8(useful, &mut cursor)?; require!(count == 1, ErrCode::InvalidRecordData); blockchain = Some(read_blockchain_record(useful, &mut cursor)?); } BLOCK_TYPE_SERVER_PROFILE => { require!(!is_server, ErrCode::InvalidRecordData); is_server = read_u8(useful, &mut cursor)? == 1; require!(is_server, ErrCode::InvalidRecordData); address_format_type = read_u8(useful, &mut cursor)?; address_format_version = read_u8(useful, &mut cursor)?; server_address = read_len_prefixed_string(useful, &mut cursor)?; let sync_count = read_u8(useful, &mut cursor)? as usize; require!(sync_count <= MAX_SYNC_SERVERS, ErrCode::InvalidRecordData); for _ in 0..sync_count { sync_servers.push(read_len_prefixed_string(useful, &mut cursor)?); } } BLOCK_TYPE_ACCESS_SERVERS => { require!(access_servers.is_empty(), ErrCode::InvalidRecordData); let access_count = read_u8(useful, &mut cursor)? as usize; for _ in 0..access_count { access_servers.push(read_len_prefixed_string(useful, &mut cursor)?); } } BLOCK_TYPE_TRUSTED_STATE => { trusted_count = read_u8(useful, &mut cursor)?; } _ => return Err(error!(ErrCode::InvalidRecordFormat)), } } let signature = read_fixed_64(useful, &mut cursor)?; require!(cursor == useful.len(), ErrCode::InvalidRecordLength); Ok(UserRecord { created_at_ms, updated_at_ms, record_number, prev_record_hash, login, root_key: root_key.ok_or(error!(ErrCode::InvalidRecordData))?, device_key: device_key.ok_or(error!(ErrCode::InvalidRecordData))?, blockchain: blockchain.ok_or(error!(ErrCode::InvalidRecordData))?, is_server, address_format_type, address_format_version, 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]> { let provided_sig = vec_to_signature(signature)?; let msg_hash = hashv(&[unsigned]); verify_ed25519_signature_instruction( instructions_sysvar, -2, root_key, &provided_sig, msg_hash.as_ref(), )?; Ok(provided_sig) } fn verify_last_block_state_signature( instructions_sysvar: &AccountInfo, record: &UserRecord, ) -> Result<()> { 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 verify_ed25519_signature_instruction( instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_signature: &[u8; 64], expected_message: &[u8], ) -> Result<()> { require_keys_eq!( *instructions_sysvar.key, anchor_lang::solana_program::sysvar::instructions::id(), ErrCode::InvalidSignature ); let ed_ix = get_instruction_relative(index_relative_to_current, instructions_sysvar) .map_err(|_| error!(ErrCode::InvalidSignature))?; require_keys_eq!(ed_ix.program_id, ed25519_program::id(), ErrCode::InvalidSignature); let parsed = parse_ed25519_ix(ed_ix.data.as_slice())?; require!(parsed.pubkey == *expected_pubkey, ErrCode::InvalidSignature); require!(parsed.signature == *expected_signature, ErrCode::InvalidSignature); require!(parsed.message == expected_message, ErrCode::InvalidSignature); Ok(()) } fn serialize_last_block_state(record: &UserRecord) -> Result> { 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 parse_ed25519_ix<'a>(data: &'a [u8]) -> Result> { 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))?; 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(ParsedEd25519Ref { pubkey, signature, message, }) } fn le_u16(data: &[u8], offset: usize) -> Result { 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<()> { require!( !fields.blockchain_name.is_empty(), ErrCode::InvalidRecordData ); require!( fields.blockchain_name.as_bytes().len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); require!( fields.last_block_hash.len() == 32 && fields.last_block_signature.len() == 64, ErrCode::InvalidRecordData ); require!( fields.arweave_tx_id.as_bytes().len() <= u8::MAX as usize, ErrCode::InvalidRecordData ); 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_blockchain_limits( blockchain: &BlockchainRecord, old_used_bytes: u64, old_last_block_number: u32, is_create: bool, ) -> Result<()> { require!( blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ErrCode::InvalidRecordData ); require!( blockchain.used_bytes <= blockchain.paid_limit_bytes, ErrCode::InvalidRecordData ); if !is_create { require!( blockchain.used_bytes >= old_used_bytes && blockchain.last_block_number >= old_last_block_number, 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 { 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 { 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, target_size: usize) -> Result> { 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 { let v = *data .get(*cursor) .ok_or(error!(ErrCode::InvalidRecordData))?; *cursor += 1; Ok(v) } fn read_u32(data: &[u8], cursor: &mut usize) -> Result { 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 { 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 { 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()) }