# @opchan/core Architecture Guide Deep dive into the architecture, design patterns, and implementation details of the OpChan Core SDK. --- ## Table of Contents 1. [System Overview](#system-overview) 2. [Core Architecture](#core-architecture) 3. [Data Flow](#data-flow) 4. [Key Subsystems](#key-subsystems) 5. [Cryptographic Design](#cryptographic-design) 6. [Storage Strategy](#storage-strategy) 7. [Network Layer](#network-layer) 8. [Design Patterns](#design-patterns) 9. [Performance Considerations](#performance-considerations) 10. [Security Model](#security-model) --- ## System Overview OpChan Core is a decentralized forum infrastructure built on three pillars: 1. **Cryptographic Identity** - Ed25519 key delegation with wallet authorization 2. **P2P Messaging** - Waku protocol for decentralized communication 3. **Local-First Storage** - IndexedDB with in-memory caching ### Design Philosophy - **Local-First**: All data persisted locally, network as synchronization mechanism - **Optimistic UI**: Immediate feedback, eventual consistency - **Privacy-Preserving**: No centralized servers, peer-to-peer architecture - **Framework-Agnostic**: Pure TypeScript library, no UI framework dependencies --- ## Core Architecture ``` ┌───────────────────────────────────────────────────────────────────┐ │ OpChanClient │ │ Entry point orchestrating all subsystems │ └───────────────────────────────────────────────────────────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ MessageManager │ │ LocalDatabase │ │ DelegationManager│ │ │ │ │ │ │ │ - WakuNode │ │ - IndexedDB │ │ - Key Generation │ │ - Reliable │ │ - In-memory │ │ - Signing │ │ Messaging │ │ Cache │ │ - Verification │ │ - Health Monitor │ │ - Validation │ │ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Service Layer │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ ForumActions │ │ Identity │ │ Relevance │ │ │ │ │ │ Service │ │ Calculator │ │ │ │ - Create │ │ │ │ │ │ │ │ - Moderate │ │ - ENS Lookup │ │ - Scoring │ │ │ │ - Vote │ │ - Profiles │ │ - Time Decay │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ``` ### Component Responsibilities #### OpChanClient - **Role**: Facade pattern - single entry point for all operations - **Responsibilities**: - Instantiate and wire all services - Configure environment - Initialize message manager with Waku config - Provide unified API surface #### MessageManager - **Role**: Network abstraction layer - **Responsibilities**: - Manage Waku node lifecycle - Handle message sending/receiving - Monitor network health - Implement reliable messaging (retries, acknowledgments) - Manage subscriptions to content topics #### LocalDatabase - **Role**: Persistence and caching layer - **Responsibilities**: - IndexedDB operations (async) - In-memory cache (sync) - Message validation - Deduplication - State management (pending, syncing) #### DelegationManager - **Role**: Cryptographic signing system - **Responsibilities**: - Generate Ed25519 keypairs - Create wallet-authorized delegations - Sign messages with browser keys - Verify message signatures - Verify delegation proofs --- ## Data Flow ### Outbound Message Flow (Creating Content) ``` 1. User Action └─> ForumActions.createPost() │ ├─> Validate permissions (wallet or anonymous) │ ├─> Create unsigned message with UUID │ ├─> DelegationManager.signMessage() │ ├─> Load cached delegation │ ├─> Check expiry │ ├─> Sign with browser private key (Ed25519) │ └─> Attach signature + browserPubKey + delegationProof │ ├─> LocalDatabase.applyMessage() │ ├─> Validate signature │ ├─> Check for duplicates │ ├─> Store in cache │ └─> Persist to IndexedDB │ ├─> LocalDatabase.markPending() │ ├─> Call updateCallback() [UI refresh] │ └─> MessageManager.sendMessage() ├─> Encode with protobuf ├─> Send via Waku └─> Wait for acknowledgment └─> LocalDatabase.clearPending() ``` ### Inbound Message Flow (Receiving Content) ``` 1. Waku Network └─> WakuNodeManager receives message │ ├─> Decode protobuf │ ├─> ReliableMessaging handles deduplication │ └─> MessageService.onMessageReceived() callback │ └─> LocalDatabase.applyMessage() │ ├─> MessageValidator.isValidMessage() │ ├─> Check required fields │ ├─> Verify signature (Ed25519) │ ├─> If delegationProof: │ │ ├─> Verify auth message format │ │ ├─> Verify wallet signature (viem) │ │ └─> Check expiry (optional) │ └─> If anonymous: │ └─> Verify session ID format (UUID) │ ├─> Check duplicate (type:id:timestamp key) │ ├─> Store in appropriate cache collection │ ├─> cells[id] │ ├─> posts[id] │ ├─> comments[id] │ ├─> votes[targetId:author] │ ├─> moderations[key] │ └─> userIdentities[address] │ ├─> Persist to IndexedDB │ ├─> Update lastSync timestamp │ └─> Return true (new message) ``` --- ## Key Subsystems ### 1. Delegation Subsystem **Purpose**: Reduce wallet signature prompts while maintaining cryptographic security. **Components**: - **DelegationManager** - Core delegation logic - **DelegationStorage** - IndexedDB persistence - **DelegationCrypto** - Ed25519 signing/verification **Delegation Process (Wallet)**: ```typescript // 1. Generate browser keypair const keypair = DelegationCrypto.generateKeypair(); // { publicKey: string, privateKey: string } // 2. Create authorization message const authMessage = ` Authorize browser key: Public Key: ${keypair.publicKey} Wallet: ${walletAddress} Expires: ${new Date(expiryTimestamp).toISOString()} Nonce: ${nonce} `; // 3. Sign with wallet (happens once) const walletSignature = await signFunction(authMessage); // 4. Store delegation const delegation: WalletDelegationInfo = { authMessage, walletSignature, expiryTimestamp, walletAddress, browserPublicKey: keypair.publicKey, browserPrivateKey: keypair.privateKey, nonce }; await DelegationStorage.store(delegation); // 5. Subsequent messages signed with browser key const signature = DelegationCrypto.signRaw(messageJson, privateKey); ``` **Delegation Process (Anonymous)**: ```typescript // 1. Generate browser keypair const keypair = DelegationCrypto.generateKeypair(); // 2. Generate session ID (UUID) const sessionId = crypto.randomUUID(); // 3. Store anonymous delegation const delegation: AnonymousDelegationInfo = { sessionId, browserPublicKey: keypair.publicKey, browserPrivateKey: keypair.privateKey, expiryTimestamp, nonce }; // 4. No wallet signature required // User address = sessionId ``` **Verification Process**: ```typescript // 1. Verify message signature const messagePayload = JSON.stringify({ ...message, signature: undefined, browserPubKey: undefined, delegationProof: undefined }); const signatureValid = DelegationCrypto.verifyRaw( messagePayload, message.signature, message.browserPubKey ); if (!signatureValid) return false; // 2. Verify delegation authorization if (message.delegationProof) { // Wallet user - verify delegation proof const proofValid = await DelegationCrypto.verifyWalletSignature( message.delegationProof.authMessage, message.delegationProof.walletSignature, message.delegationProof.walletAddress ); // Check auth message contains browser key, wallet address, expiry const authMessageValid = message.delegationProof.authMessage.includes(message.browserPubKey) && message.delegationProof.authMessage.includes(message.author) && message.delegationProof.authMessage.includes( message.delegationProof.expiryTimestamp.toString() ); return proofValid && authMessageValid; } else { // Anonymous user - verify session ID format return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( message.author ); } ``` --- ### 2. Storage Subsystem **Purpose**: Fast local access with persistent storage. **Two-Tier Architecture**: 1. **In-Memory Cache** (Tier 1) - Synchronous access - Fast reads for UI rendering - Volatile (resets on page reload) 2. **IndexedDB** (Tier 2) - Asynchronous access - Persistent across sessions - Hydrates cache on startup **IndexedDB Schema**: ```typescript const schema = { // Content stores (keyPath = 'id') cells: { keyPath: 'id' }, posts: { keyPath: 'id' }, comments: { keyPath: 'id' }, // Votes (keyPath = 'key', composite: targetId:author) votes: { keyPath: 'key' }, // Moderations (keyPath = 'key', composite varies by type) moderations: { keyPath: 'key' }, // User identities (keyPath = 'address') userIdentities: { keyPath: 'address', indexes: [] }, // Bookmarks (keyPath = 'id') bookmarks: { keyPath: 'id', indexes: [{ name: 'by_userId', keyPath: 'userId' }] }, // Auth/state stores (keyPath = 'key') userAuth: { keyPath: 'key' }, delegation: { keyPath: 'key' }, uiState: { keyPath: 'key' }, meta: { keyPath: 'key' } }; ``` **Cache Synchronization**: ``` ┌─────────────────────────────────────────────────────┐ │ Write Operation │ │ │ │ 1. Update in-memory cache (immediate) │ │ 2. Write to IndexedDB (async, fire-and-forget) │ │ 3. Notify listeners (for UI update) │ └─────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────┐ │ Read Operation │ │ │ │ 1. Check in-memory cache (fast path) │ │ 2. If not found, return null/empty │ │ (IndexedDB only used for hydration on startup) │ └─────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────┐ │ Startup Hydration │ │ │ │ 1. Open IndexedDB connection │ │ 2. Load all stores in parallel │ │ 3. Populate in-memory cache │ │ 4. Ready for use │ └─────────────────────────────────────────────────────┘ ``` **Deduplication**: Messages are deduplicated using a composite key: ```typescript const messageKey = `${message.type}:${message.id}:${message.timestamp}`; ``` This allows the same logical message to be received multiple times without creating duplicates. --- ### 3. Identity Subsystem **Purpose**: Resolve and cache user identities with ENS integration. **Resolution Strategy**: ``` ┌────────────────────────────────────────────────────┐ │ Identity Resolution Flow │ │ │ │ 1. Check LocalDatabase cache │ │ └─> If found and fresh (< 5 min): return │ │ │ │ 2. Check if Ethereum address │ │ └─> If not (anonymous/UUID): return null │ │ │ │ 3. Resolve ENS via PublicClient │ │ ├─> Get ENS name │ │ ├─> Get ENS avatar │ │ └─> Cache result for 5 minutes │ │ │ │ 4. Build UserIdentity object │ │ ├─> address │ │ ├─> ensName │ │ ├─> ensAvatar │ │ ├─> callSign (from profile messages) │ │ ├─> displayPreference │ │ ├─> displayName (computed) │ │ ├─> verificationStatus (computed) │ │ └─> lastUpdated │ │ │ │ 5. Store in LocalDatabase │ │ │ │ 6. Return identity │ └────────────────────────────────────────────────────┘ ``` **Display Name Resolution**: ```typescript function getDisplayName(identity: UserIdentity): string { // Priority 1: Call sign (if preference is CALL_SIGN) if ( identity.callSign && identity.displayPreference === EDisplayPreference.CALL_SIGN ) { return identity.callSign; } // Priority 2: ENS name if (identity.ensName) { return identity.ensName; } // Priority 3: Shortened address return `${identity.address.slice(0, 6)}...${identity.address.slice(-4)}`; } ``` **Profile Updates**: User profile updates are broadcast as USER_PROFILE_UPDATE messages: ```typescript { type: MessageType.USER_PROFILE_UPDATE, id: uuid(), timestamp: Date.now(), author: userAddress, callSign: 'alice', // optional displayPreference: EDisplayPreference.CALL_SIGN, signature: '...', browserPubKey: '...', delegationProof: { ... } } ``` These messages update the `userIdentities` cache, propagating changes across the network. --- ### 4. Relevance Subsystem **Purpose**: Score content based on engagement, verification, time, and moderation. **Scoring Algorithm**: ```typescript interface RelevanceFactors { base: 100; engagement: { upvoteWeight: 10; commentWeight: 3; }; verification: { authorBonus: 20; // ENS verified author upvoteBonus: 5; // Per ENS verified upvoter commenterBonus: 10; // Per ENS verified commenter }; timeDecay: { halfLifeDays: 7; formula: 'exponential'; // exp(-0.693 * days / halfLife) }; moderation: { penalty: 0.5; // 50% reduction if moderated }; } function calculateScore( post: Post, votes: Vote[], comments: Comment[], verifications: Map, moderations: Map ): number { // Base score let score = 100; // Engagement const upvotes = votes.filter(v => v.value === 1).length; const downvotes = votes.filter(v => v.value === -1).length; score += (upvotes * 10) + (comments.length * 3); // Verification bonuses if (verifications.get(post.author)) { score += 20; // Author ENS verified } const verifiedUpvoters = votes .filter(v => v.value === 1 && verifications.get(v.author)) .length; score += verifiedUpvoters * 5; const verifiedCommenters = new Set( comments .map(c => c.author) .filter(author => verifications.get(author)) ).size; score += verifiedCommenters * 10; // Time decay (exponential) const daysOld = (Date.now() - post.timestamp) / (1000 * 60 * 60 * 24); const decay = Math.exp(-0.693 * daysOld / 7); // Half-life 7 days score *= decay; // Moderation penalty if (moderations.has(post.id)) { score *= 0.5; } return Math.max(0, score); } ``` **Score Components Breakdown**: ```typescript interface RelevanceScoreDetails { baseScore: 100, engagementScore: (upvotes * 10) + (comments * 3), authorVerificationBonus: isENS ? 20 : 0, verifiedUpvoteBonus: verifiedUpvoters * 5, verifiedCommenterBonus: verifiedCommenters * 10, timeDecayMultiplier: exp(-0.693 * daysOld / 7), moderationPenalty: isModerated ? 0.5 : 1.0, finalScore: (base + engagement + bonuses) * decay * modPenalty } ``` --- ## Cryptographic Design ### Key Hierarchy ``` Wallet Private Key (User's wallet, never exposed) │ ├─> Signs authorization message │ └─> Stored in delegationProof.walletSignature │ └─> Authorizes ─────────────────────────┐ │ Browser Private Key (Generated, stored locally) │ │ ├─> Signs all messages │ │ └─> Stored in message.signature │ │ │ └─> Public key: message.browserPubKey ───┘ ``` ### Cryptographic Primitives **Ed25519** (via @noble/ed25519): - Browser key generation - Message signing - Signature verification **ECDSA** (via viem): - Wallet signature verification - ENS resolution ### Message Signature Structure ```typescript // Unsigned message const unsignedMessage = { type: 'post', id: 'abc123', cellId: 'xyz789', title: 'Hello', content: 'World', timestamp: 1234567890, author: '0x...' }; // Message to sign (excludes signature fields) const messageToSign = JSON.stringify({ ...unsignedMessage, signature: undefined, browserPubKey: undefined, delegationProof: undefined }); // Sign with browser private key const signature = await ed25519.sign( sha512(messageToSign), browserPrivateKey ); // Signed message const signedMessage = { ...unsignedMessage, signature: bytesToHex(signature), browserPubKey: bytesToHex(browserPublicKey), delegationProof: { authMessage: '...', walletSignature: '...', expiryTimestamp: 1234567890, walletAddress: '0x...' } }; ``` ### Verification Logic ```typescript async function verifyMessage(message: OpchanMessage): Promise { // 1. Verify message signature with browser public key const messagePayload = JSON.stringify({ ...message, signature: undefined, browserPubKey: undefined, delegationProof: undefined }); const signatureValid = await ed25519.verify( hexToBytes(message.signature), sha512(messagePayload), hexToBytes(message.browserPubKey) ); if (!signatureValid) return false; // 2. Verify delegation authorization if (message.delegationProof) { // Wallet user - verify wallet signature on auth message const { authMessage, walletSignature, walletAddress } = message.delegationProof; const walletSigValid = await verifyMessage({ account: walletAddress, message: authMessage, signature: walletSignature }); if (!walletSigValid) return false; // Verify auth message contains browser key and expiry if (!authMessage.includes(message.browserPubKey)) return false; if (!authMessage.includes(walletAddress)) return false; if (!authMessage.includes(message.delegationProof.expiryTimestamp.toString())) { return false; } return true; } else { // Anonymous user - verify session ID is valid UUID return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( message.author ); } } ``` --- ## Network Layer ### Waku Protocol Integration **Architecture**: ``` ┌─────────────────────────────────────────────────────┐ │ MessageManager │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ WakuNodeManager │ │ │ │ - Create Waku node │ │ │ │ - Connect to bootstrap peers │ │ │ │ - Monitor health │ │ │ │ - Emit health events │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ ReliableMessaging │ │ │ │ - Store & Forward protocol │ │ │ │ - Message deduplication │ │ │ │ - Acknowledgments │ │ │ │ - Retries │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────┐ │ │ │ MessageService │ │ │ │ - Send messages │ │ │ │ - Receive subscriptions │ │ │ │ - Codec management (protobuf) │ │ │ └────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ ``` **Content Topics**: Messages are published/subscribed via content topics: ```typescript const contentTopic = '/opchan/1/messages/proto'; // All message types share the same content topic // Filtering happens at application layer based on message.type ``` **Message Encoding**: Messages are encoded with protobuf before transmission (handled by Waku SDK). **Reliable Messaging**: The `ReliableMessaging` layer provides: 1. **Store-and-Forward**: Messages cached for offline peers 2. **Deduplication**: Filter out duplicate receives 3. **Acknowledgments**: Confirm message delivery 4. **Retries**: Resend on failure --- ## Design Patterns ### 1. Singleton Pattern **DelegationManager**, **LocalDatabase**, **MessageManager** are exported as singletons: ```typescript // Singleton instance export const delegationManager = new DelegationManager(); export const localDatabase = new LocalDatabase(); export default new DefaultMessageManager(); ``` ### 2. Facade Pattern **OpChanClient** acts as a facade, providing a simplified interface: ```typescript class OpChanClient { // Aggregates all services readonly delegation: DelegationManager; readonly database: LocalDatabase; readonly messageManager: DefaultMessageManager; readonly forumActions: ForumActions; // ... } ``` ### 3. Observer Pattern Event subscriptions throughout: ```typescript // MessageManager messageManager.onMessageReceived(callback); messageManager.onHealthChange(callback); messageManager.onSyncStatus(callback); // UserIdentityService userIdentityService.subscribe(callback); // LocalDatabase database.onPendingChange(callback); ``` ### 4. Strategy Pattern **MessageValidator** validates different message types: ```typescript class MessageValidator { async isValidMessage(message: unknown): Promise { // Different validation logic per message type switch (message.type) { case MessageType.CELL: return this.validateCell(message); case MessageType.POST: return this.validatePost(message); // ... } } } ``` ### 5. Repository Pattern **LocalDatabase** acts as a repository abstracting storage: ```typescript interface Repository { getById(id: string): T | null; getAll(): T[]; save(item: T): Promise; delete(id: string): Promise; } // LocalDatabase implements repository pattern for each entity type ``` --- ## Performance Considerations ### 1. In-Memory Caching All reads are synchronous from in-memory cache: ```typescript // Fast - synchronous cache access const posts = Object.values(client.database.cache.posts); // No IndexedDB reads during normal operation ``` ### 2. Lazy Identity Resolution Identities resolved on-demand with caching: ```typescript // First call: async ENS lookup const identity1 = await getIdentity(address); // Subsequent calls: cache hit (fast) const identity2 = await getIdentity(address); // same address ``` ### 3. Debouncing Identity lookups are debounced to avoid redundant calls: ```typescript // Multiple rapid calls getIdentity(address); // Starts timer getIdentity(address); // Resets timer getIdentity(address); // Resets timer // Only executes once after 100ms ``` ### 4. Batch Operations IndexedDB writes are batched: ```typescript // Single transaction for multiple writes const tx = db.transaction(['cells', 'posts'], 'readwrite'); tx.objectStore('cells').put(cell); tx.objectStore('posts').put(post); await tx.complete; ``` ### 5. Optimistic UI Immediate feedback with pending states: ```typescript // 1. Write to cache immediately cache.posts[postId] = post; // 2. Mark as pending markPending(postId); // 3. Update UI (shows post with "syncing" badge) updateUI(); // 4. Send to network (async) sendMessage(post); // 5. Clear pending when confirmed clearPending(postId); ``` --- ## Security Model ### Threat Model **Trusted**: - User's device and browser - User's wallet private key **Untrusted**: - Network peers - Network infrastructure - Message content ### Security Guarantees 1. **Message Authenticity** - All messages signed with browser key - Browser key authorized by wallet (for wallet users) - Anonymous users sign with session key (no wallet) 2. **Message Integrity** - Signatures cover entire message payload - Any modification invalidates signature 3. **Non-Repudiation** - Messages cryptographically tied to author - Cannot deny authorship of signed messages 4. **Replay Protection** - Deduplication prevents replayed messages - Timestamps in message payloads ### Attack Resistance **Impersonation**: - ❌ Prevented: Cannot forge signature without private key **Message Tampering**: - ❌ Prevented: Modified messages fail signature verification **Replay Attacks**: - ✅ Mitigated: Deduplication based on (type:id:timestamp) - ⚠️ Limited: Same message can be replayed with different timestamp **Sybil Attacks**: - ⚠️ Possible: Anonymous users can create multiple sessions - ✅ Mitigated: ENS-verified users have higher trust/scoring **DoS Attacks**: - ⚠️ Possible: Can flood network with messages - ✅ Mitigated: Client-side validation rejects malformed messages --- **End of Architecture Guide**