1045 lines
53 KiB
Rust
1045 lines
53 KiB
Rust
use solana_program::{
|
|
account_info::{next_account_info, AccountInfo},
|
|
entrypoint,
|
|
entrypoint::ProgramResult,
|
|
hash::hashv,
|
|
instruction::Instruction,
|
|
program::{get_return_data, invoke, invoke_signed},
|
|
program_error::ProgramError,
|
|
program_memory::sol_memcmp,
|
|
pubkey::Pubkey,
|
|
rent::Rent,
|
|
system_instruction,
|
|
system_program,
|
|
sysvar::{instructions::{load_current_index_checked, load_instruction_at_checked}, Sysvar},
|
|
};
|
|
use std::{convert::TryFrom, str::FromStr};
|
|
|
|
pub mod settings;
|
|
|
|
solana_program::declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
|
|
entrypoint!(process_instruction);
|
|
|
|
const MAGIC: &[u8; 5] = b"SHiNE";
|
|
const FORMAT_MAJOR: u8 = 1;
|
|
const FORMAT_MINOR: u8 = 0;
|
|
const MAX_SYNC_SERVERS: usize = 32;
|
|
const MAX_SESSIONS: usize = 64;
|
|
const MAX_SESSION_NAME_LEN: usize = 64;
|
|
const MAX_AUTO_REALLOC_INCREASE: usize = 10_000;
|
|
const ZERO_HASH: [u8; 32] = [0; 32];
|
|
const BLOCK_TYPE_ROOT_KEY: u8 = 1;
|
|
const BLOCK_TYPE_DEVICE_KEY: u8 = 2;
|
|
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY: u8 = 3;
|
|
const BLOCK_TYPE_SERVER_PROFILE: u8 = 30;
|
|
const BLOCK_TYPE_ACCESS_SERVERS: u8 = 40;
|
|
const BLOCK_TYPE_SESSIONS: u8 = 50;
|
|
const BLOCK_TYPE_TRUSTED_STATE: u8 = 70;
|
|
const BLOCK_VERSION_0: u8 = 0;
|
|
const BLOCKCHAIN_TYPE_MAIN_USER: u8 = 1;
|
|
const SESSIONS_MODE_MIXED: u8 = 1;
|
|
const SESSIONS_MODE_PDA_ONLY: u8 = 10;
|
|
const SESSION_TYPE_USER: u8 = 1;
|
|
const SESSION_TYPE_SUBSERVER: u8 = 100;
|
|
const LAST_BLOCK_STATE_PREFIX: &[u8] = b"SHiNE_LAST_BLOCK";
|
|
|
|
const IX_INIT_USERS_ECONOMY_CONFIG: u8 = 1;
|
|
const IX_UPDATE_USERS_ECONOMY_CONFIG: u8 = 2;
|
|
const IX_CREATE_USER_PDA: u8 = 3;
|
|
const IX_UPDATE_USER_PDA: u8 = 4;
|
|
const LOGIN_GUARD_IX_CLASSIFY_LOGIN: u8 = 1;
|
|
|
|
#[repr(u32)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum ShineUsersError {
|
|
InvalidInstruction = 1,
|
|
InvalidSigner = 2,
|
|
InvalidPdaAddress = 3,
|
|
UserAlreadyExists = 4,
|
|
EmptyPdaData = 5,
|
|
InvalidRecordData = 6,
|
|
InvalidRecordMagic = 7,
|
|
InvalidRecordFormat = 8,
|
|
InvalidRecordLength = 9,
|
|
InvalidLogin = 10,
|
|
InvalidLimitIncrement = 11,
|
|
InvalidFeeReceiver = 12,
|
|
InvalidLoginGuardResponse = 13,
|
|
PremiumLogin = 14,
|
|
TrademarkLoginRequiresReview = 15,
|
|
InvalidVersion = 16,
|
|
InvalidPrevHash = 17,
|
|
ImmutableFieldChanged = 18,
|
|
BalanceDecrease = 19,
|
|
InvalidSignature = 20,
|
|
RecordTooLarge = 21,
|
|
MathOverflow = 22,
|
|
SystemAlreadyInitialized = 23,
|
|
MissingRequiredSignature = 24,
|
|
InvalidSystemProgram = 25,
|
|
InvalidAccountOwner = 26,
|
|
InvalidAccountData = 27,
|
|
}
|
|
|
|
impl From<ShineUsersError> for ProgramError {
|
|
fn from(value: ShineUsersError) -> Self {
|
|
ProgramError::Custom(value as u32)
|
|
}
|
|
}
|
|
|
|
macro_rules! require {
|
|
($cond:expr, $err:expr) => {
|
|
if !($cond) {
|
|
return Err(ProgramError::from($err));
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! require_keys_eq {
|
|
($left:expr, $right:expr, $err:expr) => {
|
|
if $left != $right {
|
|
return Err(ProgramError::from($err));
|
|
}
|
|
};
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SessionRecord {
|
|
pub session_type: u8,
|
|
pub session_version: u8,
|
|
pub session_name: String,
|
|
pub session_pub_key: Pubkey,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UserMutableFields {
|
|
pub device_key: Pubkey,
|
|
pub blockchain_public_key: Pubkey,
|
|
pub blockchain_name: String,
|
|
pub used_bytes: u64,
|
|
pub last_block_number: u32,
|
|
pub last_block_hash: [u8; 32],
|
|
pub last_block_signature: [u8; 64],
|
|
pub arweave_tx_id: String,
|
|
pub is_server: bool,
|
|
pub address_format_type: u8,
|
|
pub address_format_version: u8,
|
|
pub server_address: String,
|
|
pub sync_servers: Vec<String>,
|
|
pub access_servers: Vec<String>,
|
|
pub sessions_mode: u8,
|
|
pub sessions: Vec<SessionRecord>,
|
|
pub trusted_count: u8,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CreateUserPdaArgs {
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub created_at_ms: u64,
|
|
pub additional_limit: u64,
|
|
pub fields: UserMutableFields,
|
|
pub signature: [u8; 64],
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UpdateUserPdaArgs {
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub created_at_ms: u64,
|
|
pub updated_at_ms: u64,
|
|
pub version: u32,
|
|
pub prev_hash: [u8; 32],
|
|
pub additional_limit: u64,
|
|
pub fields: UserMutableFields,
|
|
pub signature: [u8; 64],
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UpdateUsersEconomyConfigArgs {
|
|
pub registration_fee_lamports: u64,
|
|
pub lamports_per_limit_step: u64,
|
|
pub start_bonus_limit: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UsersEconomyConfigState {
|
|
pub version: u8,
|
|
pub registration_fee_lamports: u64,
|
|
pub lamports_per_limit_step: u64,
|
|
pub start_bonus_limit: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct BlockchainRecord {
|
|
pub blockchain_type: u8,
|
|
pub blockchain_name: String,
|
|
pub blockchain_public_key: Pubkey,
|
|
pub paid_limit_bytes: u64,
|
|
pub used_bytes: u64,
|
|
pub last_block_number: u32,
|
|
pub last_block_hash: [u8; 32],
|
|
pub last_block_signature: [u8; 64],
|
|
pub arweave_tx_id: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UserRecord {
|
|
pub created_at_ms: u64,
|
|
pub updated_at_ms: u64,
|
|
pub record_number: u32,
|
|
pub prev_record_hash: [u8; 32],
|
|
pub login: String,
|
|
pub root_key: Pubkey,
|
|
pub device_key: Pubkey,
|
|
pub blockchain: BlockchainRecord,
|
|
pub is_server: bool,
|
|
pub address_format_type: u8,
|
|
pub address_format_version: u8,
|
|
pub server_address: String,
|
|
pub sync_servers: Vec<String>,
|
|
pub access_servers: Vec<String>,
|
|
pub sessions_mode: u8,
|
|
pub sessions: Vec<SessionRecord>,
|
|
pub trusted_count: u8,
|
|
pub signature: [u8; 64],
|
|
}
|
|
|
|
struct Reader<'a> {
|
|
data: &'a [u8],
|
|
cursor: usize,
|
|
}
|
|
|
|
impl<'a> Reader<'a> {
|
|
fn new(data: &'a [u8]) -> Self { Self { data, cursor: 0 } }
|
|
fn read_u8(&mut self) -> Result<u8, ProgramError> {
|
|
let v = *self.data.get(self.cursor).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor += 1;
|
|
Ok(v)
|
|
}
|
|
fn read_u32(&mut self) -> Result<u32, ProgramError> {
|
|
let end = self.cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
|
|
}
|
|
fn read_u64(&mut self) -> Result<u64, ProgramError> {
|
|
let end = self.cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]))
|
|
}
|
|
fn read_fixed_32(&mut self) -> Result<[u8; 32], ProgramError> {
|
|
let end = self.cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
<[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn read_fixed_64(&mut self) -> Result<[u8; 64], ProgramError> {
|
|
let end = self.cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
<[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn read_pubkey(&mut self) -> Result<Pubkey, ProgramError> {
|
|
Ok(Pubkey::new_from_array(self.read_fixed_32()?))
|
|
}
|
|
fn read_string_u8(&mut self) -> Result<String, ProgramError> {
|
|
let len = self.read_u8()? as usize;
|
|
let end = self.cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
let s = self.data.get(self.cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidInstruction))?;
|
|
self.cursor = end;
|
|
std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidInstruction))
|
|
}
|
|
fn finish(self) -> Result<(), ProgramError> {
|
|
require!(self.cursor == self.data.len(), ShineUsersError::InvalidInstruction);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
|
|
let mut r = Reader::new(instruction_data);
|
|
let tag = r.read_u8()?;
|
|
match tag {
|
|
IX_INIT_USERS_ECONOMY_CONFIG => {
|
|
r.finish()?;
|
|
process_init_users_economy_config(program_id, accounts)
|
|
}
|
|
IX_UPDATE_USERS_ECONOMY_CONFIG => {
|
|
let args = UpdateUsersEconomyConfigArgs {
|
|
registration_fee_lamports: r.read_u64()?,
|
|
lamports_per_limit_step: r.read_u64()?,
|
|
start_bonus_limit: r.read_u64()?,
|
|
};
|
|
r.finish()?;
|
|
process_update_users_economy_config(program_id, accounts, args)
|
|
}
|
|
IX_CREATE_USER_PDA => {
|
|
let args = parse_create_args(&mut r)?;
|
|
r.finish()?;
|
|
process_create_user_pda(program_id, accounts, args)
|
|
}
|
|
IX_UPDATE_USER_PDA => {
|
|
let args = parse_update_args(&mut r)?;
|
|
r.finish()?;
|
|
process_update_user_pda(program_id, accounts, args)
|
|
}
|
|
_ => Err(ProgramError::from(ShineUsersError::InvalidInstruction)),
|
|
}
|
|
}
|
|
|
|
fn parse_create_args(r: &mut Reader<'_>) -> Result<CreateUserPdaArgs, ProgramError> {
|
|
Ok(CreateUserPdaArgs {
|
|
login: r.read_string_u8()?,
|
|
root_key: r.read_pubkey()?,
|
|
created_at_ms: r.read_u64()?,
|
|
additional_limit: r.read_u64()?,
|
|
fields: parse_fields(r)?,
|
|
signature: r.read_fixed_64()?,
|
|
})
|
|
}
|
|
|
|
fn parse_update_args(r: &mut Reader<'_>) -> Result<UpdateUserPdaArgs, ProgramError> {
|
|
Ok(UpdateUserPdaArgs {
|
|
login: r.read_string_u8()?,
|
|
root_key: r.read_pubkey()?,
|
|
created_at_ms: r.read_u64()?,
|
|
updated_at_ms: r.read_u64()?,
|
|
version: r.read_u32()?,
|
|
prev_hash: r.read_fixed_32()?,
|
|
additional_limit: r.read_u64()?,
|
|
fields: parse_fields(r)?,
|
|
signature: r.read_fixed_64()?,
|
|
})
|
|
}
|
|
|
|
fn parse_fields(r: &mut Reader<'_>) -> Result<UserMutableFields, ProgramError> {
|
|
let device_key = r.read_pubkey()?;
|
|
let blockchain_public_key = r.read_pubkey()?;
|
|
let blockchain_name = r.read_string_u8()?;
|
|
let used_bytes = r.read_u64()?;
|
|
let last_block_number = r.read_u32()?;
|
|
let last_block_hash = r.read_fixed_32()?;
|
|
let last_block_signature = r.read_fixed_64()?;
|
|
let arweave_tx_id = r.read_string_u8()?;
|
|
let is_server = r.read_u8()? == 1;
|
|
let (address_format_type, address_format_version, server_address, sync_servers) = if is_server {
|
|
let aft = r.read_u8()?;
|
|
let afv = r.read_u8()?;
|
|
let addr = r.read_string_u8()?;
|
|
let cnt = r.read_u8()? as usize;
|
|
let mut list = Vec::with_capacity(cnt);
|
|
for _ in 0..cnt { list.push(r.read_string_u8()?); }
|
|
(aft, afv, addr, list)
|
|
} else {
|
|
(0, 0, String::new(), Vec::new())
|
|
};
|
|
let access_count = r.read_u8()? as usize;
|
|
let mut access_servers = Vec::with_capacity(access_count);
|
|
for _ in 0..access_count { access_servers.push(r.read_string_u8()?); }
|
|
let sessions_mode = r.read_u8()?;
|
|
let sessions_count = r.read_u8()? as usize;
|
|
let mut sessions = Vec::with_capacity(sessions_count);
|
|
for _ in 0..sessions_count {
|
|
sessions.push(SessionRecord {
|
|
session_type: r.read_u8()?,
|
|
session_version: r.read_u8()?,
|
|
session_name: r.read_string_u8()?,
|
|
session_pub_key: r.read_pubkey()?,
|
|
});
|
|
}
|
|
let trusted_count = r.read_u8()?;
|
|
Ok(UserMutableFields {
|
|
device_key,
|
|
blockchain_public_key,
|
|
blockchain_name,
|
|
used_bytes,
|
|
last_block_number,
|
|
last_block_hash,
|
|
last_block_signature,
|
|
arweave_tx_id,
|
|
is_server,
|
|
address_format_type,
|
|
address_format_version,
|
|
server_address,
|
|
sync_servers,
|
|
access_servers,
|
|
sessions_mode,
|
|
sessions,
|
|
trusted_count,
|
|
})
|
|
}
|
|
|
|
fn process_init_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
|
|
let (expected_pda, bump) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(users_economy_config_pda.owner == &system_program::id(), ShineUsersError::SystemAlreadyInitialized);
|
|
require!(users_economy_config_pda.data_is_empty(), ShineUsersError::SystemAlreadyInitialized);
|
|
|
|
let state = UsersEconomyConfigState {
|
|
version: 1,
|
|
registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS,
|
|
lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP,
|
|
start_bonus_limit: settings::START_BONUS_LIMIT,
|
|
};
|
|
let data = serialize_users_economy_config(&state);
|
|
create_pda_account(
|
|
signer,
|
|
users_economy_config_pda,
|
|
system_program_ai,
|
|
program_id,
|
|
&[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]],
|
|
data.len(),
|
|
)?;
|
|
write_pda_exact(users_economy_config_pda, &data)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_update_users_economy_config(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUsersEconomyConfigArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
let dao_authority = Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| ProgramError::from(ShineUsersError::InvalidSigner))?;
|
|
require_keys_eq!(dao_authority, *signer.key, ShineUsersError::InvalidSigner);
|
|
let (expected_pda, _) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *users_economy_config_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(users_economy_config_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
require!(args.lamports_per_limit_step > 0, ShineUsersError::InvalidRecordData);
|
|
|
|
let mut state = read_users_economy_config(users_economy_config_pda)?;
|
|
state.registration_fee_lamports = args.registration_fee_lamports;
|
|
state.lamports_per_limit_step = args.lamports_per_limit_step;
|
|
state.start_bonus_limit = args.start_bonus_limit;
|
|
write_pda_exact(users_economy_config_pda, &serialize_users_economy_config(&state))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_create_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateUserPdaArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let user_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
let inflow_vault = next_account_info(&mut it)?;
|
|
let instructions_sysvar = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
let login_guard_program = next_account_info(&mut it)?;
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
|
|
validate_login(&args.login)?;
|
|
validate_fields(&args.fields)?;
|
|
validate_inflow_vault(inflow_vault)?;
|
|
require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement);
|
|
require_keys_eq!(*login_guard_program.key, Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?, ShineUsersError::InvalidLoginGuardResponse);
|
|
classify_login_or_fail(login_guard_program, &args.login)?;
|
|
validate_users_economy_config_pda(program_id, users_economy_config_pda)?;
|
|
|
|
let economy = read_users_economy_config(users_economy_config_pda)?;
|
|
let login_seed = login_seed_normalized(&args.login);
|
|
let (expected_pda, bump) = find_user_pda(program_id, &login_seed);
|
|
require_keys_eq!(expected_pda, *user_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(user_pda.owner == &system_program::id(), ShineUsersError::UserAlreadyExists);
|
|
require!(user_pda.data_is_empty(), ShineUsersError::UserAlreadyExists);
|
|
|
|
let start_balance = economy.start_bonus_limit.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
let mut record = UserRecord {
|
|
created_at_ms: args.created_at_ms,
|
|
updated_at_ms: args.created_at_ms,
|
|
record_number: 0,
|
|
prev_record_hash: ZERO_HASH,
|
|
login: args.login,
|
|
root_key: args.root_key,
|
|
device_key: args.fields.device_key,
|
|
blockchain: BlockchainRecord {
|
|
blockchain_type: BLOCKCHAIN_TYPE_MAIN_USER,
|
|
blockchain_name: args.fields.blockchain_name,
|
|
blockchain_public_key: args.fields.blockchain_public_key,
|
|
paid_limit_bytes: start_balance,
|
|
used_bytes: args.fields.used_bytes,
|
|
last_block_number: args.fields.last_block_number,
|
|
last_block_hash: args.fields.last_block_hash,
|
|
last_block_signature: args.fields.last_block_signature,
|
|
arweave_tx_id: args.fields.arweave_tx_id,
|
|
},
|
|
is_server: args.fields.is_server,
|
|
address_format_type: args.fields.address_format_type,
|
|
address_format_version: args.fields.address_format_version,
|
|
server_address: args.fields.server_address,
|
|
sync_servers: args.fields.sync_servers,
|
|
access_servers: args.fields.access_servers,
|
|
sessions_mode: args.fields.sessions_mode,
|
|
sessions: args.fields.sessions,
|
|
trusted_count: args.fields.trusted_count,
|
|
signature: [0; 64],
|
|
};
|
|
validate_blockchain_limits(&record.blockchain, 0, 0, true)?;
|
|
verify_last_block_state_signature(instructions_sysvar, &record)?;
|
|
let unsigned = serialize_unsigned_record(&record)?;
|
|
let unsigned_hash = hashv(&[&unsigned]);
|
|
record.signature = verify_record_signature_hash(instructions_sysvar, &record.root_key, &args.signature, unsigned_hash.as_ref())?;
|
|
|
|
let serialized = pad_to_fixed_size(serialize_full_record(&record)?, settings::USER_PDA_SPACE)?;
|
|
create_pda_account(
|
|
signer,
|
|
user_pda,
|
|
system_program_ai,
|
|
program_id,
|
|
&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login_seed.as_bytes(), &[bump]],
|
|
settings::USER_PDA_SPACE,
|
|
)?;
|
|
write_pda_exact(user_pda, &serialized)?;
|
|
|
|
let total_fee = economy.registration_fee_lamports.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
transfer_lamports(signer, inflow_vault, system_program_ai, total_fee)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn process_update_user_pda(program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateUserPdaArgs) -> ProgramResult {
|
|
let mut it = accounts.iter();
|
|
let signer = next_account_info(&mut it)?;
|
|
let user_pda = next_account_info(&mut it)?;
|
|
let system_program_ai = next_account_info(&mut it)?;
|
|
let inflow_vault = next_account_info(&mut it)?;
|
|
let instructions_sysvar = next_account_info(&mut it)?;
|
|
let users_economy_config_pda = next_account_info(&mut it)?;
|
|
|
|
require!(signer.is_signer, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*signer.key, args.fields.device_key, ShineUsersError::InvalidSigner);
|
|
require_keys_eq!(*system_program_ai.key, system_program::id(), ShineUsersError::InvalidSystemProgram);
|
|
|
|
validate_login(&args.login)?;
|
|
validate_fields(&args.fields)?;
|
|
validate_inflow_vault(inflow_vault)?;
|
|
require!(args.additional_limit % settings::LIMIT_STEP == 0, ShineUsersError::InvalidLimitIncrement);
|
|
validate_users_economy_config_pda(program_id, users_economy_config_pda)?;
|
|
let economy = read_users_economy_config(users_economy_config_pda)?;
|
|
|
|
let normalized_login = login_seed_normalized(&args.login);
|
|
require_keys_eq!(find_user_pda(program_id, &normalized_login).0, *user_pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(user_pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
|
|
let old_record = deserialize_record_from_pda(&read_pda_all(user_pda)?)?;
|
|
require!(old_record.login == args.login, ShineUsersError::ImmutableFieldChanged);
|
|
require!(old_record.created_at_ms == args.created_at_ms, ShineUsersError::ImmutableFieldChanged);
|
|
require_keys_eq!(old_record.root_key, args.root_key, ShineUsersError::ImmutableFieldChanged);
|
|
require!(args.version == old_record.record_number.saturating_add(1), ShineUsersError::InvalidVersion);
|
|
require!(hash_unsigned_record(&old_record)? == args.prev_hash, ShineUsersError::InvalidPrevHash);
|
|
require!(args.fields.blockchain_name == old_record.blockchain.blockchain_name, ShineUsersError::ImmutableFieldChanged);
|
|
require_keys_eq!(args.fields.blockchain_public_key, old_record.blockchain.blockchain_public_key, ShineUsersError::ImmutableFieldChanged);
|
|
|
|
let new_balance = old_record.blockchain.paid_limit_bytes.checked_add(args.additional_limit).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(new_balance >= old_record.blockchain.paid_limit_bytes, ShineUsersError::BalanceDecrease);
|
|
|
|
let blockchain_state_unchanged = old_record.blockchain.used_bytes == args.fields.used_bytes
|
|
&& old_record.blockchain.last_block_number == args.fields.last_block_number
|
|
&& old_record.blockchain.last_block_hash == args.fields.last_block_hash
|
|
&& old_record.blockchain.last_block_signature == args.fields.last_block_signature
|
|
&& old_record.blockchain.arweave_tx_id == args.fields.arweave_tx_id;
|
|
|
|
let mut new_record = build_update_record(&old_record, &args, new_balance)?;
|
|
validate_blockchain_limits(&new_record.blockchain, old_record.blockchain.used_bytes, old_record.blockchain.last_block_number, false)?;
|
|
if !blockchain_state_unchanged {
|
|
verify_last_block_state_signature(instructions_sysvar, &new_record)?;
|
|
}
|
|
let unsigned = serialize_unsigned_record(&new_record)?;
|
|
let unsigned_hash = hashv(&[&unsigned]);
|
|
new_record.signature = verify_record_signature_hash(instructions_sysvar, &old_record.root_key, &args.signature, unsigned_hash.as_ref())?;
|
|
|
|
let serialized = serialize_full_record(&new_record)?;
|
|
ensure_pda_size_and_rent(user_pda, signer, system_program_ai, serialized.len())?;
|
|
write_pda_prefix(user_pda, &serialized)?;
|
|
|
|
let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?;
|
|
if topup_fee > 0 {
|
|
transfer_lamports(signer, inflow_vault, system_program_ai, topup_fee)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_update_record(old_record: &UserRecord, args: &UpdateUserPdaArgs, new_balance: u64) -> Result<UserRecord, ProgramError> {
|
|
Ok(UserRecord {
|
|
created_at_ms: old_record.created_at_ms,
|
|
updated_at_ms: args.updated_at_ms,
|
|
record_number: args.version,
|
|
prev_record_hash: args.prev_hash,
|
|
login: old_record.login.clone(),
|
|
root_key: old_record.root_key,
|
|
device_key: args.fields.device_key,
|
|
blockchain: BlockchainRecord {
|
|
blockchain_type: old_record.blockchain.blockchain_type,
|
|
blockchain_name: args.fields.blockchain_name.clone(),
|
|
blockchain_public_key: args.fields.blockchain_public_key,
|
|
paid_limit_bytes: new_balance,
|
|
used_bytes: args.fields.used_bytes,
|
|
last_block_number: args.fields.last_block_number,
|
|
last_block_hash: args.fields.last_block_hash,
|
|
last_block_signature: args.fields.last_block_signature,
|
|
arweave_tx_id: args.fields.arweave_tx_id.clone(),
|
|
},
|
|
is_server: args.fields.is_server,
|
|
address_format_type: args.fields.address_format_type,
|
|
address_format_version: args.fields.address_format_version,
|
|
server_address: args.fields.server_address.clone(),
|
|
sync_servers: args.fields.sync_servers.clone(),
|
|
access_servers: args.fields.access_servers.clone(),
|
|
sessions_mode: args.fields.sessions_mode,
|
|
sessions: args.fields.sessions.clone(),
|
|
trusted_count: args.fields.trusted_count,
|
|
signature: [0; 64],
|
|
})
|
|
}
|
|
|
|
fn classify_login_or_fail(login_guard_program: &AccountInfo, login: &str) -> ProgramResult {
|
|
let login_guard_program_id = Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID)
|
|
.map_err(|_| ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?;
|
|
require_keys_eq!(*login_guard_program.key, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse);
|
|
let mut data = Vec::with_capacity(1 + 4 + login.len());
|
|
data.push(LOGIN_GUARD_IX_CLASSIFY_LOGIN);
|
|
data.extend_from_slice(&(login.len() as u32).to_le_bytes());
|
|
data.extend_from_slice(login.as_bytes());
|
|
let ix = Instruction { program_id: login_guard_program_id, accounts: vec![], data };
|
|
invoke(&ix, &[login_guard_program.clone()])?;
|
|
let (program_id, raw) = get_return_data().ok_or(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse))?;
|
|
require_keys_eq!(program_id, login_guard_program_id, ShineUsersError::InvalidLoginGuardResponse);
|
|
require!(raw.len() == 4, ShineUsersError::InvalidLoginGuardResponse);
|
|
let class = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
|
match class {
|
|
0 => Ok(()),
|
|
1 => Err(ProgramError::from(ShineUsersError::PremiumLogin)),
|
|
2 => Err(ProgramError::from(ShineUsersError::TrademarkLoginRequiresReview)),
|
|
_ => Err(ProgramError::from(ShineUsersError::InvalidLoginGuardResponse)),
|
|
}
|
|
}
|
|
|
|
fn serialize_users_economy_config(state: &UsersEconomyConfigState) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(1 + 8 + 8 + 8);
|
|
out.push(state.version);
|
|
out.extend_from_slice(&state.registration_fee_lamports.to_le_bytes());
|
|
out.extend_from_slice(&state.lamports_per_limit_step.to_le_bytes());
|
|
out.extend_from_slice(&state.start_bonus_limit.to_le_bytes());
|
|
out
|
|
}
|
|
|
|
fn validate_users_economy_config_pda(program_id: &Pubkey, pda: &AccountInfo) -> ProgramResult {
|
|
let (expected_pda, _) = find_users_economy_config_pda(program_id);
|
|
require_keys_eq!(expected_pda, *pda.key, ShineUsersError::InvalidPdaAddress);
|
|
require!(pda.owner == program_id, ShineUsersError::InvalidPdaAddress);
|
|
Ok(())
|
|
}
|
|
|
|
fn read_users_economy_config(pda: &AccountInfo) -> Result<UsersEconomyConfigState, ProgramError> {
|
|
let raw = read_pda_all(pda)?;
|
|
require!(!raw.is_empty(), ShineUsersError::EmptyPdaData);
|
|
require!(raw.len() >= 25, ShineUsersError::InvalidAccountData);
|
|
Ok(UsersEconomyConfigState {
|
|
version: raw[0],
|
|
registration_fee_lamports: u64::from_le_bytes(raw[1..9].try_into().unwrap()),
|
|
lamports_per_limit_step: u64::from_le_bytes(raw[9..17].try_into().unwrap()),
|
|
start_bonus_limit: u64::from_le_bytes(raw[17..25].try_into().unwrap()),
|
|
})
|
|
}
|
|
|
|
fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord, ProgramError> {
|
|
require!(raw.len() >= 9, ShineUsersError::InvalidRecordData);
|
|
require!(sol_memcmp(&raw[0..5], MAGIC, 5) == 0, ShineUsersError::InvalidRecordMagic);
|
|
require!(raw[5] == FORMAT_MAJOR && raw[6] == FORMAT_MINOR, ShineUsersError::InvalidRecordFormat);
|
|
let record_len = u16::from_le_bytes([raw[7], raw[8]]) as usize;
|
|
require!(record_len >= 9 + 64, ShineUsersError::InvalidRecordLength);
|
|
require!(record_len <= raw.len(), ShineUsersError::InvalidRecordLength);
|
|
let useful = &raw[..record_len];
|
|
let mut cursor = 9usize;
|
|
|
|
let created_at_ms = read_u64_from(useful, &mut cursor)?;
|
|
let updated_at_ms = read_u64_from(useful, &mut cursor)?;
|
|
let record_number = read_u32_from(useful, &mut cursor)?;
|
|
let prev_record_hash = read_fixed_32_from(useful, &mut cursor)?;
|
|
let login = read_len_prefixed_string_from(useful, &mut cursor)?;
|
|
|
|
let blocks_count = read_u8_from(useful, &mut cursor)? as usize;
|
|
let mut root_key = None;
|
|
let mut device_key = None;
|
|
let mut blockchain = None;
|
|
let mut is_server = false;
|
|
let mut address_format_type = 0u8;
|
|
let mut address_format_version = 0u8;
|
|
let mut server_address = String::new();
|
|
let mut sync_servers = Vec::new();
|
|
let mut access_servers = Vec::new();
|
|
let mut sessions_mode = SESSIONS_MODE_MIXED;
|
|
let mut sessions = Vec::new();
|
|
let mut trusted_count = 0u8;
|
|
|
|
for _ in 0..blocks_count {
|
|
let block_type = read_u8_from(useful, &mut cursor)?;
|
|
let block_version = read_u8_from(useful, &mut cursor)?;
|
|
require!(block_version == BLOCK_VERSION_0, ShineUsersError::InvalidRecordFormat);
|
|
match block_type {
|
|
BLOCK_TYPE_ROOT_KEY => {
|
|
require!(root_key.is_none(), ShineUsersError::InvalidRecordData);
|
|
root_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
|
|
}
|
|
BLOCK_TYPE_DEVICE_KEY => {
|
|
require!(device_key.is_none(), ShineUsersError::InvalidRecordData);
|
|
device_key = Some(Pubkey::new_from_array(read_fixed_32_from(useful, &mut cursor)?));
|
|
}
|
|
BLOCK_TYPE_BLOCKCHAIN_REGISTRY => {
|
|
require!(blockchain.is_none(), ShineUsersError::InvalidRecordData);
|
|
let count = read_u8_from(useful, &mut cursor)?;
|
|
require!(count == 1, ShineUsersError::InvalidRecordData);
|
|
blockchain = Some(read_blockchain_record(useful, &mut cursor)?);
|
|
}
|
|
BLOCK_TYPE_SERVER_PROFILE => {
|
|
require!(!is_server, ShineUsersError::InvalidRecordData);
|
|
is_server = read_u8_from(useful, &mut cursor)? == 1;
|
|
require!(is_server, ShineUsersError::InvalidRecordData);
|
|
address_format_type = read_u8_from(useful, &mut cursor)?;
|
|
address_format_version = read_u8_from(useful, &mut cursor)?;
|
|
server_address = read_len_prefixed_string_from(useful, &mut cursor)?;
|
|
let sync_count = read_u8_from(useful, &mut cursor)? as usize;
|
|
require!(sync_count <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData);
|
|
for _ in 0..sync_count { sync_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); }
|
|
}
|
|
BLOCK_TYPE_ACCESS_SERVERS => {
|
|
require!(access_servers.is_empty(), ShineUsersError::InvalidRecordData);
|
|
let access_count = read_u8_from(useful, &mut cursor)? as usize;
|
|
for _ in 0..access_count { access_servers.push(read_len_prefixed_string_from(useful, &mut cursor)?); }
|
|
}
|
|
BLOCK_TYPE_SESSIONS => {
|
|
require!(sessions.is_empty(), ShineUsersError::InvalidRecordData);
|
|
sessions_mode = read_u8_from(useful, &mut cursor)?;
|
|
let sessions_count = read_u8_from(useful, &mut cursor)? as usize;
|
|
require!(sessions_count <= MAX_SESSIONS, ShineUsersError::InvalidRecordData);
|
|
for _ in 0..sessions_count { sessions.push(read_session_record(useful, &mut cursor)?); }
|
|
}
|
|
BLOCK_TYPE_TRUSTED_STATE => {
|
|
trusted_count = read_u8_from(useful, &mut cursor)?;
|
|
}
|
|
_ => return Err(ProgramError::from(ShineUsersError::InvalidRecordFormat)),
|
|
}
|
|
}
|
|
validate_sessions_fields(sessions_mode, &sessions)?;
|
|
let signature = read_fixed_64_from(useful, &mut cursor)?;
|
|
require!(cursor == useful.len(), ShineUsersError::InvalidRecordLength);
|
|
|
|
Ok(UserRecord {
|
|
created_at_ms,
|
|
updated_at_ms,
|
|
record_number,
|
|
prev_record_hash,
|
|
login,
|
|
root_key: root_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
device_key: device_key.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
blockchain: blockchain.ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?,
|
|
is_server,
|
|
address_format_type,
|
|
address_format_version,
|
|
server_address,
|
|
sync_servers,
|
|
access_servers,
|
|
sessions_mode,
|
|
sessions,
|
|
trusted_count,
|
|
signature,
|
|
})
|
|
}
|
|
|
|
fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result<BlockchainRecord, ProgramError> {
|
|
let blockchain_type = read_u8_from(data, cursor)?;
|
|
require!(blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData);
|
|
let blockchain_name = read_len_prefixed_string_from(data, cursor)?;
|
|
let blockchain_public_key = Pubkey::new_from_array(read_fixed_32_from(data, cursor)?);
|
|
let paid_limit_bytes = read_u64_from(data, cursor)?;
|
|
let used_bytes = read_u64_from(data, cursor)?;
|
|
let last_block_number = read_u32_from(data, cursor)?;
|
|
let last_block_hash = read_fixed_32_from(data, cursor)?;
|
|
let last_block_signature = read_fixed_64_from(data, cursor)?;
|
|
let arweave_present = read_u8_from(data, cursor)?;
|
|
let arweave_tx_id = match arweave_present { 0 => String::new(), 1 => read_len_prefixed_string_from(data, cursor)?, _ => return Err(ProgramError::from(ShineUsersError::InvalidRecordData)) };
|
|
Ok(BlockchainRecord {
|
|
blockchain_type,
|
|
blockchain_name,
|
|
blockchain_public_key,
|
|
paid_limit_bytes,
|
|
used_bytes,
|
|
last_block_number,
|
|
last_block_hash,
|
|
last_block_signature,
|
|
arweave_tx_id,
|
|
})
|
|
}
|
|
|
|
fn read_session_record(data: &[u8], cursor: &mut usize) -> Result<SessionRecord, ProgramError> {
|
|
Ok(SessionRecord {
|
|
session_type: read_u8_from(data, cursor)?,
|
|
session_version: read_u8_from(data, cursor)?,
|
|
session_name: read_len_prefixed_string_from(data, cursor)?,
|
|
session_pub_key: Pubkey::new_from_array(read_fixed_32_from(data, cursor)?),
|
|
})
|
|
}
|
|
|
|
fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
|
|
let login_bytes = record.login.as_bytes();
|
|
require!(login_bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidLogin);
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(MAGIC);
|
|
out.push(FORMAT_MAJOR);
|
|
out.push(FORMAT_MINOR);
|
|
out.extend_from_slice(&0u16.to_le_bytes());
|
|
out.extend_from_slice(&record.created_at_ms.to_le_bytes());
|
|
out.extend_from_slice(&record.updated_at_ms.to_le_bytes());
|
|
out.extend_from_slice(&record.record_number.to_le_bytes());
|
|
out.extend_from_slice(&record.prev_record_hash);
|
|
out.push(login_bytes.len() as u8);
|
|
out.extend_from_slice(login_bytes);
|
|
|
|
let blocks_count = if record.is_server { 7 } else { 6 };
|
|
out.push(blocks_count);
|
|
write_root_key_block(&mut out, record);
|
|
write_device_key_block(&mut out, record);
|
|
write_blockchain_registry_block(&mut out, &record.blockchain)?;
|
|
if record.is_server { write_server_profile_block(&mut out, record)?; }
|
|
write_access_servers_block(&mut out, record)?;
|
|
write_sessions_block(&mut out, record)?;
|
|
write_trusted_state_block(&mut out, record);
|
|
|
|
let record_len = out.len().checked_add(64).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(record_len <= u16::MAX as usize, ShineUsersError::RecordTooLarge);
|
|
let len_bytes = (record_len as u16).to_le_bytes();
|
|
out[7] = len_bytes[0];
|
|
out[8] = len_bytes[1];
|
|
Ok(out)
|
|
}
|
|
|
|
fn serialize_full_record(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
|
|
let mut out = serialize_unsigned_record(record)?;
|
|
out.extend_from_slice(&record.signature);
|
|
Ok(out)
|
|
}
|
|
|
|
fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32], ProgramError> {
|
|
let unsigned = serialize_unsigned_record(record)?;
|
|
let digest = hashv(&[&unsigned]);
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(digest.as_ref());
|
|
Ok(out)
|
|
}
|
|
|
|
fn write_root_key_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_ROOT_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.root_key.as_ref()); }
|
|
fn write_device_key_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_DEVICE_KEY); out.push(BLOCK_VERSION_0); out.extend_from_slice(record.device_key.as_ref()); }
|
|
fn write_blockchain_registry_block(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<(), ProgramError> { out.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY); out.push(BLOCK_VERSION_0); out.push(1); write_blockchain_record(out, blockchain) }
|
|
fn write_blockchain_record(out: &mut Vec<u8>, blockchain: &BlockchainRecord) -> Result<(), ProgramError> {
|
|
out.push(blockchain.blockchain_type);
|
|
write_len_prefixed_string(out, &blockchain.blockchain_name)?;
|
|
out.extend_from_slice(blockchain.blockchain_public_key.as_ref());
|
|
out.extend_from_slice(&blockchain.paid_limit_bytes.to_le_bytes());
|
|
out.extend_from_slice(&blockchain.used_bytes.to_le_bytes());
|
|
out.extend_from_slice(&blockchain.last_block_number.to_le_bytes());
|
|
out.extend_from_slice(&blockchain.last_block_hash);
|
|
out.extend_from_slice(&blockchain.last_block_signature);
|
|
if blockchain.arweave_tx_id.is_empty() { out.push(0); } else { out.push(1); write_len_prefixed_string(out, &blockchain.arweave_tx_id)?; }
|
|
Ok(())
|
|
}
|
|
fn write_server_profile_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
|
|
out.push(BLOCK_TYPE_SERVER_PROFILE); out.push(BLOCK_VERSION_0); out.push(1); out.push(record.address_format_type); out.push(record.address_format_version); write_len_prefixed_string(out, &record.server_address)?; require!(record.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData); out.push(record.sync_servers.len() as u8); for login in &record.sync_servers { write_len_prefixed_string(out, login)?; } Ok(())
|
|
}
|
|
fn write_access_servers_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
|
|
out.push(BLOCK_TYPE_ACCESS_SERVERS); out.push(BLOCK_VERSION_0); require!(record.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(record.access_servers.len() as u8); for login in &record.access_servers { write_len_prefixed_string(out, login)?; } Ok(())
|
|
}
|
|
fn write_sessions_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<(), ProgramError> {
|
|
out.push(BLOCK_TYPE_SESSIONS); out.push(BLOCK_VERSION_0); out.push(record.sessions_mode); require!(record.sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData); out.push(record.sessions.len() as u8); for session in &record.sessions { out.push(session.session_type); out.push(session.session_version); write_len_prefixed_string(out, &session.session_name)?; out.extend_from_slice(session.session_pub_key.as_ref()); } Ok(())
|
|
}
|
|
fn write_trusted_state_block(out: &mut Vec<u8>, record: &UserRecord) { out.push(BLOCK_TYPE_TRUSTED_STATE); out.push(BLOCK_VERSION_0); out.push(record.trusted_count); }
|
|
fn write_len_prefixed_string(out: &mut Vec<u8>, value: &str) -> Result<(), ProgramError> { let bytes = value.as_bytes(); require!(bytes.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); out.push(bytes.len() as u8); out.extend_from_slice(bytes); Ok(()) }
|
|
|
|
fn verify_record_signature_hash(instructions_sysvar: &AccountInfo, root_key: &Pubkey, signature: &[u8; 64], message_hash: &[u8]) -> Result<[u8; 64], ProgramError> {
|
|
verify_ed25519_signature_instruction(instructions_sysvar, -2, root_key, signature, message_hash)?;
|
|
Ok(*signature)
|
|
}
|
|
|
|
fn verify_last_block_state_signature(instructions_sysvar: &AccountInfo, record: &UserRecord) -> ProgramResult {
|
|
let message = serialize_last_block_state(record)?;
|
|
let msg_hash = hashv(&[&message]);
|
|
verify_ed25519_signature_instruction(instructions_sysvar, -1, &record.blockchain.blockchain_public_key, &record.blockchain.last_block_signature, msg_hash.as_ref())
|
|
}
|
|
|
|
fn serialize_last_block_state(record: &UserRecord) -> Result<Vec<u8>, ProgramError> {
|
|
let mut out = Vec::new();
|
|
out.extend_from_slice(LAST_BLOCK_STATE_PREFIX);
|
|
write_len_prefixed_string(&mut out, &record.login)?;
|
|
write_len_prefixed_string(&mut out, &record.blockchain.blockchain_name)?;
|
|
out.extend_from_slice(&record.blockchain.last_block_number.to_le_bytes());
|
|
out.extend_from_slice(&record.blockchain.last_block_hash);
|
|
out.extend_from_slice(&record.blockchain.used_bytes.to_le_bytes());
|
|
Ok(out)
|
|
}
|
|
|
|
struct ParsedEd25519Ref<'a> {
|
|
pubkey: Pubkey,
|
|
signature: [u8; 64],
|
|
message: &'a [u8],
|
|
}
|
|
|
|
fn verify_ed25519_signature_instruction(instructions_sysvar: &AccountInfo, index_relative_to_current: i64, expected_pubkey: &Pubkey, expected_signature: &[u8; 64], expected_message: &[u8]) -> ProgramResult {
|
|
require_keys_eq!(*instructions_sysvar.key, solana_program::sysvar::instructions::id(), ShineUsersError::InvalidSignature);
|
|
let current_index = load_current_index_checked(instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))? as i64;
|
|
let target_index = current_index.checked_add(index_relative_to_current).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
require!(target_index >= 0, ShineUsersError::InvalidSignature);
|
|
let ed_ix = load_instruction_at_checked(target_index as usize, instructions_sysvar).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
require_keys_eq!(ed_ix.program_id, solana_program::ed25519_program::id(), ShineUsersError::InvalidSignature);
|
|
let parsed = parse_ed25519_ix(ed_ix.data.as_slice())?;
|
|
require!(parsed.pubkey == *expected_pubkey, ShineUsersError::InvalidSignature);
|
|
require!(parsed.signature == *expected_signature, ShineUsersError::InvalidSignature);
|
|
require!(parsed.message == expected_message, ShineUsersError::InvalidSignature);
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_ed25519_ix<'a>(data: &'a [u8]) -> Result<ParsedEd25519Ref<'a>, ProgramError> {
|
|
require!(data.len() >= 16, ShineUsersError::InvalidSignature);
|
|
require!(data[0] == 1, ShineUsersError::InvalidSignature);
|
|
let signature_offset = le_u16(data, 2)? as usize;
|
|
let signature_ix_index = le_u16(data, 4)?;
|
|
let pubkey_offset = le_u16(data, 6)? as usize;
|
|
let pubkey_ix_index = le_u16(data, 8)?;
|
|
let message_offset = le_u16(data, 10)? as usize;
|
|
let message_size = le_u16(data, 12)? as usize;
|
|
let message_ix_index = le_u16(data, 14)?;
|
|
require!(signature_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
require!(pubkey_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
require!(message_ix_index == u16::MAX, ShineUsersError::InvalidSignature);
|
|
|
|
let signature_end = signature_offset.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let pubkey_end = pubkey_offset.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let message_end = message_offset.checked_add(message_size).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let signature_slice = data.get(signature_offset..signature_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let pubkey_slice = data.get(pubkey_offset..pubkey_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let message = data.get(message_offset..message_end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let mut signature = [0u8; 64]; signature.copy_from_slice(signature_slice);
|
|
let pubkey = Pubkey::new_from_array(<[u8; 32]>::try_from(pubkey_slice).map_err(|_| ProgramError::from(ShineUsersError::InvalidSignature))?);
|
|
Ok(ParsedEd25519Ref { pubkey, signature, message })
|
|
}
|
|
|
|
fn le_u16(data: &[u8], offset: usize) -> Result<u16, ProgramError> {
|
|
let end = offset.checked_add(2).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
let s = data.get(offset..end).ok_or(ProgramError::from(ShineUsersError::InvalidSignature))?;
|
|
Ok(u16::from_le_bytes([s[0], s[1]]))
|
|
}
|
|
|
|
fn validate_login(login: &str) -> ProgramResult {
|
|
require!(!login.is_empty(), ShineUsersError::InvalidLogin);
|
|
require!(login.len() <= 20, ShineUsersError::InvalidLogin);
|
|
for ch in login.chars() { if !(ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_') { return Err(ProgramError::from(ShineUsersError::InvalidLogin)); } }
|
|
Ok(())
|
|
}
|
|
fn login_seed_normalized(login: &str) -> String { login.to_ascii_lowercase() }
|
|
|
|
fn validate_fields(fields: &UserMutableFields) -> ProgramResult {
|
|
require!(!fields.blockchain_name.is_empty(), ShineUsersError::InvalidRecordData);
|
|
require!(fields.blockchain_name.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
require!(fields.arweave_tx_id.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
if fields.is_server {
|
|
require!(!fields.server_address.is_empty(), ShineUsersError::InvalidRecordData);
|
|
require!(fields.server_address.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
require!(fields.sync_servers.len() <= MAX_SYNC_SERVERS, ShineUsersError::InvalidRecordData);
|
|
for login in &fields.sync_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); }
|
|
} else {
|
|
require!(fields.server_address.is_empty(), ShineUsersError::InvalidRecordData);
|
|
require!(fields.sync_servers.is_empty(), ShineUsersError::InvalidRecordData);
|
|
}
|
|
require!(fields.access_servers.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData);
|
|
for login in &fields.access_servers { require!(!login.is_empty(), ShineUsersError::InvalidRecordData); require!(login.len() <= u8::MAX as usize, ShineUsersError::InvalidRecordData); }
|
|
validate_sessions_fields(fields.sessions_mode, &fields.sessions)
|
|
}
|
|
|
|
fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> ProgramResult {
|
|
require!(mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY, ShineUsersError::InvalidRecordData);
|
|
require!(sessions.len() <= MAX_SESSIONS, ShineUsersError::InvalidRecordData);
|
|
for i in 0..sessions.len() {
|
|
validate_session_record(&sessions[i])?;
|
|
for j in (i + 1)..sessions.len() {
|
|
require!(sessions[i].session_name != sessions[j].session_name, ShineUsersError::InvalidRecordData);
|
|
require!(sessions[i].session_pub_key != sessions[j].session_pub_key, ShineUsersError::InvalidRecordData);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_session_record(session: &SessionRecord) -> ProgramResult {
|
|
require!(session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER, ShineUsersError::InvalidRecordData);
|
|
require!(session.session_version == 1, ShineUsersError::InvalidRecordData);
|
|
let bytes = session.session_name.as_bytes();
|
|
require!(!bytes.is_empty(), ShineUsersError::InvalidRecordData);
|
|
require!(bytes.len() <= MAX_SESSION_NAME_LEN, ShineUsersError::InvalidRecordData);
|
|
for &b in bytes { require!(b.is_ascii_alphanumeric() || b == b'_', ShineUsersError::InvalidRecordData); }
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_blockchain_limits(blockchain: &BlockchainRecord, old_used_bytes: u64, old_last_block_number: u32, is_create: bool) -> ProgramResult {
|
|
require!(blockchain.blockchain_type == BLOCKCHAIN_TYPE_MAIN_USER, ShineUsersError::InvalidRecordData);
|
|
require!(blockchain.used_bytes <= blockchain.paid_limit_bytes, ShineUsersError::InvalidRecordData);
|
|
if !is_create {
|
|
require!(blockchain.used_bytes >= old_used_bytes && blockchain.last_block_number >= old_last_block_number, ShineUsersError::InvalidRecordData);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_inflow_vault(inflow_vault: &AccountInfo) -> ProgramResult {
|
|
let payments_program_id = Pubkey::from_str(settings::SHINE_PAYMENTS_PROGRAM_ID).map_err(|_| ProgramError::from(ShineUsersError::InvalidFeeReceiver))?;
|
|
let (expected, _) = Pubkey::find_program_address(&[settings::SHINE_PAYMENTS_INFLOW_VAULT_SEED], &payments_program_id);
|
|
require_keys_eq!(expected, *inflow_vault.key, ShineUsersError::InvalidFeeReceiver);
|
|
Ok(())
|
|
}
|
|
|
|
fn transfer_lamports<'a>(payer: &AccountInfo<'a>, recipient: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, lamports: u64) -> ProgramResult {
|
|
if lamports == 0 { return Ok(()); }
|
|
let ix = system_instruction::transfer(payer.key, recipient.key, lamports);
|
|
invoke(&ix, &[payer.clone(), recipient.clone(), system_program_ai.clone()])
|
|
}
|
|
|
|
fn create_pda_account<'a>(payer: &AccountInfo<'a>, pda: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, owner: &Pubkey, seeds: &[&[u8]], space: usize) -> ProgramResult {
|
|
let rent = Rent::get()?;
|
|
let lamports = rent.minimum_balance(space);
|
|
let ix = system_instruction::create_account(payer.key, pda.key, lamports, space as u64, owner);
|
|
invoke_signed(&ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds])
|
|
}
|
|
|
|
fn ensure_pda_size_and_rent<'a>(pda: &AccountInfo<'a>, payer: &AccountInfo<'a>, system_program_ai: &AccountInfo<'a>, required_len: usize) -> ProgramResult {
|
|
let current_len = pda.data_len();
|
|
if required_len <= current_len { return Ok(()); }
|
|
let increase = required_len.checked_sub(current_len).ok_or(ProgramError::from(ShineUsersError::MathOverflow))?;
|
|
require!(increase <= MAX_AUTO_REALLOC_INCREASE, ShineUsersError::RecordTooLarge);
|
|
let rent = Rent::get()?;
|
|
let required_lamports = rent.minimum_balance(required_len);
|
|
let current_lamports = pda.lamports();
|
|
let top_up = required_lamports.saturating_sub(current_lamports);
|
|
if top_up > 0 { transfer_lamports(payer, pda, system_program_ai, top_up)?; }
|
|
pda.realloc(required_len, false)
|
|
}
|
|
|
|
fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USER_PDA_SEED_PREFIX.as_bytes(), login.as_bytes()], program_id) }
|
|
fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id) }
|
|
fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result<u64, ProgramError> { (limit_delta / settings::LIMIT_STEP).checked_mul(lamports_per_limit_step).ok_or(ProgramError::from(ShineUsersError::MathOverflow)) }
|
|
fn pad_to_fixed_size(mut bytes: Vec<u8>, target_size: usize) -> Result<Vec<u8>, ProgramError> { require!(bytes.len() <= target_size, ShineUsersError::RecordTooLarge); bytes.resize(target_size, 0); Ok(bytes) }
|
|
|
|
fn read_pda_all(pda: &AccountInfo) -> Result<Vec<u8>, ProgramError> { Ok(pda.try_borrow_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?.to_vec()) }
|
|
fn write_pda_exact(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); for b in &mut dst[data.len()..] { *b = 0; } Ok(()) }
|
|
fn write_pda_prefix(pda: &AccountInfo, data: &[u8]) -> ProgramResult { let mut dst = pda.try_borrow_mut_data().map_err(|_| ProgramError::from(ShineUsersError::InvalidAccountData))?; require!(data.len() <= dst.len(), ShineUsersError::RecordTooLarge); dst[..data.len()].copy_from_slice(data); Ok(()) }
|
|
|
|
fn read_u8_from(data: &[u8], cursor: &mut usize) -> Result<u8, ProgramError> { let v = *data.get(*cursor).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor += 1; Ok(v) }
|
|
fn read_u32_from(data: &[u8], cursor: &mut usize) -> Result<u32, ProgramError> { let end = cursor.checked_add(4).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) }
|
|
fn read_u64_from(data: &[u8], cursor: &mut usize) -> Result<u64, ProgramError> { let end = cursor.checked_add(8).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; Ok(u64::from_le_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])) }
|
|
fn read_fixed_32_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 32], ProgramError> { let end = cursor.checked_add(32).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 32]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|
|
fn read_fixed_64_from(data: &[u8], cursor: &mut usize) -> Result<[u8; 64], ProgramError> { let end = cursor.checked_add(64).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; <[u8; 64]>::try_from(s).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|
|
fn read_len_prefixed_string_from(data: &[u8], cursor: &mut usize) -> Result<String, ProgramError> { let len = read_u8_from(data, cursor)? as usize; let end = cursor.checked_add(len).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; let s = data.get(*cursor..end).ok_or(ProgramError::from(ShineUsersError::InvalidRecordData))?; *cursor = end; std::str::from_utf8(s).map(|v| v.to_string()).map_err(|_| ProgramError::from(ShineUsersError::InvalidRecordData)) }
|