SHiNE-server/shine-solana/shine/programs/shine_users/src/users.rs

1212 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

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

use 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<u8>,
pub last_block_signature: Vec<u8>,
pub arweave_tx_id: String,
pub is_server: bool,
pub address_format_type: u8,
pub address_format_version: u8,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub 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 record_number: u32,
pub prev_record_hash: [u8; 32],
pub login: String,
pub root_key: Pubkey,
pub device_key: Pubkey,
pub blockchain: BlockchainRecord,
pub is_server: bool,
pub address_format_type: u8,
pub address_format_version: u8,
pub server_address: String,
pub sync_servers: Vec<String>,
pub access_servers: Vec<String>,
pub 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<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,
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<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.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<Vec<u8>> {
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<Vec<u8>> {
let mut out = serialize_unsigned_record(record)?;
out.extend_from_slice(&record.signature);
Ok(out)
}
fn write_root_key_block(out: &mut Vec<u8>, record: &UserRecord) {
out.push(BLOCK_TYPE_ROOT_KEY);
out.push(BLOCK_VERSION_0);
out.extend_from_slice(record.root_key.as_ref());
}
fn write_device_key_block(out: &mut Vec<u8>, record: &UserRecord) {
out.push(BLOCK_TYPE_DEVICE_KEY);
out.push(BLOCK_VERSION_0);
out.extend_from_slice(record.device_key.as_ref());
}
fn write_blockchain_registry_block(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<()> {
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<u8>, 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<u8>, 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<u8>, 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<u8>, record: &UserRecord) {
out.push(BLOCK_TYPE_TRUSTED_STATE);
out.push(BLOCK_VERSION_0);
out.push(record.trusted_count);
}
fn write_len_prefixed_string(out: &mut Vec<u8>, value: &str) -> Result<()> {
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<BlockchainRecord> {
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<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 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<Pubkey> = None;
let mut device_key: Option<Pubkey> = None;
let mut blockchain: Option<BlockchainRecord> = 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<Vec<u8>> {
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<ParsedEd25519Ref<'a>> {
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<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<()> {
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<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_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())
}