879 lines
29 KiB
Rust
879 lines
29 KiB
Rust
use crate::settings;
|
||
use anchor_lang::prelude::*;
|
||
use anchor_lang::solana_program::{
|
||
ed25519_program,
|
||
hash::hashv,
|
||
instruction::Instruction,
|
||
program::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 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 connection_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 connection_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>,
|
||
}
|
||
|
||
#[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)?;
|
||
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 (expected_pda, bump) = find_user_pda(ctx.program_id, &args.login);
|
||
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(),
|
||
connection_servers: args.fields.connection_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(),
|
||
args.login.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(())
|
||
}
|
||
|
||
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 (expected_pda, _) = find_user_pda(ctx.program_id, &args.login);
|
||
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(),
|
||
connection_servers: args.fields.connection_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)?;
|
||
require!(
|
||
serialized.len() <= settings::USER_PDA_SPACE,
|
||
ErrCode::RecordTooLarge
|
||
);
|
||
let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?;
|
||
write_to_pda(&ctx.accounts.user_pda, &padded)?;
|
||
|
||
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.connection_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);
|
||
}
|
||
|
||
out.push(record.connection_servers.len() as u8);
|
||
for login in &record.connection_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 connections_count = read_u8(useful, &mut cursor)? as usize;
|
||
let mut connection_servers = Vec::with_capacity(connections_count);
|
||
for _ in 0..connections_count {
|
||
connection_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,
|
||
connection_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() <= 25, ErrCode::InvalidLogin);
|
||
for ch in login.chars() {
|
||
if !(ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_') {
|
||
return Err(error!(ErrCode::InvalidLogin));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
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
|
||
);
|
||
} else {
|
||
require!(fields.server_address.is_empty(), ErrCode::InvalidRecordData);
|
||
}
|
||
require!(
|
||
fields.connection_servers.len() <= u8::MAX as usize,
|
||
ErrCode::InvalidRecordData
|
||
);
|
||
for login in &fields.connection_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 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())
|
||
}
|