Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 28 章 项目四:Sui Stack Messaging 加密消息应用

Sui Stack Messaging 是 Mysten Labs 提供的端到端加密消息工具,面向想把私密通信直接嵌入 dApp、游戏、DAO、AI Agent 或客户支持系统的开发者。它不是独立聊天产品,而是一套可组合基础设施:Sui 负责群组权限和密钥历史,Seal 负责阈值加密,relayer 负责实时投递,Walrus 负责附件和消息归档恢复。

本章把它作为 Walrus 的高级集成案例:消息正文走端到端加密和 relayer,文件附件用 Walrus 存储密文,历史消息可以由 relayer 批量归档到 Walrus quilt,再通过 discovery indexer 恢复。

当前 sui-stack-messaging 处于 Beta,可用于 Testnet 和 Mainnet。生产使用前必须评估 relayer、Seal key server、Walrus 归档、密钥轮换和审计要求。

项目目标

  • @mysten/sui-stack-messaging 创建加密群组。
  • 发送、订阅和拉取端到端加密消息。
  • 用 Walrus 保存加密附件。
  • 理解 relayer 的信任边界和 Walrus 归档恢复路径。
  • 给出生产上线前的安全检查清单。

架构

Browser / App
  |  client-side AES-256-GCM encrypt
  v
@mysten/sui-stack-messaging SDK
  |             |                |
  |             |                +--> Walrus publisher / aggregator
  |             |                     encrypted attachments and archives
  |             |
  |             +--> Seal key servers
  |                  group DEK access control
  |
  +--> Relayer
       encrypted message delivery, ordering, archive jobs
          |
          +--> Sui gRPC
               group permissions, events, key history

关键边界:

  • 明文只应出现在用户设备或可信后端内存中。
  • relayer 存储和转发密文,不应看到明文。
  • Sui 链上保存群组权限、加密历史和元数据,不保存消息正文。
  • Walrus 保存加密附件或归档消息,不应保存明文附件。

依赖版本

写作时核对到的最新版本:

{
  "dependencies": {
    "@mysten/sui-stack-messaging": "0.0.2",
    "@mysten/sui-groups": "0.0.1",
    "@mysten/seal": "1.1.1",
    "@mysten/sui": "2.16.0",
    "@mysten/bcs": "2.0.3"
  }
}

上游要求 Node.js >=22、pnpm >=10.17.0。如果项目已经使用 Sui TS SDK 和 Seal,只需要新增 messaging 和 groups 包:

pnpm add @mysten/sui-stack-messaging@0.0.2 @mysten/sui-groups@0.0.1

完整安装:

pnpm add \
  @mysten/sui-stack-messaging@0.0.2 \
  @mysten/sui-groups@0.0.1 \
  @mysten/seal@1.1.1 \
  @mysten/sui@2.16.0 \
  @mysten/bcs@2.0.3

客户端初始化

createMessagingGroupsClient() 会把 Sui client、groups、Seal 和 messaging 扩展串起来。Testnet 和 Mainnet 的 messaging Move package 会按网络自动识别;localnet 或自定义部署才需要手动传 packageConfig

import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import {
  WalrusHttpStorageAdapter,
  createMessagingGroupsClient,
} from "@mysten/sui-stack-messaging";

const keypair = Ed25519Keypair.fromSecretKey(
  Uint8Array.from(process.env.SUI_SECRET_KEY_BYTES!.split(",").map(Number)),
);

const baseClient = new SuiGrpcClient({
  network: "testnet",
  baseUrl: process.env.SUI_GRPC_URL ?? "https://fullnode.testnet.sui.io:443",
});

export const messagingClient = createMessagingGroupsClient(baseClient, {
  seal: {
    serverConfigs: [
      { objectId: process.env.SEAL_KEY_SERVER_1!, weight: 1 },
      { objectId: process.env.SEAL_KEY_SERVER_2!, weight: 1 },
    ],
  },
  encryption: {
    sessionKey: {
      signer: keypair,
      ttlMin: 10,
      refreshBufferMs: 60_000,
    },
  },
  relayer: {
    relayerUrl: process.env.MESSAGING_RELAYER_URL!,
    pollingIntervalMs: 3_000,
    timeout: 30_000,
  },
  attachments: {
    storageAdapter: new WalrusHttpStorageAdapter({
      publisherUrl:
        process.env.WALRUS_PUBLISHER_URL ??
        "https://publisher.walrus-testnet.walrus.space",
      aggregatorUrl:
        process.env.WALRUS_AGGREGATOR_URL ??
        "https://aggregator.walrus-testnet.walrus.space",
      epochs: Number(process.env.WALRUS_ATTACHMENT_EPOCHS ?? 5),
    }),
    maxAttachments: 10,
    maxFileSizeBytes: 10_485_760,
    maxTotalFileSizeBytes: 52_428_800,
  },
});

浏览器钱包接入时,不要把私钥放到前端。使用钱包适配器签名 session key:

const client = createMessagingGroupsClient(baseClient, {
  seal: { serverConfigs },
  encryption: {
    sessionKey: {
      address: account.address,
      onSign: async (message: Uint8Array) => {
        const result = await signPersonalMessage({ message });
        return result.signature;
      },
    },
  },
  relayer: { relayerUrl },
});

创建群组并发送消息

群组权限保存在 Sui 对象中。SDK 的高层方法会构造并提交交易:

const groupUuid = crypto.randomUUID();

await messagingClient.messaging.createAndShareGroup({
  signer: keypair,
  uuid: groupUuid,
  name: "Walrus Builders",
  initialMembers: [
    "0xALICE_ADDRESS",
    "0xBOB_ADDRESS",
  ],
});

const { messageId } = await messagingClient.messaging.sendMessage({
  signer: keypair,
  groupRef: { uuid: groupUuid },
  text: "第一条端到端加密消息",
});

console.log({ groupUuid, messageId });

读取最近消息:

const { messages, hasNext } = await messagingClient.messaging.getMessages({
  signer: keypair,
  groupRef: { uuid: groupUuid },
  limit: 50,
});

for (const message of messages) {
  if (!message.senderVerified) {
    continue;
  }

  console.log(message.senderAddress, message.text);
}

console.log({ hasNext });

实时订阅使用 AsyncIterable。UI 组件卸载时必须 abort,并断开 transport:

const controller = new AbortController();

for await (const message of messagingClient.messaging.subscribe({
  signer: keypair,
  groupRef: { uuid: groupUuid },
  signal: controller.signal,
})) {
  console.log(`[${message.senderAddress}] ${message.text}`);
}

controller.abort();
messagingClient.messaging.disconnect();

Walrus 附件

附件不是链上对象。SDK 会用群组 DEK 分别加密每个文件,再通过 WalrusHttpStorageAdapter 上传到 Walrus。附件 metadata 也会单独加密,relayer 只保存 opaque wire format。

const reportBytes = new TextEncoder().encode("monthly report");

await messagingClient.messaging.sendMessage({
  signer: keypair,
  groupRef: { uuid: groupUuid },
  text: "报告见附件",
  files: [
    {
      fileName: "report.txt",
      mimeType: "text/plain",
      data: reportBytes,
    },
  ],
});

收到附件后,metadata 会先解密;文件内容按需下载和解密:

for (const attachment of messages[0].attachments) {
  console.log(attachment.fileName, attachment.fileSize);

  const bytes = await attachment.data();
  console.log(new TextDecoder().decode(bytes));
}

工程注意事项:

  • Walrus HTTP publisher 不支持删除附件,删除消息时只能尽力清理 storage backend;Walrus 上的密文会按保存期自然过期。
  • 附件文件名、MIME、大小可能泄露信息;高隐私场景应考虑对展示层 metadata 做额外策略。
  • 大附件应限制总大小,并给用户显示上传、归档、下载和解密状态。

Relayer 运维

relayer 是消息投递和排序的主要运营信任边界。它不能解密消息,也不能伪造 sender signature,但它可以影响消息投递、可用性、排序和 metadata 暴露。

参考 relayer 是 Rust/Axum 服务:

git clone https://github.com/MystenLabs/sui-stack-messaging.git
cd sui-stack-messaging/relayer
cp .env.example .env
cargo run

核心环境变量:

SUI_RPC_URL=https://fullnode.testnet.sui.io:443
GROUPS_PACKAGE_ID=0x...
PORT=3000
WALRUS_PUBLISHER_URL=https://publisher.walrus-testnet.walrus.space
WALRUS_AGGREGATOR_URL=https://aggregator.walrus-testnet.walrus.space
WALRUS_STORAGE_EPOCHS=5
WALRUS_SYNC_INTERVAL_SECS=3600
WALRUS_SYNC_BATCH_SIZE=100
WALRUS_SYNC_MESSAGE_THRESHOLD=50

生产环境不要使用默认内存存储作为唯一数据源。至少需要:

  • 持久化 message storage,例如 PostgreSQL adapter。
  • relayer 水平扩展和 sticky/polling 策略。
  • 请求签名校验、重放保护和 timestamp TTL。
  • Sui checkpoint lag 监控。
  • Walrus sync pending 队列监控。
  • 失败消息重试和死信队列。

Walrus 归档与恢复

relayer 会把待归档消息批量写入 Walrus quilt。每条消息成为一个 patch,例如 msg-{messageId};patch tags 会记录 sourcegroup_idsenderordersync_status

归档触发条件通常是:

  • 定时触发,例如每小时。
  • 消息数量阈值触发,例如 50 条新消息。

恢复路径由 discovery indexer 和 SDK recovery transport 组成:

Walrus BlobCertified event
  v
walrus-discovery-indexer
  v
GET /v1/groups/:groupId/patches
  v
RecoveryTransport
  v
client.messaging.recoverMessages()

客户端恢复:

const recovered = await messagingClient.messaging.recoverMessages({
  groupRef: { uuid: groupUuid },
  limit: 50,
});

for (const message of recovered.messages) {
  if (message.senderVerified) {
    console.log(message.text);
  }
}

恢复消息仍要经过同样的解密和签名校验。因为 Walrus 是公开写入网络,任何人都可以按相同 tag 约定写入 quilt;应用必须检查 senderVerified,并在 indexer 侧根据业务需要限制 publisher 地址。

成员管理与密钥轮换

移除成员时不要只调用普通移除。默认模型下,成员移除不会自动轮换 DEK;被移除成员可能仍能读取旧 key version 下的未来消息,直到管理员轮换密钥。

推荐使用原子操作:

await messagingClient.messaging.removeMembersAndRotateKey({
  signer: keypair,
  groupRef: { uuid: groupUuid },
  members: ["0xFORMER_MEMBER"],
});

长期群组应定期轮换:

await messagingClient.messaging.rotateEncryptionKey({
  signer: keypair,
  groupRef: { uuid: groupUuid },
});

如果项目有高安全要求,还应自定义 Seal policy,例如订阅权限、NFT gate、组织身份或版本限制。

推荐目录结构

sui-messaging-app/
  apps/
    web/
      src/
        messaging/client.ts
        messaging/useMessages.ts
        messaging/useAttachments.ts
  services/
    relayer/
      .env
    discovery-indexer/
      .env
  packages/
    shared/
      messaging-config.ts
  package.json

配置集中管理:

export const messagingConfig = {
  network: "testnet",
  suiGrpcUrl: "https://fullnode.testnet.sui.io:443",
  relayerUrl: "https://messaging-relayer.example.com",
  walrusPublisherUrl: "https://publisher.walrus-testnet.walrus.space",
  walrusAggregatorUrl: "https://aggregator.walrus-testnet.walrus.space",
  walrusAttachmentEpochs: 5,
};

安全边界

上线前必须确认:

  • 客户端只在本地加密和解密消息。
  • relayer request 都验证 wallet signature 和 group permission。
  • UI 默认隐藏 senderVerified=false 的消息,或明确标记为不可信。
  • 移除成员时调用 removeMembersAndRotateKey()
  • 高流量群组有周期性 key rotation。
  • 附件只上传密文,且有大小、数量和 MIME 限制。
  • discovery indexer 有持久化存储和 checkpoint 进度。
  • relayer 和 indexer 日志不记录明文消息、文件名或敏感 metadata。
  • 自托管 relayer 有认证、限流、监控、备份和故障恢复流程。

验收标准

  • 用户能用钱包创建群组,并邀请至少两个成员。
  • 群组成员能发送、读取和订阅消息。
  • 非成员无法通过 relayer 读取消息,也无法通过 Seal 解密 DEK。
  • 移除成员后执行 key rotation,新消息无法被被移除成员解密。
  • 附件上传到 Walrus 后可以按需下载并解密。
  • relayer 重启后,持久化后端能恢复未归档消息。
  • Walrus sync 后,discovery indexer 能查到 group patches。
  • recoverMessages() 能恢复历史消息,并验证 sender signature。
  • 所有生产环境地址、包 ID、Seal key servers 和 Walrus endpoint 都来自显式配置,不依赖本地 CLI active env。