diff --git a/Dev_Docs/Personal_Messages/README.md b/Dev_Docs/Personal_Messages/README.md index 72902f9..84812aa 100644 --- a/Dev_Docs/Personal_Messages/README.md +++ b/Dev_Docs/Personal_Messages/README.md @@ -216,7 +216,34 @@ UI чата строится на этих типах: текстовые соо - при открытии диалога UI автопрокручивает ленту в самый низ; - после отправки нового сообщения UI сразу прокручивает ленту вниз. -## 10) Инварианты (обязательно соблюдать при доработках) +## 10) Синхронизация личных сообщений между серверами + +Когда пользователи зарегистрированы на разных серверах SHiNE, серверы должны синхронизировать DM между собой. + +### Общий принцип + +- Сервер A получает DM-блок, адресованный пользователю на сервере B. +- Сервер A пересылает этот блок серверу B (межсерверный relay). +- Сервер B сохраняет блок и доставляет его в активные сессии получателя. +- Серверы, между которыми идёт синхронизация, задаются списком `sync_servers` в PDA пользователя-сервера. + +### Что синхронизируется + +- Все DM-блоки типов `1/2` (текстовые сообщения) и `3/4` (read-receipt). +- Синхронизация двусторонняя: оба сервера должны уметь принимать и пересылать блоки. + +### Идемпотентность + +- Блоки имеют уникальный `message_key` (`from|to|timeMs|nonce|type`). +- Повторная доставка одного и того же блока безопасна — дедупликация происходит по `message_key`. + +### Статус реализации + +Межсерверная синхронизация DM **пока не реализована**. Текущая версия работает только в рамках одного сервера. Это задача для следующего этапа. + +--- + +## 11) Инварианты (обязательно соблюдать при доработках) 1. Пара блоков (1/2 или 3/4) должна оставаться атомарной. 2. `messageKey`/`baseKey` формат должен быть совместим с текущей логикой дедупликации и receipt. @@ -224,7 +251,7 @@ UI чата строится на этих типах: текстовые соо 4. Read-receipt не должен отправляться многократно на один и тот же `baseKey`. 5. Любые изменения DM-логики в коде должны сразу отражаться в этом документе. -## 11) Ключевые файлы реализации +## 12) Ключевые файлы реализации - UI: - `shine-UI/js/services/auth-service.js` diff --git a/VERSION.properties b/VERSION.properties index c3de824..b83d272 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.107 -server.version=1.2.100 +client.version=1.2.109 +server.version=1.2.101 diff --git a/shine-UI/js/pages/wallet-view.js b/shine-UI/js/pages/wallet-view.js index b9a8af0..ca6e529 100644 --- a/shine-UI/js/pages/wallet-view.js +++ b/shine-UI/js/pages/wallet-view.js @@ -898,19 +898,22 @@ export function render({ navigate }) { setStatus('Генерация Arweave-кошелька...'); try { + let wasFirstTimeGeneration = false; arweaveWalletCtx = await getArweaveWalletFromStoredDeviceKey({ ...sessionArgsOrThrow(), onStatus: (message) => { const text = String(message || '').trim(); if (!text) return; if (text.includes('впервые получаем Arweave-кошелёк')) { - setStatus('Сейчас мы впервые получаем Arweave-кошелёк из вашего приватного device key. Это может занять немного времени. После этого кошелёк будет храниться только в зашифрованном контейнере этого устройства.'); + wasFirstTimeGeneration = true; + setStatus('Подождите — ваш Arweave-ключ вычисляется из device key. Это происходит только один раз, потом будет мгновенно.'); return; } setStatus(text); }, }); if (modeToken !== activeModeToken) return; + if (wasFirstTimeGeneration) setStatus(''); walletAddress = arweaveWalletCtx.address; addressEl.textContent = walletAddress; await refreshBalance(); diff --git a/shine-UI/js/services/sawd-v1.js b/shine-UI/js/services/sawd-v1.js index 057840a..f9bf68f 100644 --- a/shine-UI/js/services/sawd-v1.js +++ b/shine-UI/js/services/sawd-v1.js @@ -7,7 +7,7 @@ export const DERIVATION_NAME = 'SAWD-v1'; export const MASTER_LABEL = 'SHINE/ARWEAVE/RSA4096/SAWD-v1/MASTER'; export const STREAM_LABEL = 'SHINE/ARWEAVE/RSA4096/SAWD-v1/STREAM'; export const MR_LABEL = 'SHINE/ARWEAVE/RSA4096/SAWD-v1/MILLER-RABIN'; -export const MILLER_RABIN_ROUNDS = 64; +export const MILLER_RABIN_ROUNDS = 42; export const SMALL_PRIME_LIMIT = 10000; function getSubtle() { diff --git a/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md b/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md index 036c130..e4cebb8 100644 --- a/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md +++ b/shine-solana/shine/doc/SHiNE-user-format-v.1.0.md @@ -230,7 +230,8 @@ ServerProfileBlock - block_type: u8 = 30 - block_version: u8 = 0 - is_server: u8 -- server_key: [u8; 32], только если is_server = 1 +- address_format_type: u8, только если is_server = 1 +- address_format_version: u8, только если is_server = 1 - server_address: string, только если is_server = 1 - sync_servers_count: u8, только если is_server = 1 - sync_servers: string[sync_servers_count], только если is_server = 1 @@ -240,9 +241,11 @@ ServerProfileBlock - `is_server = 0` означает, что серверных данных нет; - `is_server = 1` означает, что пользователь публикует серверный профиль; +- `address_format_type` — тип формата адреса сервера: `1` = URL-строка (например `https://shineup.me/ws`); +- `address_format_version` — версия формата адреса, сейчас `0`; - `sync_servers_count` максимум `32`; -- `server_address` - строковый адрес сервера в формате, который будет отдельно закреплен на уровне приложения; -- `sync_servers` - логины пользователей системы, через которых этот сервер пытается синхронизироваться. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы. +- `server_address` - строковый адрес сервера в соответствии с `address_format_type`; +- `sync_servers` - логины SHiNE-пользователей, зарегистрированных как серверы, с которыми этот сервер синхронизирует блокчейн и личные сообщения. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы. ## 12. AccessServersBlock diff --git a/shine-solana/shine/programs/shine_users/src/users.rs b/shine-solana/shine/programs/shine_users/src/users.rs index 7a30cfe..8efa6c8 100644 --- a/shine-solana/shine/programs/shine_users/src/users.rs +++ b/shine-solana/shine/programs/shine_users/src/users.rs @@ -38,7 +38,8 @@ pub struct UserMutableFields { pub last_block_signature: Vec, pub arweave_tx_id: String, pub is_server: bool, - pub server_key: Pubkey, + pub address_format_type: u8, + pub address_format_version: u8, pub server_address: String, pub sync_servers: Vec, pub access_servers: Vec, @@ -78,7 +79,8 @@ pub struct UserRecord { pub device_key: Pubkey, pub blockchain: BlockchainRecord, pub is_server: bool, - pub server_key: Pubkey, + pub address_format_type: u8, + pub address_format_version: u8, pub server_address: String, pub sync_servers: Vec, pub access_servers: Vec, @@ -298,7 +300,8 @@ pub fn create_user_pda(ctx: Context, args: CreateUserPdaArgs) -> arweave_tx_id: args.fields.arweave_tx_id.clone(), }, is_server: args.fields.is_server, - server_key: args.fields.server_key, + 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(), @@ -465,7 +468,8 @@ pub fn update_user_pda(ctx: Context, args: UpdateUserPdaArgs) -> arweave_tx_id: args.fields.arweave_tx_id.clone(), }, is_server: args.fields.is_server, - server_key: args.fields.server_key, + 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(), @@ -606,7 +610,8 @@ fn write_server_profile_block(out: &mut Vec, record: &UserRecord) -> Result< out.push(BLOCK_TYPE_SERVER_PROFILE); out.push(BLOCK_VERSION_0); out.push(1); - out.extend_from_slice(record.server_key.as_ref()); + 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, @@ -705,7 +710,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result { let mut device_key: Option = None; let mut blockchain: Option = None; let mut is_server = false; - let mut server_key = Pubkey::default(); + 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(); @@ -737,7 +743,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result { require!(!is_server, ErrCode::InvalidRecordData); is_server = read_u8(useful, &mut cursor)? == 1; require!(is_server, ErrCode::InvalidRecordData); - server_key = Pubkey::new_from_array(read_fixed_32(useful, &mut cursor)?); + 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); @@ -772,7 +779,8 @@ fn deserialize_record_from_pda(raw: &[u8]) -> Result { device_key: device_key.ok_or(error!(ErrCode::InvalidRecordData))?, blockchain: blockchain.ok_or(error!(ErrCode::InvalidRecordData))?, is_server, - server_key, + address_format_type, + address_format_version, server_address, sync_servers, access_servers,