Разработка децентрализованного мессенджера
Главный вопрос при проектировании децентрализованного мессенджера — что именно децентрализовано? Хранение сообщений? Маршрутизация? Identity? Шифрование? Большинство «децентрализованных» мессенджеров централизованы в одном или нескольких из этих слоёв, просто маскируя это блокчейн-обёрткой. Честная архитектура требует явного выбора trade-offs на каждом уровне.
Протокольный стек: выбор компонентов
Transport layer
XMTP (Extensible Message Transport Protocol) — de facto стандарт для Web3 мессенджеров на 2024-2025. Поверх Waku (libp2p-based messaging network). Сообщения хранятся на XMTP нодах (федерированная сеть), идентификация — Ethereum адрес, шифрование — Double Ratchet (как в Signal).
import { Client } from '@xmtp/xmtp-js';
import { Wallet } from 'ethers';
// Создание XMTP идентификатора (подпись кошельком)
const xmtpClient = await Client.create(signer, { env: 'production' });
// Проверка: зарегистрирован ли адрес в XMTP
const isOnNetwork = await Client.canMessage(recipientAddress);
// Создание или открытие conversation
const conversation = await xmtpClient.conversations.newConversation(recipientAddress);
// Отправка сообщения
await conversation.send('Hello from Web3');
// Получение истории
const messages = await conversation.messages({ limit: 50 });
// Streaming новых сообщений
for await (const message of await conversation.streamMessages()) {
console.log(`${message.senderAddress}: ${message.content}`);
}
Преимущество XMTP: готовая E2E шифрование, cross-app (сообщения работают между разными dApp на базе XMTP: Coinbase Wallet, Converse, Lens), не нужно строить p2p инфраструктуру.
Waku (standalone) — если нужна полностью кастомная реализация без зависимости от XMTP. Light node в браузере через @waku/sdk:
import { createLightNode, waitForRemotePeer } from '@waku/sdk';
const node = await createLightNode({ defaultBootstrap: true });
await waitForRemotePeer(node);
const topic = '/my-messenger/1/chat/proto';
const encoder = createEncoder({ contentTopic: topic });
const decoder = createDecoder(topic);
// Подписка
await node.filter.subscribe([decoder], (message) => {
// Обработка входящего сообщения
});
// Публикация
await node.lightPush.send(encoder, { payload: encryptedBytes });
Waku без XMTP даёт больше контроля, но требует самостоятельно реализовать шифрование, identity, доставку приватных сообщений.
Identity и key management
XMTP привязывает идентификатор к Ethereum адресу автоматически. При standalone подходе нужна схема key derivation.
Deterministic key derivation: мастер-ключ шифрования деривируем из подписи детерминированного сообщения. Пользователь подписывает один раз, ключи воспроизводимы на любом устройстве:
async function deriveMessagingKeys(signer: ethers.Signer): Promise<{
identityKey: Uint8Array;
preKey: Uint8Array;
}> {
const message = 'MyMessenger Identity Key v1\n\nThis key is used for encrypted messaging.\nSign to generate your keys.';
const signature = await signer.signMessage(message);
// HKDF из подписи
const keyMaterial = await crypto.subtle.importKey('raw', hexToBytes(signature), 'HKDF', false, ['deriveKey', 'deriveBits']);
const identityKeyBits = await crypto.subtle.deriveBits(
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new TextEncoder().encode('identity-key') },
keyMaterial, 256
);
const preKeyBits = await crypto.subtle.deriveBits(
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new TextEncoder().encode('pre-key') },
keyMaterial, 256
);
return { identityKey: new Uint8Array(identityKeyBits), preKey: new Uint8Array(preKeyBits) };
}
Важно: если пользователь меняет кошелёк — он теряет ключи. Backup механизм критичен.
Шифрование сообщений
Для приватных чатов — X25519 ECDH для key agreement + AES-GCM для шифрования контента:
async function encryptMessage(
plaintext: string,
senderPrivateKey: Uint8Array,
recipientPublicKey: Uint8Array
): Promise<{ ciphertext: Uint8Array; nonce: Uint8Array }> {
// Shared secret через ECDH
const sharedSecret = await performECDH(senderPrivateKey, recipientPublicKey);
// Деривируем ключ шифрования из shared secret
const encryptionKey = await crypto.subtle.importKey(
'raw', sharedSecret, { name: 'AES-GCM' }, false, ['encrypt']
);
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
encryptionKey,
new TextEncoder().encode(plaintext)
);
return { ciphertext: new Uint8Array(ciphertext), nonce };
}
Для группового чата — симметричный ключ группы, зашифрованный публичными ключами каждого участника (sealed sender модель).
Forward secrecy через Double Ratchet
Статичный ECDH ключ — слабость: компрометация ключа раскрывает всю историю переписки. Double Ratchet (алгоритм Signal Protocol) решает это: каждое сообщение шифруется новым эфемерным ключом, деривированным из ratchet состояния. Реализация с нуля — сложно; используйте @signalapp/libsignal-client (официальный WASM порт).
XMTP реализует Double Ratchet внутренне — это одна из причин выбирать его вместо самостоятельной реализации.
Хранение сообщений
Проблема: блокчейн дорог для хранения сообщений. Даже 100 байт текста на Ethereum = несколько долларов. Варианты:
| Хранилище | Децентрализация | Стоимость | Скорость |
|---|---|---|---|
| XMTP nodes | Федерированная | Бесплатно | ~200ms |
| IPFS + Filecoin | Высокая | ~$0.01/GB/месяц | 1-5 сек |
| Ceramic/ComposeDB | Высокая | Бесплатно (light) | ~500ms |
| Arweave | Максимальная | ~$0.005/MB разово | 2-30 сек |
| Собственный сервер | Нет | Дёшево | <50ms |
Для реального UX — гибридная схема: сообщения в XMTP/Waku (fast, p2p), архивные сообщения старше N дней — в IPFS с Filecoin pinning.
Push уведомления
Waku и XMTP не имеют нативного push. Для мобильных уведомлений нужен PUSH service. XMTP поддерживает Push notifications через @xmtp/react-native-sdk + XMTP push service, который можно self-host.
Для web: Service Worker + Web Push API. Паттерн: SW подписывается на XMTP streaming, при новом сообщении — показывает push notification через showNotification.
Групповые чаты
XMTP v3 (MLS — Messaging Layer Security) добавляет нативные группы с E2E шифрованием и forward secrecy для всей группы. Это значительно сложнее, чем диалоги один-на-один: управление membership (add/remove участника требует обновления группового ключа), согласование состояния между клиентами.
// XMTP v3 Group API
const group = await xmtpClient.conversations.newGroup([member1, member2, member3]);
await group.send('Hello group');
// Добавить участника
await group.addMembers([newMemberAddress]);
// Группа пересоздаёт ключи автоматически при изменении состава
On-chain компонент: что стоит хранить в блокчейне
Большинство данных мессенджера НЕ должны идти в блокчейн. Разумно хранить on-chain только:
- Публичные ключи (identity registration) — один раз при первом использовании
- Group registry — список групп, участниками которых является адрес (если публичные группы)
- Токен-gated access — проверка владения NFT/токенами для входа в group chat
ENS интеграция: резолвить name.eth → адрес → XMTP проверка через canMessage. Отображать ENS имена вместо адресов в UI.
Структура фронтенда
src/
components/
ConversationList/ # Список чатов
MessageThread/ # Отображение сообщений
MessageInput/ # Ввод с attachment support
ContactSearch/ # Поиск по ENS/адресу
hooks/
useXmtpClient # Инициализация XMTP
useConversations # Список conversations с streaming
useMessages # Сообщения конкретного чата
stores/ # Zustand / Recoil state
React Query + Zustand — для кэширования данных с XMTP нод. Сообщения кэшируются локально (IndexedDB), streaming добавляет новые без перезагрузки.
Ориентиры по срокам
XMTP-based мессенджер (1-на-1 чаты, ENS резолвинг, базовый UI) — 2-3 недели. Групповые чаты (MLS v3), push уведомления, token-gated rooms — ещё 2-3 недели. Полноценный продукт с file sharing, read receipts, mobile-адаптацией — 2-3 месяца.







