Зафиксировать все текущие изменения проекта

This commit is contained in:
AidarKC
2026-06-04 22:27:09 +04:00
parent 624557ebfd
commit 60049442f1
9 changed files with 1526 additions and 182 deletions
@@ -14,6 +14,8 @@ 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;
@@ -22,10 +24,23 @@ 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_TYPE_SESSIONS: u8 = 55;
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";
#[derive(AnchorSerialize, AnchorDeserialize, 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(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UserMutableFields {
pub device_key: Pubkey,
@@ -42,6 +57,8 @@ pub struct UserMutableFields {
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,
}
@@ -83,6 +100,8 @@ pub struct UserRecord {
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],
}
@@ -242,7 +261,35 @@ pub fn update_users_economy_config(
}
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
validate_login(&args.login)?;
let CreateUserPdaArgs {
login,
root_key,
created_at_ms,
additional_limit,
fields,
signature,
} = args;
let 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,
} = fields;
validate_login(&login)?;
require_keys_eq!(
ctx.accounts.login_guard_program.key(),
Pubkey::from_str(settings::SHINE_LOGIN_GUARD_PROGRAM_ID)
@@ -252,17 +299,35 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
classify_login_or_fail(
&ctx.accounts.login_guard_program.to_account_info(),
&ctx.accounts.signer,
&args.login,
&login,
)?;
validate_fields(&args.fields)?;
validate_fields(&UserMutableFields {
device_key,
blockchain_public_key,
blockchain_name: blockchain_name.clone(),
used_bytes,
last_block_number,
last_block_hash: last_block_hash.clone(),
last_block_signature: last_block_signature.clone(),
arweave_tx_id: arweave_tx_id.clone(),
is_server,
address_format_type,
address_format_version,
server_address: server_address.clone(),
sync_servers: sync_servers.clone(),
access_servers: access_servers.clone(),
sessions_mode,
sessions: sessions.clone(),
trusted_count,
})?;
validate_inflow_vault(&ctx.accounts.inflow_vault)?;
require!(
args.additional_limit % settings::LIMIT_STEP == 0,
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 login_seed = login_seed_normalized(&login);
let (expected_pda, bump) = find_user_pda(ctx.program_id, &login_seed);
require_keys_eq!(
expected_pda,
@@ -276,46 +341,50 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
let start_balance = economy
.start_bonus_limit
.checked_add(args.additional_limit)
.checked_add(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,
created_at_ms,
updated_at_ms: 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,
login,
root_key,
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,
blockchain_name,
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(),
used_bytes,
last_block_number,
last_block_hash: vec_to_hash32(&last_block_hash)?,
last_block_signature: vec_to_signature(&last_block_signature)?,
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.clone(),
sync_servers: args.fields.sync_servers.clone(),
access_servers: args.fields.access_servers.clone(),
trusted_count: args.fields.trusted_count,
is_server,
address_format_type,
address_format_version,
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
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(
let unsigned_hash = hashv(&[&unsigned]);
drop(unsigned);
record.signature = verify_record_signature_hash(
&ctx.accounts.instructions,
&record.root_key,
&args.signature,
&unsigned,
&signature,
unsigned_hash.as_ref(),
)?;
let serialized = serialize_full_record(&record)?;
@@ -342,10 +411,7 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
let total_fee = economy
.registration_fee_lamports
.checked_add(limit_fee_lamports(
args.additional_limit,
economy.lamports_per_limit_step,
)?)
.checked_add(limit_fee_lamports(additional_limit, economy.lamports_per_limit_step)?)
.ok_or(error!(ErrCode::MathOverflow))?;
transfer_lamports(
&ctx.accounts.signer,
@@ -396,10 +462,11 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
);
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,
{
let normalized_login = login_seed_normalized(&args.login);
find_user_pda(ctx.program_id, &normalized_login).0
},
ctx.accounts.user_pda.key(),
ErrCode::InvalidPdaAddress
);
@@ -410,7 +477,7 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
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())?;
let old_record = Box::new(deserialize_record_from_pda(raw.as_slice())?);
drop(raw);
require!(
@@ -431,24 +498,21 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
ErrCode::InvalidVersion
);
let expected_prev_hash = hash_unsigned_record(&old_record)?;
let provided_prev_hash = vec_to_hash32(&args.prev_hash)?;
let provided_prev_hash = Box::new(vec_to_hash32(&args.prev_hash)?);
require!(
expected_prev_hash == provided_prev_hash,
hash_unsigned_record(&old_record)? == *provided_prev_hash,
ErrCode::InvalidPrevHash
);
let new_balance = old_record
let new_balance = Box::new(old_record
.blockchain
.paid_limit_bytes
.checked_add(args.additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?;
.ok_or(error!(ErrCode::MathOverflow))?);
require!(
new_balance >= old_record.blockchain.paid_limit_bytes,
*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()
@@ -456,60 +520,60 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
== 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
args.fields.blockchain_name == old_record.blockchain.blockchain_name
&& args.fields.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);
{
let candidate_blockchain =
build_candidate_blockchain_for_update(&old_record, *new_balance, &args.fields)?;
validate_blockchain_limits(
&candidate_blockchain,
old_record.blockchain.used_bytes,
old_record.blockchain.last_block_number,
false,
)?;
}
if !blockchain_state_unchanged {
verify_last_block_state_signature(&ctx.accounts.instructions, &new_record)?;
verify_last_block_state_signature_candidate(
&ctx.accounts.instructions,
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
}
let unsigned = serialize_unsigned_record(&new_record)?;
new_record.signature = verify_record_signature(
&ctx.accounts.instructions,
&new_record.root_key,
&args.signature,
&unsigned,
let unsigned = serialize_unsigned_update_candidate(
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
let unsigned_hash = hashv(&[&unsigned]);
drop(unsigned);
let verified_signature = Box::new(verify_record_signature_hash(
&ctx.accounts.instructions,
&old_record.root_key,
&args.signature,
unsigned_hash.as_ref(),
)?);
let mut new_record = build_update_record(
&old_record,
args.updated_at_ms,
args.version,
*provided_prev_hash,
*new_balance,
&args.fields,
)?;
new_record.signature = *verified_signature;
drop(old_record);
let serialized = serialize_full_record(&new_record)?;
ensure_pda_size_and_rent(
@@ -533,6 +597,97 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
Ok(())
}
#[inline(never)]
fn build_candidate_blockchain_for_update(
old_record: &UserRecord,
new_balance: u64,
fields: &UserMutableFields,
) -> Result<BlockchainRecord> {
Ok(BlockchainRecord {
blockchain_type: old_record.blockchain.blockchain_type,
blockchain_name: fields.blockchain_name.clone(),
blockchain_public_key: fields.blockchain_public_key,
paid_limit_bytes: new_balance,
used_bytes: fields.used_bytes,
last_block_number: fields.last_block_number,
last_block_hash: vec_to_hash32(&fields.last_block_hash)?,
last_block_signature: vec_to_signature(&fields.last_block_signature)?,
arweave_tx_id: fields.arweave_tx_id.clone(),
})
}
#[inline(never)]
fn build_update_record(
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<UserRecord> {
Ok(UserRecord {
created_at_ms: old_record.created_at_ms,
updated_at_ms,
record_number: version,
prev_record_hash,
login: old_record.login.clone(),
root_key: old_record.root_key,
device_key: fields.device_key,
blockchain: build_candidate_blockchain_for_update(old_record, new_balance, fields)?,
is_server: fields.is_server,
address_format_type: fields.address_format_type,
address_format_version: fields.address_format_version,
server_address: fields.server_address.clone(),
sync_servers: fields.sync_servers.clone(),
access_servers: fields.access_servers.clone(),
sessions_mode: fields.sessions_mode,
sessions: fields.sessions.clone(),
trusted_count: fields.trusted_count,
signature: [0; 64],
})
}
#[inline(never)]
fn verify_last_block_state_signature_candidate(
instructions_sysvar: &AccountInfo,
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<()> {
let candidate = build_update_record(
old_record,
updated_at_ms,
version,
prev_record_hash,
new_balance,
fields,
)?;
verify_last_block_state_signature(instructions_sysvar, &candidate)
}
#[inline(never)]
fn serialize_unsigned_update_candidate(
old_record: &UserRecord,
updated_at_ms: u64,
version: u32,
prev_record_hash: [u8; 32],
new_balance: u64,
fields: &UserMutableFields,
) -> Result<Vec<u8>> {
let candidate = build_update_record(
old_record,
updated_at_ms,
version,
prev_record_hash,
new_balance,
fields,
)?;
serialize_unsigned_record(&candidate)
}
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);
@@ -551,7 +706,7 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
out.push(login_bytes.len() as u8);
out.extend_from_slice(login_bytes);
let blocks_count = if record.is_server { 6 } else { 5 };
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);
@@ -560,6 +715,7 @@ fn serialize_unsigned_record(record: &UserRecord) -> Result<Vec<u8>> {
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
@@ -650,6 +806,29 @@ fn write_access_servers_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<
Ok(())
}
fn write_sessions_block(out: &mut Vec<u8>, record: &UserRecord) -> Result<()> {
out.push(BLOCK_TYPE_SESSIONS);
out.push(BLOCK_VERSION_0);
out.push(record.sessions_mode);
require!(
record.sessions.len() <= MAX_SESSIONS,
ErrCode::InvalidRecordData
);
out.push(record.sessions.len() as u8);
for session in &record.sessions {
write_session_record(out, session)?;
}
Ok(())
}
fn write_session_record(out: &mut Vec<u8>, session: &SessionRecord) -> Result<()> {
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);
@@ -696,6 +875,19 @@ fn read_blockchain_record(data: &[u8], cursor: &mut usize) -> Result<BlockchainR
})
}
fn read_session_record(data: &[u8], cursor: &mut usize) -> Result<SessionRecord> {
let session_type = read_u8(data, cursor)?;
let session_version = read_u8(data, cursor)?;
let session_name = read_len_prefixed_string(data, cursor)?;
let session_pub_key = Pubkey::new_from_array(read_fixed_32(data, cursor)?);
Ok(SessionRecord {
session_type,
session_version,
session_name,
session_pub_key,
})
}
fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
require!(raw.len() >= 9, ErrCode::InvalidRecordData);
require!(&raw[0..5] == MAGIC, ErrCode::InvalidRecordMagic);
@@ -727,6 +919,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
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 {
@@ -771,6 +965,15 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
access_servers.push(read_len_prefixed_string(useful, &mut cursor)?);
}
}
BLOCK_TYPE_SESSIONS => {
require!(sessions.is_empty(), ErrCode::InvalidRecordData);
sessions_mode = read_u8(useful, &mut cursor)?;
let sessions_count = read_u8(useful, &mut cursor)? as usize;
require!(sessions_count <= MAX_SESSIONS, ErrCode::InvalidRecordData);
for _ in 0..sessions_count {
sessions.push(read_session_record(useful, &mut cursor)?);
}
}
BLOCK_TYPE_TRUSTED_STATE => {
trusted_count = read_u8(useful, &mut cursor)?;
}
@@ -778,6 +981,7 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
}
}
validate_sessions_fields(sessions_mode, &sessions)?;
let signature = read_fixed_64(useful, &mut cursor)?;
require!(cursor == useful.len(), ErrCode::InvalidRecordLength);
@@ -796,6 +1000,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result<UserRecord> {
server_address,
sync_servers,
access_servers,
sessions_mode,
sessions,
trusted_count,
signature,
})
@@ -809,20 +1015,19 @@ fn hash_unsigned_record(record: &UserRecord) -> Result<[u8; 32]> {
Ok(out)
}
fn verify_record_signature(
fn verify_record_signature_hash(
instructions_sysvar: &AccountInfo,
root_key: &Pubkey,
signature: &[u8],
unsigned: &[u8],
message_hash: &[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(),
message_hash,
)?;
Ok(provided_sig)
}
@@ -1007,6 +1212,46 @@ fn validate_fields(fields: &UserMutableFields) -> Result<()> {
ErrCode::InvalidRecordData
);
}
validate_sessions_fields(fields.sessions_mode, &fields.sessions)?;
Ok(())
}
fn validate_sessions_fields(mode: u8, sessions: &[SessionRecord]) -> Result<()> {
require!(
mode == SESSIONS_MODE_MIXED || mode == SESSIONS_MODE_PDA_ONLY,
ErrCode::InvalidRecordData
);
require!(sessions.len() <= MAX_SESSIONS, ErrCode::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,
ErrCode::InvalidRecordData
);
require!(
sessions[i].session_pub_key != sessions[j].session_pub_key,
ErrCode::InvalidRecordData
);
}
}
Ok(())
}
fn validate_session_record(session: &SessionRecord) -> Result<()> {
require!(
session.session_type == SESSION_TYPE_USER || session.session_type == SESSION_TYPE_SUBSERVER,
ErrCode::InvalidRecordData
);
require!(session.session_version == 1, ErrCode::InvalidRecordData);
let bytes = session.session_name.as_bytes();
require!(!bytes.is_empty(), ErrCode::InvalidRecordData);
require!(bytes.len() <= MAX_SESSION_NAME_LEN, ErrCode::InvalidRecordData);
for &b in bytes {
let ok = b.is_ascii_alphanumeric() || b == b'_';
require!(ok, ErrCode::InvalidRecordData);
}
Ok(())
}