<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>RippleMessenger (RM)</title>
    <link>https://w2solo.com/RippleMessenger</link>
    <description/>
    <language>en-us</language>
    <item>
      <title>通过 agent 分析 RippleMessenger 源码梳理的项目设计思路和逻辑</title>
      <description>&lt;h2 id="RippleMessenger 系统设计文档"&gt;RippleMessenger 系统设计文档&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;范围: RippleMessengerClient + RippleMessengerServer 的通讯协议与去中心化实现
目的: 记录系统自身的设计思想、密码学原则和实现逻辑&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#1-%E8%BA%AB%E4%BB%BD%E7%B3%BB%E7%BB%9F--%E5%AF%86%E7%A0%81%E5%AD%A6%E5%8D%B3%E6%B3%A8%E5%86%8C" title=""&gt;身份系统 — 密码学即注册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#2-%E7%AD%BE%E5%90%8D%E6%9C%BA%E5%88%B6--zero-trust-%E6%B6%88%E6%81%AF%E9%AA%8C%E8%AF%81" title=""&gt;签名机制 — Zero Trust 消息验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3-%E5%85%AC%E5%91%8A%E9%93%BE--hash-linked-per-address-chain" title=""&gt;公告链 — Hash-Linked Per-Address Chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4-dhsequence-%E5%88%86%E5%8C%BA%E7%B3%BB%E7%BB%9F--%E7%A1%AE%E5%AE%9A%E6%80%A7%E5%AF%86%E9%92%A5%E8%BD%AE%E6%8D%A2" title=""&gt;DHSequence 分区系统 — 确定性密钥轮换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#5-eecd-%E6%8F%A1%E6%89%8B%E5%8D%8F%E8%AE%AE--%E7%A7%81%E8%81%8A%E4%B8%8E%E7%BE%A4%E8%81%8A%E7%AB%AF%E5%88%B0%E7%AB%AF%E5%8A%A0%E5%AF%86" title=""&gt;ECDH 握手协议 — 私聊与群聊端到端加密&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#6-%E6%B6%88%E6%81%AF%E9%93%BE%E9%AA%8C%E8%AF%81--%E7%A7%81%E8%81%8A%E5%AE%8C%E6%95%B4%E6%80%A7%E4%BF%9D%E8%AF%81" title=""&gt;消息链验证 — 私聊完整性保证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#7-%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE--nonce-based-%E5%88%86%E5%9D%97%E5%8A%A0%E5%AF%86%E4%BC%A0%E8%BE%93" title=""&gt;文件传输协议 — Nonce-based 分块加密传输&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#8-%E5%8D%8F%E8%AE%AE%E8%B7%AF%E7%94%B1%E7%9F%A9%E9%98%B5--actioncodeobjecttype-%E5%8F%8C%E8%BD%B4%E5%88%86%E5%8F%91" title=""&gt;协议路由矩阵 — ActionCode/ObjectType 双轴分发&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="#9-%E6%95%B0%E6%8D%AE%E5%B1%82%E8%AE%BE%E8%AE%A1---client-sqlite--server-postgresql-%E5%8F%8C%E5%AD%98%E5%82%A8" title=""&gt;数据层设计 — Client SQLite + Server PostgreSQL 双存储&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="#10-%E6%B6%88%E6%81%AF%E4%BA%A4%E4%BA%92%E4%B8%8E%E5%90%8C%E6%AD%A5%E5%8D%8F%E8%AE%AE---%E4%BB%8E%E7%99%BB%E5%BD%95%E5%88%B0%E7%A6%BB%E7%BA%BF%E6%81%A2%E5%A4%8D%E7%9A%84%E5%AE%8C%E6%95%B4%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" title=""&gt;消息交互与同步协议 — 从登录到离线恢复的完整生命周期&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#11-%E8%81%94%E9%82%A6%E7%BD%91%E7%BB%9C--declare-%E5%8F%8C%E9%87%8D%E7%94%A8%E9%80%94%E4%B8%8E%E8%8A%82%E7%82%B9%E5%90%8C%E6%AD%A5" title=""&gt;联邦网络 — Declare 双重用途与节点同步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#12-%E5%AE%89%E5%85%A8%E6%A8%A1%E5%9E%8B%E4%B8%8E%E8%AE%BE%E8%AE%A1%E6%9D%83%E8%A1%A1" title=""&gt;安全模型与设计权衡&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#13-%E8%AE%BE%E8%AE%A1%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%E6%80%BB%E7%BB%93" title=""&gt;设计核心思想总结&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="1. 身份系统 — 密码学即注册"&gt;1. 身份系统 — 密码学即注册&lt;/h2&gt;&lt;h3 id="1.1 Seed 即身份"&gt;1.1 Seed 即身份&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户拥有: XRPL Seed (私钥助记词)
  ↓ ripple-keypairs.deriveKeypair()
公钥 (PublicKey, hex)
  ↓ ripple-keypairs.deriveAddress()
XRPL 地址 (rXXXXXXXXXXXXXXXXXXXXX)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;无注册机制。&lt;/strong&gt; RippleMessenger 没有用户表、没有邮箱验证、没有密码重置。身份完全由密码学密钥对定义:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ripple-keypairs&lt;/code&gt; (XRPL 库) 基于 secp256k1 EdDSA (主网) 或 Ed25519 (测试网)&lt;/li&gt;
&lt;li&gt;Seed → PublicKey → Address 的派生是确定性的，全球唯一&lt;/li&gt;
&lt;li&gt;谁持有私钥，谁就是该地址。私钥永不离开客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="1.2 登录 = 自认证"&gt;1.2 登录 = 自认证&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client 启动流程&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt; &lt;span class="nx"&gt;恢复&lt;/span&gt; &lt;span class="nx"&gt;Seed&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;AES&lt;/span&gt; &lt;span class="nx"&gt;加密种子&lt;/span&gt;
  &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="nx"&gt;handleLogin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
  &lt;span class="nx"&gt;updateAccountUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// 本地 SQLite 标记活跃&lt;/span&gt;
  &lt;span class="nx"&gt;loginSuccess&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;       &lt;span class="c1"&gt;// Redux: 有私钥 = 已登录&lt;/span&gt;
  &lt;span class="nx"&gt;LoadContactList&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;LoadServerList&lt;/span&gt; &lt;span class="c1"&gt;// 从本地 DB 恢复社交图&lt;/span&gt;
  &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="nx"&gt;Declare&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EdDSA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键区别:&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;传统系统&lt;/th&gt;
&lt;th&gt;RippleMessenger&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注册中心 (user 表 + email 验证)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;无注册&lt;/strong&gt; — Seed 即身份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server 发放 session token / JWT&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Server 不发放任何凭证&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;账号可被封禁&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;不可封禁&lt;/strong&gt; — 身份不在服务端数据库中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户名可能重复/抢注&lt;/td&gt;
&lt;td&gt;XRPL 地址密码学唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Server 对 Declare 消息只验证签名有效，不注册、不授权。&lt;code&gt;Conns[address] = ws&lt;/code&gt; 只是记录 WebSocket 连接，不做任何认证状态的持久化。&lt;/p&gt;
&lt;h3 id="1.3 去中心化意义"&gt;1.3 去中心化意义&lt;/h3&gt;
&lt;p&gt;身份系统借鉴了 &lt;strong&gt;比特币的地址模型&lt;/strong&gt;: 全球唯一、无需许可、抗审查。任何持有 XRPL 私钥的人都能使用 RippleMessenger，不需要 Server 批准。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 签名机制 — Zero Trust 消息验证"&gt;2. 签名机制 — Zero Trust 消息验证&lt;/h2&gt;&lt;h3 id="2.1 每条消息必须签名"&gt;2.1 每条消息必须签名&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MessageGenerator.signJson()&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;signJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;json_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QuarterSHA512Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// SHA-512 前 32 hex char&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rippleKeyPairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;签名覆盖消息全部内容 (JSON.stringify 后 SHA-512 截断哈希)。验证方用 &lt;code&gt;ripple-keypairs.verify(hash, sig, publicKey)&lt;/code&gt; 确认来源。&lt;/p&gt;
&lt;h3 id="2.2 双层验证流水线"&gt;2.2 双层验证流水线&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Server 端:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WS 收到文本消息
  ↓
Phase 1: JSON.parse() → object
  ↓
Phase 2: MsgValidate(strJson) — AJV Schema 验证
  (根据 Action 或 ObjectType 选对应 Schema)
  ↓
Phase 3: VerifyJsonSignature(json) — EdDSA 签名验证
  ↓
Phase 4: Route to business logic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Client 端:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WS 收到消息
  ↓
checkBulletinSchema(json) &amp;amp;&amp;amp; VerifyJsonSignature(json)
  ↓ (任一失败 → 丢弃)
路由到对应处理函数
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="2.3 QuarterSHA512 — 有意识的碰撞风险取舍"&gt;2.3 QuarterSHA512 — 有意识的碰撞风险取舍&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SHA-512 全输出 = 128 hex char (512 bits)&lt;/span&gt;
&lt;span class="c1"&gt;// QuarterSHA512 = 前 32 hex char (128 bits) 用于签名哈希&lt;/span&gt;
&lt;span class="c1"&gt;// 显示 Hash = 前 10 hex char (40 bits) 用于 UI 展示&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;QuarterSHA512Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SHA512&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 128 bits — 签名足够安全&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;截断长度&lt;/th&gt;
&lt;th&gt;碰撞风险&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;消息签名哈希&lt;/td&gt;
&lt;td&gt;32 hex (128 bit)&lt;/td&gt;
&lt;td&gt;可忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件/公告展示 Hash&lt;/td&gt;
&lt;td&gt;10 hex (40 bit)&lt;/td&gt;
&lt;td&gt;~100 万条后 50% — 但&lt;strong&gt;签名是安全底线&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;设计逻辑:&lt;/strong&gt; 即使 hash 碰撞，伪造者也无法生成有效 EdDSA 签名。Hash 仅用于去重和 UI 引用，签名才是防篡改的真实防线。这是 &lt;strong&gt;Usability &amp;gt; Pure Cryptography 的有意识 trade-off&lt;/strong&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="3. 公告链 — Hash-Linked Per-Address Chain"&gt;3. 公告链 — Hash-Linked Per-Address Chain&lt;/h2&gt;&lt;h3 id="3.1 链结构"&gt;3.1 链结构&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;每个 XRPL 地址独立维护一条公告链:

Genesis Hash "44F8764BCACFF5424D4044B784549A1B"
  │
  ▼
┌─────────────────────────────┐
│ sequence_number: 1          │
│ content: "Hello World!"     │
│ pre_hash: GenesisHash      │ ◄── 指向创世
│ hash: H1 = QuarterSHA512(   │
│    full_json_without_sig)   │ ◄── 内容哈希
│ signature: EdDSA(H1)       │ ◄── 私钥签名
│ timestamp                  │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ sequence_number: 2          │
│ content: "Second post"      │
│ pre_hash: H1               │ ◄── 指向上一条的 hash
│ hash: H2                   │
│ signature: EdDSA(H2)       │
│ tag: ["news"]              │
│ quote: [{hash: H1}]        │ ◄── 引用其他公告
│ file: [{hash, size}]       │
└──────────┬──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3.2 Server 端的链维护"&gt;3.2 Server 端的链维护&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CacheBulletin() — main.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;CacheBulletin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFromNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QuarterSHA512Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deriveAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// upsert — hash 唯一键，幂等写入&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Bulletin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pre_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// 新记录才链接: 更新上一条的 next_hash → 当前 hash&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isNewRecord&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Bulletin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PreHash&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;next_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 白名单地址才广播到其他节点&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAddressAllowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;broadcastBulletinToNodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3.3 "&gt;3.3 "去共识区块链"设计&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blockchain:     hash-linking + signature + consensus + mining + incentive
Bulletin Chain: hash-linking + signature + [单写者, 无需共识]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么不需要共识?&lt;/strong&gt; 每条链只有一个合法生产者——一个地址只对应一个私钥持有者。无竞争 = 无需 PoW/PoS。这是 &lt;strong&gt;Blockchain 思想的"单写者模式"&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="3.4 完整性保证"&gt;3.4 完整性保证&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;实现&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不可篡改&lt;/td&gt;
&lt;td&gt;pre_hash → next_hash 双向链接&lt;/td&gt;
&lt;td&gt;改一条则整条链断裂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有序性&lt;/td&gt;
&lt;td&gt;sequence_number 单调递增&lt;/td&gt;
&lt;td&gt;消息不可重排&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;来源可验证&lt;/td&gt;
&lt;td&gt;EdDSA 签名, PublicKey → Address&lt;/td&gt;
&lt;td&gt;任何人都能验签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;去重&lt;/td&gt;
&lt;td&gt;QuarterSHA512 hash 作为唯一键&lt;/td&gt;
&lt;td&gt;同内容公告只存一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引用追溯&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;quote&lt;/code&gt; 字段存被引用公告的 hash&lt;/td&gt;
&lt;td&gt;类似 Quote-tweet&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="4. DHSequence 分区系统 — 确定性密钥轮换"&gt;4. DHSequence 分区系统 — 确定性密钥轮换&lt;/h2&gt;&lt;h3 id="4.1 核心问题"&gt;4.1 核心问题&lt;/h3&gt;
&lt;p&gt;ECDH 握手需要双方协商一个 &lt;code&gt;sequence&lt;/code&gt; 编号来派生 AES 密钥。如果双方时间不同步或 sequence 不匹配，就算出不同的密钥，消息无法解密。&lt;strong&gt;如何让两个独立客户端算出相同的 sequence？&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="4.2 DHSequence 函数"&gt;4.2 DHSequence 函数&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MessengerUtil.js — Client 端与 Server 端逻辑一致&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1320981071000&lt;/span&gt;  &lt;span class="c1"&gt;// 2011-11-11 11:11:11 UTC&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DefaultPartition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;  &lt;span class="c1"&gt;// 90 天 (秒)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;DHSequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. 地址对称化: 字典序排序，确保双方结果一致&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tmpStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. 从地址对派生确定性 cursor (0 ~ partition-1 的偏移)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tmpInt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HalfSHA512&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpStr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpInt&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. 计算当前时间落在第几个分区周期&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Epoch&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partition&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4.3 设计精妙之处"&gt;4.3 设计精妙之处&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;确定性:&lt;/strong&gt; 双方输入相同的 &lt;code&gt;(partition, timestamp, addrA, addrB)&lt;/code&gt; → 输出相同的 &lt;code&gt;seq&lt;/code&gt;。地址对排序消除顺序依赖，SHA512 cursor 使每对用户有独特的时间偏移。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;时间分区轮换:&lt;/strong&gt; &lt;code&gt;DefaultPartition = 90天&lt;/code&gt;。每 90 天，&lt;code&gt;seq&lt;/code&gt; 自动递增，派生新的 AES 密钥。旧密钥不再使用，实现 &lt;strong&gt;周期性的前向安全轮换&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cursor 的作用:&lt;/strong&gt; &lt;code&gt;HalfSHA512(addrA + addrB)&lt;/code&gt; 的不同值意味着每对用户有唯一的时间偏移。避免所有用户在同一时刻轮换密钥，分散握手流量。&lt;/p&gt;
&lt;h3 id="4.4 AES 密钥派生链"&gt;4.4 AES 密钥派生链&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AppUtil.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;genAESKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shared_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 地址对称化 (同 DHSequence)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;addrPair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;address2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;address1&lt;/span&gt;

  &lt;span class="c1"&gt;// salt = SHA512(GenesisHash + addrPair + sequence)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SHA512&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GenesisHash&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;addrPair&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// HKDF-DH: 从 ECDH shared secret 派生 AES key&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aesKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hkdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shared_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 256 bits&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;aesKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// hex string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;密钥派生公式: &lt;code&gt;AES-Key = HKDF(ECDH-Shared-Secret, SHA512(Genesis + AddrPair + Seq))&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;双方独立计算，结果相同。Server 不存储、不传输 AES 密钥。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="5. ECDH 握手协议 — 私聊与群聊端到端加密"&gt;5. ECDH 握手协议 — 私聊与群聊端到端加密&lt;/h2&gt;&lt;h3 id="5.1 握手消息格式"&gt;5.1 握手消息格式&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ObjectType.ECDH (101)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// 分区周期 (秒)&lt;/span&gt;
  &lt;span class="nx"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DHSequence&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;          &lt;span class="c1"&gt;// 当前分区序列号&lt;/span&gt;
  &lt;span class="nx"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ecdh_public_key_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// 本次握手的 ECDH 公钥&lt;/span&gt;
  &lt;span class="nx"&gt;Pair&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// 对方公钥 (首轮空，回执时填入)&lt;/span&gt;
  &lt;span class="nx"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destination_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;xrpl_public_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// XRPL 身份公钥&lt;/span&gt;
  &lt;span class="nx"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EdDSA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// 签名覆盖全部内容&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="5.2 私聊握手流程"&gt;5.2 私聊握手流程&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A (发起方)                          User B (接收方)
  │                                          │
  │── ECDH { Self: pubA, Pair: "", Seq } ──▶│
  │                                         │── db.getHandshake(A, B, partition, seq)
  │                                         │   → null (首次)
  │                                         │
  │                                         │── ecdh_private = HalfSHA512(Genesis + seedB + addrB + seq)
  │                                         │── ecdh_pubB = derivePublic(ecdh_private)
  │                                          │── shared = ECDH(pubA, privB)
  │                                          │── aesKey = HKDF(shared, salt)
  │                                          │── db.initHandshakeFromRemote(B, A, ..., aesKey, pubB, pair=pubA)
  │                                          │
  │◄── ECDH { Self: pubB, Pair: pubA } ────│
  │                                          │
  │── db.updateHandshake(A, B, ..., aesKey, Pair=pubB)
  │    (Pair ≠ "" → 握手完成，不再回传)
  │
  │── 此后 AES-CBC(aesKey) 加密所有私聊消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;确定性私钥派生:&lt;/strong&gt; &lt;code&gt;ecdh_private = HalfSHA512(GenesisHash + seed + address + sequence)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;同一 sequence 内，ECDH 私钥可以从 XRPL Seed 重新派生，无需持久化 ECDH 私钥本身。这简化了密钥管理——SQLite 只存 AES key 和握手 JSON。&lt;/p&gt;
&lt;h3 id="5.3 加密/解密路径"&gt;5.3 加密/解密路径&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 发送私聊消息:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AesEncrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content_json&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next_seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;PreHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;last_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="na"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 接收私聊消息:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHandshake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DHSequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AesDecrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Server 的角色:&lt;/strong&gt; 收到 ObjectType.PrivateMessage → AJV Schema 验证 → EdDSA 签名验证 → &lt;code&gt;CachePrivateMessage()&lt;/code&gt; 存密文 → 转发给 &lt;code&gt;json.To&lt;/code&gt;。&lt;strong&gt;Server 只存密文，不解密。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="5.4 群聊 Star Topology"&gt;5.4 群聊 Star Topology&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      User A (creator)
     /    │    │    \
AES-K1  AES-K2 AES-K3 AES-K4
   /        │       │       \
User B    User C  User D   User E
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;每个成员与 creator 之间有一条独立的 ECDH 链路&lt;/li&gt;
&lt;li&gt;消息发送: sender 用 &lt;code&gt;sender↔creator&lt;/code&gt; 的 AES key 加密 → Server 转发给所有成员 → 各成员用自己与 creator 的 AES key 解密&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;O(n) key 管理&lt;/strong&gt; (不是 O(n²))，因为所有加密都通过 creator 作为中心&lt;/li&gt;
&lt;li&gt;max 16 members: ECDH 握手数量和消息分发成本随人数线性增长，但 SQLite 查询复杂度上升&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5.5 群聊消息同步"&gt;5.5 群聊消息同步&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GroupMessageSync — 向特定成员同步缺失消息&lt;/span&gt;
&lt;span class="nx"&gt;ActionCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GroupMessageSync&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="nx"&gt;转发给目标成员&lt;/span&gt;
  &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="nx"&gt;收到&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;DHSequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;that_member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;找&lt;/span&gt; &lt;span class="nx"&gt;AES&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUnsyncGroupSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groupHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sinceTimestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;逐条&lt;/span&gt; &lt;span class="nx"&gt;AesEncrypt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;打包&lt;/span&gt; &lt;span class="nx"&gt;GroupMessageList&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;发送&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="6. 消息链验证 — 私聊完整性保证"&gt;6. 消息链验证 — 私聊完整性保证&lt;/h2&gt;&lt;h3 id="6.1 私聊消息也是 Hash-Linked Chain"&gt;6.1 私聊消息也是 Hash-Linked Chain&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MessengerSaga.js — 接收私聊消息的验证逻辑&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getLastPrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last_msg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 第一条消息: Sequence=1, PreHash=GenesisHash&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PreHash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;GenesisHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addPrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;         &lt;span class="c1"&gt;// ✓ 保存&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;SyncPrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;           &lt;span class="c1"&gt;// ⚠ 请求同步缺失消息&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 链式验证: sequence 连续 + PreHash 匹配&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PreHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addPrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;         &lt;span class="c1"&gt;// ✓ 保存&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;SyncPrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;           &lt;span class="c1"&gt;// ⚠ 有gap, 请求同步&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// else: sequence ≤ last → 旧消息，丢弃&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;私聊公告链与公告链采用&lt;strong&gt;相同的完整性验证模式&lt;/strong&gt;: &lt;code&gt;PreHash + Sequence&lt;/code&gt; 双条件检查。任何消息注入、重放、乱序都会被检测。&lt;/p&gt;
&lt;h3 id="6.2 自修复同步"&gt;6.2 自修复同步&lt;/h3&gt;
&lt;p&gt;当检测到链断裂时，Client 发送 &lt;code&gt;PrivateMessageSync&lt;/code&gt; 请求:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ Action: 501, To: partner, PairSequence: 我方已知的对方最大 sequence,
   SelfSequence: 我希望对方知道的我方最大 sequence }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server 查询数据库，返回缺失的消息窗口。这是一个 &lt;strong&gt;双向增量同步协议&lt;/strong&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="7. 文件传输协议 — Nonce-based 分块加密传输"&gt;7. 文件传输协议 — Nonce-based 分块加密传输&lt;/h2&gt;&lt;h3 id="7.1 参数"&gt;7.1 参数&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大文件大小&lt;/td&gt;
&lt;td&gt;64 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分块大小&lt;/td&gt;
&lt;td&gt;1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大分块数&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonce 范围&lt;/td&gt;
&lt;td&gt;0 ~ 2³²-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传输通道&lt;/td&gt;
&lt;td&gt;WebSocket binary frame&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="7.2 帧格式"&gt;7.2 帧格式&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Binary WS Frame:
┌─────────────┬──────────────────┐
│ 4-byte nonce│ chunk data       │
│ (big-endian)│ (encrypted if E2E│
│ Uint32 BE   │  private/group)  │
└─────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="7.3 协议分离设计"&gt;7.3 协议分离设计&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Text WS Frame  → JSON 控制消息 (公告、握手、请求)
Binary WS Frame → 文件分块数据 (头像、公告附件、聊天文件)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;互不阻塞:&lt;/strong&gt; 控制信令走文本帧，文件传输走二进制帧。即使大文件正在传输，握手和公告仍能即时送达。&lt;/p&gt;
&lt;h3 id="7.4 私聊/群聊文件加密"&gt;7.4 私聊/群聊文件加密&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client 发送私聊文件:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHandshake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DHSequence&lt;/span&gt;&lt;span class="p"&gt;(...))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted_chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AesEncryptBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ecdh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted_chunk&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

&lt;span class="c1"&gt;// Server 转发 (不解密):&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PrivateChatFile&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;GroupChatFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;From&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 原封不动转发 binary frame&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="7.5 Nonce 匹配接收"&gt;7.5 Nonce 匹配接收&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FileRequestList 维护 pending 请求&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isBinary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isBinary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BufferToUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;FileRequestList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Nonce&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 匹配 → 写入磁盘 / 转发&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nonce 是请求方生成的随机数，响应方原样返回。120 秒 TTL 自动清理过期请求。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="8. 协议路由矩阵 — ActionCode/ObjectType 双轴分发"&gt;8. 协议路由矩阵 — ActionCode/ObjectType 双轴分发&lt;/h2&gt;&lt;h3 id="8.1 双轴设计"&gt;8.1 双轴设计&lt;/h3&gt;
&lt;p&gt;每条 JSON 消息携带 &lt;strong&gt;Action&lt;/strong&gt; (动词) 或 &lt;strong&gt;ObjectType&lt;/strong&gt; (名词)，互斥:&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Action — 客户端发起的请求&lt;/span&gt;
&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="nx"&gt;Declare&lt;/span&gt;              &lt;span class="c1"&gt;// 身份宣告 / 节点发现&lt;/span&gt;
&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="nx"&gt;AvatarRequest&lt;/span&gt;        &lt;span class="c1"&gt;// 头像拉取&lt;/span&gt;
&lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="nx"&gt;FileRequest&lt;/span&gt;          &lt;span class="c1"&gt;// 文件分块请求&lt;/span&gt;
&lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="nx"&gt;BulletinRequest&lt;/span&gt;      &lt;span class="c1"&gt;// 公告获取&lt;/span&gt;
&lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="nx"&gt;BulletinSubscribe&lt;/span&gt;    &lt;span class="c1"&gt;// 公告订阅推送&lt;/span&gt;
&lt;span class="mi"&gt;402&lt;/span&gt; &lt;span class="nx"&gt;RandomBulletinRequest&lt;/span&gt;&lt;span class="c1"&gt;// 随机公告&lt;/span&gt;
&lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="nx"&gt;ServerAddressRequest&lt;/span&gt; &lt;span class="c1"&gt;// 活跃地址列表&lt;/span&gt;
&lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="nx"&gt;ReplyBulletinRequest&lt;/span&gt; &lt;span class="c1"&gt;// 回复列表&lt;/span&gt;
&lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="nx"&gt;TagBulletinRequest&lt;/span&gt;   &lt;span class="c1"&gt;// 标签搜索&lt;/span&gt;
&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="nx"&gt;FriendRequest&lt;/span&gt;        &lt;span class="c1"&gt;// 好友添加&lt;/span&gt;
&lt;span class="mi"&gt;501&lt;/span&gt; &lt;span class="nx"&gt;PrivateMessageSync&lt;/span&gt;   &lt;span class="c1"&gt;// 私聊消息同步&lt;/span&gt;
&lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="nx"&gt;GroupSync&lt;/span&gt;            &lt;span class="c1"&gt;// 群列表同步&lt;/span&gt;
&lt;span class="mi"&gt;601&lt;/span&gt; &lt;span class="nx"&gt;GroupMessageSync&lt;/span&gt;     &lt;span class="c1"&gt;// 群消息同步&lt;/span&gt;

&lt;span class="c1"&gt;// ObjectType — 数据对象 / 响应&lt;/span&gt;
&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="nx"&gt;Nothing&lt;/span&gt;              &lt;span class="c1"&gt;// 空响应&lt;/span&gt;
&lt;span class="mi"&gt;101&lt;/span&gt; &lt;span class="nx"&gt;ECDH&lt;/span&gt;                 &lt;span class="c1"&gt;// 密钥交换握手&lt;/span&gt;
&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="nx"&gt;Avatar&lt;/span&gt;               &lt;span class="c1"&gt;// 头像数据&lt;/span&gt;
&lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="nx"&gt;AvatarList&lt;/span&gt;           &lt;span class="c1"&gt;// 头像列表&lt;/span&gt;
&lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="nx"&gt;Bulletin&lt;/span&gt;             &lt;span class="c1"&gt;// 公告内容&lt;/span&gt;
&lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="nx"&gt;ServerAddressList&lt;/span&gt;    &lt;span class="c1"&gt;// 地址列表&lt;/span&gt;
&lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="nx"&gt;ReplyBulletinList&lt;/span&gt;    &lt;span class="c1"&gt;// 回复列表&lt;/span&gt;
&lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="nx"&gt;TagBulletinList&lt;/span&gt;      &lt;span class="c1"&gt;// 标签结果&lt;/span&gt;
&lt;span class="mi"&gt;406&lt;/span&gt; &lt;span class="nx"&gt;RandomBulletinList&lt;/span&gt;   &lt;span class="c1"&gt;// 随机公告&lt;/span&gt;
&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="nx"&gt;PrivateMessage&lt;/span&gt;       &lt;span class="c1"&gt;// 私聊消息&lt;/span&gt;
&lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="nx"&gt;GroupCreate&lt;/span&gt;          &lt;span class="c1"&gt;// 建群&lt;/span&gt;
&lt;span class="mi"&gt;601&lt;/span&gt; &lt;span class="nx"&gt;GroupDelete&lt;/span&gt;          &lt;span class="c1"&gt;// 删群&lt;/span&gt;
&lt;span class="mi"&gt;602&lt;/span&gt; &lt;span class="nx"&gt;GroupList&lt;/span&gt;            &lt;span class="c1"&gt;// 群列表&lt;/span&gt;
&lt;span class="mi"&gt;603&lt;/span&gt; &lt;span class="nx"&gt;GroupMessage&lt;/span&gt;         &lt;span class="c1"&gt;// 群消息&lt;/span&gt;
&lt;span class="mi"&gt;604&lt;/span&gt; &lt;span class="nx"&gt;GroupMessageList&lt;/span&gt;     &lt;span class="c1"&gt;// 群消息列表&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="8.2 Server 端路由逻辑"&gt;8.2 Server 端路由逻辑&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// checkMessage() → MsgValidate() 解析 JSON&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;handleObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFromNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 数据对象路由&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;handleAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// 请求路由&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// handleObject — 核心转发逻辑&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFromNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;To&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;SendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 有目标地址 → 转发&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ObjectType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;Bulletin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;CacheBulletin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 持久化 + 拉更多&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PrivateMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CachePrivateMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// 存密文 + 转发&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;ECDH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;CacheECDH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// 配对握手记录&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;GroupCreate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// 注册群成员映射&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计要点:&lt;/strong&gt; Server 对 &lt;code&gt;ObjectType.Bulletin&lt;/code&gt; 做 &lt;code&gt;VerifyJsonSignature()&lt;/code&gt; 后才持久化，但对 &lt;code&gt;ObjectType.PrivateMessage&lt;/code&gt; 只做签名验证后存密文转发——不解密、不解析内容。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="9. 数据层设计 — Client SQLite + Server PostgreSQL 双存储"&gt;9. 数据层设计 — Client SQLite + Server PostgreSQL 双存储&lt;/h2&gt;&lt;h3 id="9.1 架构总览"&gt;9.1 架构总览&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────┐         ┌─────────────────────────┐
│   RippleMessengerClient │         │   RippleMessengerServer │
│                         │         │                         │
│  Tauri SQLite (app.db)  │ ◄WS►   │  PostgreSQL (Prisma)    │
│                         │         │                         │
│  18 Tables              │         │  8 Models               │
│  Local-First 缓存       │         │  Store-and-Forward      │
└─────────────────────────┘         └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计哲学:&lt;/strong&gt; Client SQLite 是 UI 的真实数据源 (Local-First)，Server PostgreSQL 是消息中继与存证。两者不是主从关系，而是 &lt;strong&gt;各自维护完整的业务副本&lt;/strong&gt;。Client 断开网络后仍能读取全部历史。&lt;/p&gt;
&lt;h3 id="9.2 Server 端 PostgreSQL — Prisma Schema (8 Models)"&gt;9.2 Server 端 PostgreSQL — Prisma Schema (8 Models)&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ─── Avatar ───
model Avatar {
  address   String  @id          // XRPL 地址 = PK
  hash      String                 // 头像文件 SHA512-32
  size      Int
  json      String                 // 完整签名 JSON (stringified)
  signed_at BigInt
  is_saved  Boolean @default(false) // 图片二进制是否已下载到磁盘
}

// ─── Bulletin ───
model Bulletin {
  hash       String  @id          // QuarterSHA512(bulletin_json)
  address    String                 // 发布者 XRPL 地址
  sequence   Int                    // 链上位置
  content    String                 // 明文内容 (Server 可索引搜索)
  json       String                 // 完整签名 JSON 原封不动存储
  signed_at  BigInt                // 消息自带时间戳
  created_at BigInt                // Server 入库时间
  pre_hash   String                 // 前驱公告 hash
  next_hash  String?               // 后继公告 hash (回填)

  tags       Tag[]                  // N:M 标签关联
  files      File[]                // 1:N 附件关联
}

// ─── Tag + Bulletin 多对多 ───
model Tag {
  name      String  @id
  bulletins Bulletin[]
}

// ─── File (公告附件) ───
model File {
  hash         String  @id          // 文件 SHA512-32
  size         Int                    // 总字节数
  chunk_length Int                    // 分块总数 (ceil(size/1MB))
  chunk_cursor Int                    // 已接收块数
  updated_at   BigInt
  is_saved     Boolean @default(false) // 全部块下载完成且校验通过
  bulletins    Bulletin[]            // 反向关联
}

// ─── Reply (引用回复关系) ───
model Reply {
  post_hash   String                 // 被引用公告 hash
  reply_hash  String                // 引用方公告 hash
  signed_at   BigInt
  @@id([post_hash, reply_hash])     // 组合 PK 防重复引用
}

// ─── ECDH (握手记录) ───
model ECDH {
  address1  String                 // 字典序较大地址
  address2  String                 // 字典序较小地址
  partition Int                    // 时间分区
  sequence  Int                    // 序列号
  json1     String?                // address1 方向的握手 JSON
  json2     String?                // address2 方向的握手 JSON
  @@id([address1, address2, partition, sequence])
}

// ─── PrivateMessage (私聊密文) ───
model PrivateMessage {
  hash         String  @id          // QuarterSHA512(message_json)
  sour_address String                 // 发送者地址
  dest_address String                 // 接收者地址
  sequence     Int                    // 消息链位置
  signed_at    BigInt
  json         String                 // 完整密文 JSON (不解密)
}

// ─── Group ───
model Group {
  hash        String  @id
  created_by  String                 // 创建者地址
  created_at  BigInt
  create_json String                 // 建群 JSON 原文
  member      String                 // JSON array (成员地址列表)
  deleted_at  BigInt?               // 软删除时间
  delete_json String?               // 删群 JSON 原文
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="9.3 Client 端 SQLite — 18 Tables"&gt;9.3 Client 端 SQLite — 18 Tables&lt;/h3&gt;&lt;h4 id="身份与社交"&gt;身份与社交&lt;/h4&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;servers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;url&lt;/td&gt;
&lt;td&gt;WebSocket 服务器配置，优先级排序 (default 64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contacts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;address&lt;/td&gt;
&lt;td&gt;通讯录：昵称→XRPL 地址映射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;address&lt;/td&gt;
&lt;td&gt;多账号存储，含 salt + cipher_data (AES 加密的 Seed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;follows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(local, remote)&lt;/td&gt;
&lt;td&gt;单向关注关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;friends&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(local, remote)&lt;/td&gt;
&lt;td&gt;双向好友关系 (私聊前提条件)&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h4 id="头像与文件"&gt;头像与文件&lt;/h4&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;avatar_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;address&lt;/td&gt;
&lt;td&gt;头像元数据 (hash, size, is_saved)。实际图片在 &lt;code&gt;&amp;lt;resourceDir&amp;gt;/avatar/&lt;/code&gt; 磁盘目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;公告附件下载进度追踪 (chunk_length, chunk_cursor, is_saved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private_chat_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ehash&lt;/td&gt;
&lt;td&gt;私聊文件映射表：加密 hash → 真实 hash + size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group_chat_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ehash&lt;/td&gt;
&lt;td&gt;群聊文件映射表：加密 hash → group_hash + 真实 hash + size&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h4 id="公告与标签"&gt;公告与标签&lt;/h4&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulletins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;核心表&lt;/strong&gt; — 公告链副本，含 pre_hash/next_hash 双向链接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulletin_replys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(bulletin_hash, reply_hash)&lt;/td&gt;
&lt;td&gt;回复关系映射，CASCADE DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulletin_files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(bulletin_hash, file_hash)&lt;/td&gt;
&lt;td&gt;公告→附件关联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id (自增)&lt;/td&gt;
&lt;td&gt;标签名注册表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bulletin_tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(bulletin_hash, tag_id)&lt;/td&gt;
&lt;td&gt;公告→标签 N:M 关联&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h4 id="加密握手与消息"&gt;加密握手与消息&lt;/h4&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handshakes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(self_address, pair_address, partition, sequence)&lt;/td&gt;
&lt;td&gt;ECDH 握手状态：aes_key, private_key, public_key, self_json, pair_json&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private_messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;私聊消息：&lt;strong&gt;解密后&lt;/strong&gt;的明文存 content 列，原文 JSON 存 json 列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group_messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;群聊消息：同私聊结构&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h4 id="群聊定义"&gt;群聊定义&lt;/h4&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;PK&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;群元数据：name, member (JSON array), is_accepted, deleted_at (软删除)&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="9.4 Client vs Server 存储差异 — 设计取舍"&gt;9.4 Client vs Server 存储差异 — 设计取舍&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Client SQLite                    Server PostgreSQL
                  ────────────────                 ───────────────────
PrivateMessage   存解密后的明文 content           只存密文 json 原文
GroupMessage     存解密后的明文 content           不持久化，纯转发
ECDH             存完整握手记录 + AES key        只存双方 JSON 对 (json1/json2)
Bulletin         存明文 content                  存明文 content (可搜索)
Group            有 is_accepted 本地状态          有 GroupMap 内存映射
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键取舍 — 私聊消息的双重存储策略:&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;层面&lt;/th&gt;
&lt;th&gt;存什么&lt;/th&gt;
&lt;th&gt;为什么&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;密文 json&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Store-and-forward&lt;/strong&gt; — 消息投递后保留副本，支持客户端重连同步。但 Server 无 AES key → 永远不可见明文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;解密后的明文 content + 原文 json&lt;/td&gt;
&lt;td&gt;UI 展示需要读取明文；原文 json 保留用于签名验证和转发&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键取舍 — 群聊消息只存 Client:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Server 对群聊不做任何持久化，只做内存转发 (&lt;code&gt;GroupMap[hash] = members&lt;/code&gt;)。理由：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;群聊有 E2E 加密，Server 存的密文对其他成员无用&lt;/li&gt;
&lt;li&gt;每个成员用自己的 AES key 解密 → 数据天然属于 Client&lt;/li&gt;
&lt;li&gt;减少 Server 存储压力 (N 成员的群有 N 份不同密文)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="9.5 Handshake 存储 — Client 端是加密基础设施的核心"&gt;9.5 Handshake 存储 — Client 端是加密基础设施的核心&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// handshakes 表核心字段:&lt;/span&gt;
&lt;span class="nx"&gt;self_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pair_address&lt;/span&gt;    &lt;span class="c1"&gt;// 通信双方&lt;/span&gt;
&lt;span class="nx"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;           &lt;span class="c1"&gt;// 时间分区定位&lt;/span&gt;
&lt;span class="nx"&gt;aes_key&lt;/span&gt;                       &lt;span class="c1"&gt;// HKDF-DH 派生的 AES key (null = 握手未完成)&lt;/span&gt;
&lt;span class="nx"&gt;private_key&lt;/span&gt;                   &lt;span class="c1"&gt;// ECDH secp256k1 私钥 hex (确定性派生)&lt;/span&gt;
&lt;span class="nx"&gt;public_key&lt;/span&gt;                    &lt;span class="c1"&gt;// 对应公钥 hex&lt;/span&gt;
&lt;span class="nx"&gt;self_json&lt;/span&gt;                     &lt;span class="c1"&gt;// 我方发出的 ECDH 消息 JSON&lt;/span&gt;
&lt;span class="nx"&gt;pair_json&lt;/span&gt;                     &lt;span class="c1"&gt;// 对方回复的 ECDH 消息 JSON (null = 未收到)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client 每次发/收私聊或群消息时，通过 &lt;code&gt;DHSequence(timestamp, self, partner)&lt;/code&gt; 定位正确的 handshake 行 → 取出 &lt;code&gt;aes_key&lt;/code&gt; → AES-CBC 加解密。这是 Local-First 架构的关键：离线时仍能从 SQLite 完整还原所有加密密钥。&lt;/p&gt;
&lt;h3 id="9.6 File 下载进度追踪"&gt;9.6 File 下载进度追踪&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    chunk_length          chunk_cursor            is_saved
File ───────────────┬─────────────        ─────────────           ─────────
公告附件 (明文)       Server + Client      每收1MB加1               全部块到且 hash 校验通过 → true
头像 (明文)          Server + Client      N/A (单文件一次性)        文件写入磁盘成功 → true
私聊文件 (E2E加密)   Client 仅 (private_chat_files)     ehash 映射, 不走 chunk 追踪
群聊文件 (E2E加密)   Client 仅 (group_chat_files)       ehash 映射, 走 Server 转发
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="10. 消息交互与同步协议 — 从登录到离线恢复的完整生命周期"&gt;10. 消息交互与同步协议 — 从登录到离线恢复的完整生命周期&lt;/h2&gt;&lt;h3 id="10.1 Client 登录 → 全量数据恢复流程"&gt;10.1 Client 登录 → 全量数据恢复流程&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户输入密码解密 Seed
  │
  ├─ dbAPI.updateAccountUpdatedAt()      // SQLite: 标记活跃时间
  │
  ├─ LoadContactList                     // SQLite: contacts + follows + friends
  │
  ├─ LoadMineBulletinSequence            // SQLite: getAddressBulletinCount()
  │
  ├─ LoadGroupList                       // SQLite: groups WHERE is_accepted=1
  │
  ├─ LoadSessionList                     // 每个好友/群查 unread count + last timestamp
  │
  └─ LoadServerList                      // SQLite: servers WHERE is_connect=1, ORDER BY priority DESC
       │
       └─ WebsocketUtil.connect(url)     // 按优先级连接 WebSocket
            │
            ├─ Declare { PublicKey }    // 身份宣告
            │   Server 回应: Declare { URL } + SyncClientRequest()
            │
            ├─ AvatarRequest({flag})     // 拉取过期头像
            │
            └─ SubscribeFollow           // BulletinSubscribe → SubscribeMap[address].push(client)
                 │
                 └─ FetchFollowBulletin   // 遍历 follow list, 查本地 DB 最大 sequence
                      │                   → genBulletinRequest(address, localSeq+1, ...)
                      │                   → 请求缺失公告
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SyncClientRequest — Server 对新连接的一次性数据推送:&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;Server 动作&lt;/th&gt;
&lt;th&gt;Client 收到什么&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;① Bulletin 缺口检测&lt;/td&gt;
&lt;td&gt;遍历我方已存的该地址所有公告 sequence → 找第一个断裂点&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BulletinRequest(address, missing_seq+1)&lt;/code&gt; — 告诉 Client "你的链断了，从这条开始发给我"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② 文件断点续传&lt;/td&gt;
&lt;td&gt;查询 &lt;code&gt;File WHERE is_saved=false&lt;/code&gt; → 对每个未完成文件发分块请求&lt;/td&gt;
&lt;td&gt;二进制 chunk 逐步补齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ ECDH 握手配对&lt;/td&gt;
&lt;td&gt;查 `ECDH WHERE (address1\&lt;/td&gt;
&lt;td&gt;=addr OR address2\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;④ 群列表同步&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GenGroupSync()&lt;/code&gt; (请求 Client 上报自身群) + &lt;code&gt;HandelGroupSync(addr)&lt;/code&gt; (推送 Server 已知的群)&lt;/td&gt;
&lt;td&gt;GroupList 响应 — 建群/删群 JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="10.2 Bulletin 缓存完整数据流"&gt;10.2 Bulletin 缓存完整数据流&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebSocket 收到 ObjectType.Bulletin
  │
  ├─ Phase 1: checkBulletinSchema(json) &amp;amp;&amp;amp; VerifyJsonSignature(json)
  │   → 失败 → 丢弃
  │
  ├─ CacheBulletin Saga (Client 端):
  │   dbAPI.getLastBulletin(address)
  │     → null?
  │       ├─ Yes: json.Sequence===1 &amp;amp;&amp;amp; json.PreHash===GenesisHash → addBulletin()
  │       └─ No: last.sequence + 1 === json.Sequence → addBulletin()
  │               last.sequence ≠ json.Sequence → RequestNextBulletin (请求中间缺失的)
  │
  │   addBulletin():
  │     INSERT OR IGNORE INTO bulletins (hash, address, sequence, content, json, signed_at, pre_hash)
  │
  │   关联操作:
  │     ↳ tags[]?       → INSERT OR IGNORE INTO tags → addTagsToBulletin()
  │     ↳ quote[]?      → addReplyToBulletins(bulletin_hash, reply_hash pairs)
  │     ↳ file[]?       → addFile({hash, size, chunk_length, chunk_cursor=0, is_saved=false})
  │                       ↳ FetchBulletinFile — 通过 WebSocket 二进制帧下载
  │
  └─ UI 刷新:
      ↳ RefreshPortalBulletin()          // 首页公告流更新
      ↳ RefreshFollowBulletin()          // 关注页公告流更新
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="10.3 Private Message 发送完整数据流"&gt;10.3 Private Message 发送完整数据流&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户输入文本 → SendContent Saga
  │
  ├─ dbAPI.getLastConfirmPrivateMessage(self, remote)
  │   → 有未确认消息? → confirmPrivateMessage(hash, true) + 发送确认回执
  │
  ├─ 计算下一个 sequence:
  │   last_confirm ? last_confirm.sequence + 1 : (last_unconfirm ? last_unconfirm.sequence + 1 : 1)
  │
  ├─ genPrivateMessage(seed, sequence, preHash, null, content, remote)
  │
  ├─ dbAPI.addPrivateMessage(hash, sour, dest, seq, preHash, content, json, ...)
  │   → 本地先存 (Offline-First: UI 立即显示消息)
  │
  └─ WebSocket.send(encrypted_json)
       │
       └─ Server 收到 ObjectType.PrivateMessage:
           ├─ VerifyJsonSignature(json)
           ├─ CachePrivateMessage(json):
           │   last_msg = SELECT sequence, hash FROM PrivateMessage ORDER BY sequence DESC LIMIT 1
           │   → chain check: sequence === last.sequence + 1 &amp;amp;&amp;amp; preHash === last.hash
           │     ✓ INSERT (hash, sour, dest, sequence, json)
           │     ✗ GenPrivateMessageSync() → 告诉 Client "你断了，补发"
           │
           └─ SendMessage(json.To, message)     // 转发给接收方
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计要点 — Offline-First 写入:&lt;/strong&gt; Client 在发送 WebSocket 之前就把消息存入本地 SQLite。即使网络断开，消息已经出现在 UI 中。这是 Local-First 架构的核心行为。&lt;/p&gt;
&lt;h3 id="10.4 私聊消息双向同步协议 — PrivateMessageSync"&gt;10.4 私聊消息双向同步协议 — PrivateMessageSync&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client A                          Server                        Client B
  │                                 │                              │
  │── PrivateMessageSync ──────────▶│                                │
  │   { To: B,                       │                               │
  │     PairSequence: 10,            │  查询 OR:                      │
  │     SelfSequence: 8 }            │    sour=A,dest=B,seq&amp;gt;8         │
  │                                 │    sour=B,dest=A,seq&amp;gt;10        │
  │◄── PrivateMessage ──────────────│                                │
  │   (A→B seq=9, delay 1s)         │      ORDER BY sequence ASC     │
  │                                 │       逐条推送,间隔 1s          │
  │◄── PrivateMessage ──────────────│                                │
  │   (A→B seq=10, delay 1s)        │                                │
  │                                 │                                │
  │◄── PrivateMessage ──────────────│                                │
  │   (B→A seq=11, delay 1s)        │  Server 每秒延迟推送            │
  │                                 │  → 避免洪水攻击                 │
  │◄── PrivateMessage ──────────────│                                │
  │   (B→A seq=12, delay 1s)        │                                │
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PrivateMessageSync 请求内容:&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;To&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通信对方地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PairSequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"我知道对方最多发到第几条" (sour=partner, dest=self 的最大 sequence)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SelfSequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"我发出去的消息最大序列号是多少" (sour=self, dest=partner 的最大 sequence)&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Server 返回两个方向的缺失消息窗口，按 sequence ASC 排序，1 秒/条限速。&lt;/p&gt;
&lt;h3 id="10.5 Server CachePrivateMessage — 链验证细节"&gt;10.5 Server CachePrivateMessage — 链验证细节&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server side: main.js CachePrivateMessage()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;CachePrivateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QuarterSHA512Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deriveAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;To&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrivateMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findFirst&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sour_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dest_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Chain check — 三种合法情况:&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;last_msg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ① 首条消息: seq=1, preHash=GenesisHash&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PreHash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;GenesisHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrivateMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;GenPrivateMessageSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// ✗ 链起点不匹配 → 触发同步&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sequence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
             &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PreHash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ② 正常续链: seq 连续 + preHash 匹配&lt;/span&gt;
    &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrivateMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ③ 断裂: 请求重同步&lt;/span&gt;
    &lt;span class="nx"&gt;GenPrivateMessageSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;last_msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Server 和 Client 执行相同的链验证逻辑。&lt;/strong&gt; Server 端验证通过后才持久化，否则拒绝存储并发出同步请求。这是 &lt;strong&gt;两端对称的完整性检查&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="10.6 ECDH 握手 — Server 端 Rendezvous 模式"&gt;10.6 ECDH 握手 — Server 端 Rendezvous 模式&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ECDH 表: json1 (address1 方向) + json2 (address2 方向)

User A 先发:                              User B 后发:
  ────────────                             ────────────
Server 收到 ECDH from A                    Server 收到 ECDH from B
  → address1 = max(A,B), json1=A          → 查找同一 PK (address1,address2,partition,sequence)
  → INSERT json1=A, json2=empty           → UPDATE json2=B

CacheECDH() 关键逻辑:
  - address1 &amp;gt; address2 规则 → 双方映射到同一条记录
  - timestamp 比较: 新消息时间戳 &amp;gt;= 旧消息 → 跳过 (防重放)
  - SyncClientRequest 时检测: 如果 counterpart JSON 为空, 推送配对消息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server 端的 ECDH 表是一个 &lt;strong&gt;双向 Rendezvous Buffer&lt;/strong&gt;: 每一方写入自己方向的 JSON，当另一方到达时取出。AES key 永远不在 Server 上计算或存储。&lt;/p&gt;
&lt;h3 id="10.7 Client 断线重连行为"&gt;10.7 Client 断线重连行为&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebSocket.onclose → WebsocketListener Saga:
  │
  ├─ clearInterval(keepAliveTimer)        // 清除心跳
  │
  ├─ Retry connect (exponential backoff):
  │   servers = getServerListByPriority()
  │   for (server in servers):
  │     ws.connect(url) → Declare → SyncClientRequest
  │
  └─ 重连后自动触发:
      ↳ AvatarRequest — 补拉过期头像
      ↳ SubscribeFollow — 重新注册推送订阅
      ↳ FetchFollowBulletin — 补齐离线期间的公告
      ↳ PrivateMessageSync — 断链时自动修复
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;无状态连接:&lt;/strong&gt; WebSocket 是纯传输层。每次重连都走完整的 Declare → SyncClientRequest 流程。Server 不保留任何会话状态 (Conns[address] 只是内存映射，WS 断开即丢失)。所有恢复数据来自 PostgreSQL 持久层。&lt;/p&gt;
&lt;h3 id="10.8 Client 多服务器连接策略"&gt;10.8 Client 多服务器连接策略&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;servers 表:
┌──────────────┬──────────┬──────────┐
│ url          │ priority │ is_connect│
├──────────────┼──────────┼──────────┤
│ wss://jp...  │   128    │     1     ◄── 最高优先级, 优先连接
│ wss://uk...  │    64    │     1     ◄── 备份节点
│ wss://us...  │    32    │     0     ◄── 禁用中
└──────────────┴──────────┴──────────┘

priority 更新规则: updateServerPriority() — 成功连接则加分，失败则减分
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client 可同时维持多个 WS 连接 (通过 &lt;code&gt;WebsocketUtil.js&lt;/code&gt; 管理多实例)，每个连接有独立的 keepalive timer。公告推送到任何一个连接都会触发 CacheBulletin → SQLite upsert by hash (幂等，不会重复)。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="11. 联邦网络 — Declare 双重用途与节点同步"&gt;11. 联邦网络 — Declare 双重用途与节点同步&lt;/h2&gt;&lt;h3 id="11.1 Declare 消息的双重身份"&gt;11.1 Declare 消息的双重身份&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Declare { Action: 100, PublicKey, Signature, URL? }
                    │
                    ├─ Client 发送 (无 URL) → 身份认证宣告
                    │   Server: Conns[address] = ws (注册连接)
                    │
                    └─ Server 发送 (带 URL)  → 节点互发现
                        Server: NodeList.push({URL})  (加入联邦)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;一条消息类型，两种语义:&lt;/strong&gt; Client 用 Declare 证明身份；Server 用 Declare 交换自己的 WebSocket URL，实现节点互发现。&lt;code&gt;json.URL&lt;/code&gt; 的有无是区分标准。&lt;/p&gt;
&lt;h3 id="11.2 节点互联拓扑"&gt;11.2 节点互联拓扑&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client A ──wss──► Node JP ◄──Declare(URL_UK)──► Node UK
                      │                        │
                      │    SyncNodeData()       │
                      │   (pull + push + file)  │
                      └─────5min delta sync────┘
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="11.3 SyncNodeData — 三管齐下"&gt;11.3 SyncNodeData — 三管齐下&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;SyncNodeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pullBulletin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// 拉: 从对方获取我方缺失的公告&lt;/span&gt;
  &lt;span class="nx"&gt;pushBulletin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// 推: 向对方推送我方有的公告&lt;/span&gt;
  &lt;span class="nx"&gt;downloadBulletinFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="c1"&gt;// 文件: 同步未完成的附件下载&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;增量同步:&lt;/strong&gt; &lt;code&gt;pullBulletin&lt;/code&gt; 先请求对方的地址列表，再按每个地址的 &lt;code&gt;sequence + 1&lt;/code&gt; 拉取增量。不是全量 dump，是精准的 delta sync。&lt;/p&gt;
&lt;h3 id="11.4 同步周期"&gt;11.4 同步周期&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;事件&lt;/th&gt;
&lt;th&gt;间隔&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;节点首次连接&lt;/td&gt;
&lt;td&gt;immediate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SyncNodeData(url)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定时心跳&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;keepNodeSync()&lt;/code&gt; → 遍历 NodeList&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;断线重连&lt;/td&gt;
&lt;td&gt;5 s&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;keepNodeConn()&lt;/code&gt; → 随机选未连节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="11.5 SubscribeMap — 服务端推送"&gt;11.5 SubscribeMap — 服务端推送&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BulletinSubscribe (Action: 401)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;HandelBulletinSubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SubscribeMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;subscriber1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subscriber2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// CacheBulletin() 新公告时:&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SubscribeMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;SubscribeMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;SubscribeMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;SendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bulletin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client 声明要订阅的地址列表，Server 内存维护 &lt;code&gt;SubscribeMap&lt;/code&gt;。当这些地址发布新公告时，主动推送给订阅者。&lt;strong&gt;这是 push 补充 pull 的混合模式。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="12. 安全模型与设计权衡"&gt;12. 安全模型与设计权衡&lt;/h2&gt;&lt;h3 id="12.1 三层安全防线"&gt;12.1 三层安全防线&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: 身份
  XRPL KeyPairs — Seed(私钥) + Address(公钥派生)
  私钥不出 Client, Server 不托管任何密码材料

Layer 2: 完整性
  EdDSA Signature 每条消息必签
  AJV JSON Schema 结构校验
  PreHash + Sequence 链式验证 (公告 + 私聊)
  → 来源可验证 + 内容不可篡改

Layer 3: 保密性
  ECDH secp256k1 密钥交换 → HKDF → AES-CBC 加密
  Server 仅存密文并转发
  → 端到端加密, Server 不可见内容
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="12.2 权衡表"&gt;12.2 权衡表&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;设计选择&lt;/th&gt;
&lt;th&gt;优势&lt;/th&gt;
&lt;th&gt;代价/风险&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QuarterSHA512 (40-bit 展示 hash)&lt;/td&gt;
&lt;td&gt;人类可读，易复制搜索&lt;/td&gt;
&lt;td&gt;碰撞概率高 — &lt;strong&gt;但 EdDSA 签名兜底&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-CBC (非 GCM)&lt;/td&gt;
&lt;td&gt;crypto-js 兼容性好&lt;/td&gt;
&lt;td&gt;无 authenticated encryption — 依赖外层 hash chain 防篡改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed 存 localStorage (AES 加密)&lt;/td&gt;
&lt;td&gt;重启恢复&lt;/td&gt;
&lt;td&gt;XSS 可窃取 — Tauri WebView 缩小攻击面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDH on secp256k1&lt;/td&gt;
&lt;td&gt;与 XRPL 统一曲线&lt;/td&gt;
&lt;td&gt;NIST 对 secp256k1 有质疑 (但比特币/以太坊使用)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HKDF-DH 密钥派生&lt;/td&gt;
&lt;td&gt;标准化 KDF，防弱 shared secret&lt;/td&gt;
&lt;td&gt;实现复杂度高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;确定性 ECDH 私钥派生&lt;/td&gt;
&lt;td&gt;无需持久化 ECDH 私钥&lt;/td&gt;
&lt;td&gt;Seed 泄露 → 所有历史密钥可还原 (无前向安全)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 天密钥轮换&lt;/td&gt;
&lt;td&gt;定期自动轮换&lt;/td&gt;
&lt;td&gt;轮换窗口内消息可被新泄露的 Seed 解密&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="12.3 去中心化程度分层评估"&gt;12.3 去中心化程度分层评估&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer              程度        实现方式
─────────────────────────────────────────────────
Identity           ████████░░ 80%  XRPL KeyPairs, 无注册 (但 GenesisHash 硬编码)
Privacy            █████████░ 90%  E2E AES-CBC, 密钥不出 Client
Network Topology   ██████░░░░ 60%  联邦多节点, 用户自配列表, 共享数据库
Content Integrity  █████████░ 90%  Hash-linked chain + EdDSA (但存储集中化)
Data Storage       ███░░░░░░░ 30%  PostgreSQL (同一 DB 实例)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个 &lt;strong&gt;"身份和隐私去中心化、网络联邦化、数据集中化"&lt;/strong&gt; 的混合模型。有意选择这条路径——用 XRPL 获得无摩擦的全球唯一身份，用联邦节点获得可用性冗余，但放弃数据层的去中心化以换取一致性和查询能力。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="13. 设计核心思想总结"&gt;13. 设计核心思想总结&lt;/h2&gt;&lt;h3 id="13.1 十大设计原则"&gt;13.1 十大设计原则&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;原则&lt;/th&gt;
&lt;th&gt;体现&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;密码学即身份&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;XRPL 密钥对 = 全球唯一 ID，免注册、免托管、不可封禁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hash-Linking 即完整性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bulletin Chain + Private Message Chain，去共识化区块链思想，单写者无需 PoW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;确定性密钥派生&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DHSequence 时间分区系统，双方独立计算相同 AES key，无协商开销&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zero Trust 消息验证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AJV Schema + EdDSA Signature 双重门控，不信任传输层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;E2E 加密是底线&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;私聊/群聊端到端 AES-CBC，Server 仅存密文并转发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Declare 双重语义&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client 身份认证 + Server 节点发现，一条消息两种用途&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;混合推拉同步&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SubscribeMap push + sequence-based pull，兼顾实时与一致性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;协议帧分离&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text WS frame 控制信令 / Binary WS frame 文件数据，互不阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Local-First UI 状态&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client SQLite = UI truth，Server 离线仍可读全部历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Usability &amp;gt; Cryptography (有意识取舍)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;QuarterSHA512 牺牲碰撞抗性换取可读性；EdDSA 签名兜底&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h3 id="13.2 一句话总结"&gt;13.2 一句话总结&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;RippleMessenger 的设计哲学是 &lt;strong&gt;"区块链思维 + 即时通讯体验"的融合&lt;/strong&gt;: 用密码学身份消除注册摩擦，用 hash-linked chain 实现发布存证，用确定性 DHSequence 分区系统驱动 ECDH 密钥轮换保护隐私，用 Declare 双重语义构建联邦网络——但整个系统在存储层拥抱集中化 PostgreSQL，以换取查询能力和数据一致性。这是一条&lt;strong&gt;实用主义的去中心化路径&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>RippleMessenger</author>
      <pubDate>Sun, 28 Jun 2026 09:07:48 +0800</pubDate>
      <link>https://w2solo.com/topics/7606</link>
      <guid>https://w2solo.com/topics/7606</guid>
    </item>
    <item>
      <title>开源基于区块链的信息发布和端到端加密聊天项目</title>
      <description>&lt;h2 id="RippleMessengerClient"&gt;RippleMessengerClient&lt;/h2&gt;&lt;h2 id="特点"&gt;特点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;账号本地生成，无需手机认证、生物识别，我就是我无需向任何人证明，使用得当可以做到对所有人匿名，也可在物理世界对其他个人实名使用；&lt;/li&gt;
&lt;li&gt;数据本地存储，包括公告、私聊消息、群聊消息及文件等等全部数据均本地存储，使用得当可保证自己的数据安全，与其吐槽服务提供方丢数据、删数据，不如自己保存好数据；&lt;/li&gt;
&lt;li&gt;数据交换服务可私有部署，源码参见&lt;a href="https://github.com/ripplemessenger/RippleMessengerServer" rel="nofollow" target="_blank" title=""&gt;RippleMessengerServer&lt;/a&gt;，部署简单、成本低廉，无需担心服务不可用，鼓励有实力的玩家部署公开服务或提供计算资源；&lt;/li&gt;
&lt;li&gt;公告功能可以实现推特、微博、博客、论坛帖子等网络产品功能，同时基于 1、2、3 点，公告数据是跟着个人走；&lt;/li&gt;
&lt;li&gt;聊天功能可以实现端到端数据加密（强度为 256 位的 AES 算法），保障聊天内容仅仅在聊天参与方可见，同时基于 1、2、3 点，聊天数据是跟着个人走；&lt;/li&gt;
&lt;li&gt;公告和聊天功能均支持传输文件，当前设定最大可传 64M 文件，其中聊天文件的传输也是采用端到端加密传输；&lt;/li&gt;
&lt;li&gt;客户端软件的 exe 文件只有 5M 小体积，源码开源绿色安全。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="欢迎star、fork、贡献代码、推荐、捐赠，谢谢！"&gt;欢迎 star、fork、贡献代码、推荐、捐赠，谢谢！&lt;/h2&gt;</description>
      <author>RippleMessenger</author>
      <pubDate>Sun, 08 Mar 2026 22:50:25 +0800</pubDate>
      <link>https://w2solo.com/topics/7008</link>
      <guid>https://w2solo.com/topics/7008</guid>
    </item>
  </channel>
</rss>
