第 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 会记录 source、group_id、sender、order 和 sync_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。