前言
Walrus 是构建在 Sui 之上的去中心化 Blob 存储网络。它适合保存需要可验证、可编程、长期可用的数据,例如前端站点资源、链下大文件、AI 数据集、审计日志、Agent 记忆和链上应用的外部内容。
本书按开发者的真实学习路径组织:先用 CLI 完成上传和下载,再发布 Walrus Sites,然后进入 HTTP API、TypeScript SDK、Move 集成和系统原理。每一章都优先给出可以运行的命令或代码,再解释这些命令背后的对象模型、费用、安全边界和生产注意事项。
版本策略
本书所有安装命令默认使用最新稳定版本:
- Walrus CLI 和节点组件:优先使用
suiup install walrus,或从官方mainnet/testnet分支安装。 - Walrus Sites:优先使用官方文档推荐的最新
site-builder安装方式。 - npm SDK:示例项目固定到写作时通过 npm registry 查询到的最新稳定版本,并在附录 A 中记录查询时间和命令。
- 实验性或预发布版本只在明确标注的章节出现,不作为主线教程默认选择。
读者准备
你需要熟悉基本命令行、JavaScript/TypeScript 和 HTTP。读到 Move 集成章节时,最好已经知道 Sui 对象、交易和 package 的基本概念;如果还没有,也可以先按书中的最小示例跑通,再回头补 Sui Move。
如何阅读本书
前两部分适合顺序阅读。CLI 章节会建立全书最重要的直觉:一次写入会产生 blob_id 和 Sui object ID,读取路径、生命周期管理和费用都围绕这两个标识展开。
第三部分可以作为前端工程师的主线:先发布一个最小站点,再处理路由、headers、SuiNS、portal 和 CI/CD。第四部分进入应用开发,适合后端、全栈和工具开发者。第五到第七部分解释 Move 集成、协议原理和生产运维,建议在已经跑通过上传、下载和站点发布后阅读。
第八部分是综合项目。它不是演示性质的代码片段,而是把本书前面分散出现的能力组合成可扩展的应用骨架。读者可以按项目逐步实现,也可以把其中的模块拆到自己的产品中。
代码约定
书中命令默认使用 Testnet,除非章节明确说明 Mainnet。所有对象 ID、私钥、RPC、域名和 relayer 地址都以占位符展示,复制前必须替换为自己的环境值。示例代码保存在仓库 examples/ 目录,并由 GitHub Actions 做构建和类型检查。
本书项目
全书会完成四个综合项目:
- 去中心化文件网盘:上传、下载、状态查询、分享和生命周期管理。
- Walrus Sites 内容站:静态内容发布、更新、路由、SuiNS 和 CI/CD。
- MemWal AI Agent 记忆库:用加密记忆、语义检索和 Walrus 存储构建可恢复的 Agent 记忆。
- Sui Stack Messaging 加密消息应用:用 Sui Groups、Seal、relayer 和 Walrus 构建可恢复的端到端加密群聊。
注意:Walrus 上的 Blob 默认公开可读。任何敏感数据都必须先加密,再上传到 Walrus。本书在涉及隐私和 AI 记忆的章节会使用 Seal/MemWal 的安全模型作为默认实践。
第 1 章 Walrus 是什么
学习目标
- 用开发者视角理解 Walrus、Sui、Blob、WAL、Epoch 的关系。
- 判断一个文件上传后应该保存哪些 ID。
- 明确本书默认使用 Testnet,何时才切到 Mainnet。
前置条件
- 你能使用 macOS、Linux 或 WSL 终端。
- 你知道普通对象存储里的“上传文件、获得 URL、下载文件”流程。
- 本章不要求已经安装 Walrus。
一分钟模型
Walrus 是面向大文件和非结构化数据的去中心化存储网络。你把文件交给 Walrus,Walrus 客户端把文件编码成多个 sliver,分发到存储节点;Sui 链上对象记录这个 Blob 的所有权、生命周期和可管理状态。
上传后至少要记录两个值:
Blob ID: 内容地址,用来 read、blob-status,也可从同一文件重新计算。
Sui object ID: 链上对象 ID,用来 extend、share、attributes、burn 等管理操作。
这两个 ID 不是同一个东西。Blob ID 由内容决定,同一个文件重复上传会得到同一个 Blob ID;Sui object ID 是链上对象,重复上传或 --force 可能得到新的对象。
网络选择
本书所有命令默认使用 Testnet:
walrus info --context testnet
sui client envs
sui client active-env
常见网络含义:
| 网络 | 用途 | 成本 | 持久性预期 |
|---|---|---|---|
| Testnet | 学习、开发、调试 | 测试 SUI/WAL | 可能重置,不承诺长期保存 |
| Mainnet | 生产数据、真实用户 | 真实 SUI/WAL | 按购买的 Epoch 和网络规则保存 |
| Devnet | 预发布验证 | 测试资产 | 变化更快,不适合本书主线 |
截至 2026-05-06,本书采用的版本事实是:Mainnet mainnet-v1.47.1,Testnet testnet-v1.47.1,Devnet devnet-v1.48.0 为预发布。除非你在验证预发布功能,否则不要把 Devnet 当作稳定教程环境。
最小工作流
下面是本书后续章节反复使用的闭环:
printf 'hello walrus\n' > hello.txt
walrus store hello.txt --epochs 2 --context testnet
walrus blob-status --file hello.txt --context testnet
walrus read <BLOB_ID> --out hello.downloaded.txt --context testnet
diff hello.txt hello.downloaded.txt
store 会消耗 SUI 交易 gas 和 WAL 存储费用。Testnet 可以从 faucet 获取测试 SUI,再用 walrus get-wal --context testnet 换取测试 WAL。
输出解读
典型上传输出会包含类似字段:
Blob ID: oehkoh0352bRGNPjuwcy0nye3OLKT649K62imdNAlXg
Sui object ID: 0x1c086e216c4d35bf4c1ea493aea701260ffa5b0070622b17271e4495a030fe83
保存规则:
- 要下载或公开引用数据,保存
Blob ID。 - 要延长、设置属性、共享、销毁链上对象,保存
Sui object ID。 - 要证明某个本地文件已经在 Walrus 上,优先用
walrus blob-status --file <FILE>。
常见坑
- 把 Blob ID 传给
extend:extend需要--blob-obj-id,也就是 Sui object ID。 - 误以为
delete等于隐私删除:Walrus Blob 是公开可发现的,缓存、历史节点、别人上传的同内容副本都不受你删除命令控制。 - 用 Testnet 数据做生产承诺:Testnet 会变,长期数据请用 Mainnet。
- 忘记 Epoch:Blob 到期后不可读,过期 Blob 不能再正常 extend。
下一步
下一章安装 suiup、sui 和 walrus,并把客户端连到 Testnet。
第 2 章 安装 Sui、Walrus 与环境配置
学习目标
- 用
suiup安装最新稳定的 Sui 和 Walrus CLI。 - 配置 Walrus Testnet 客户端文件。
- 初始化 Sui 钱包并检查环境是否正确。
前置条件
- macOS、Linux 或 WSL。
- 终端可以访问 GitHub 和 Walrus 文档站。
- 推荐安装
curl、jq、diffutils。
安装 suiup
官方推荐用 suiup 管理 Sui 生态 CLI:
curl -sSfL https://raw.githubusercontent.com/Mystenlabs/suiup/main/install.sh | sh
安装完成后,重新打开终端,或者按安装脚本提示把 suiup 所在目录加入 PATH。
检查:
suiup --version
suiup list
安装 sui 和 walrus
默认安装当前推荐版本:
suiup install sui
suiup install walrus
查看实际命中的二进制:
suiup show
suiup which
sui --version
walrus --version
如果你需要明确锁定 Testnet 线,先用 suiup list 查看可用版本,再安装对应项。不要混用 Mainnet Walrus CLI 和 Testnet 配置做本书练习。
配置 Walrus Testnet
下载官方预填配置:
curl --create-dirs \
https://docs.wal.app/setup/client_config.yaml \
-o ~/.config/walrus/client_config.yaml
检查文件存在:
test -f ~/.config/walrus/client_config.yaml
walrus info --context testnet
walrus info --context testnet 应该能显示当前 Walrus 网络信息。Testnet 的 Epoch 时长通常显示为 1day。
初始化 Sui Testnet 钱包
首次运行:
sui client
交互输入建议:
Connect to a Sui Full Node server? Y
Full node server URL: https://fullnode.testnet.sui.io:443
Environment alias: testnet
Select key scheme: 0
已有钱包时,确认当前环境:
sui client envs
sui client switch --env testnet
sui client active-env
sui client active-address
获取测试资产
先复制地址:
sui client active-address
到 Sui Testnet faucet 领取测试 SUI:
https://faucet.sui.io/
检查余额:
sui client balance
把一部分测试 SUI 换成测试 WAL:
walrus get-wal --context testnet
sui client balance
余额里应能看到 Sui 和 WAL Token。
输出解读
sui client active-env
应输出:
testnet
walrus info --context testnet
重点看:
- 当前连接的 Walrus context 是
testnet。 - Epoch 信息能正常加载。
- 没有 RPC、钱包、配置文件错误。
常见坑
walrus: command not found:终端没有加载suiup修改后的PATH,重开终端或检查 shell 配置。No such file or directory ~/.config/walrus/client_config.yaml:重新运行下载配置命令。Insufficient balance:先领 Testnet SUI,再运行walrus get-wal --context testnet。- 当前 Sui 环境不是 Testnet:执行
sui client switch --env testnet。 - Mainnet 操作会花真实资产:本书命令都显式带
--context testnet,生产前再改成mainnet。
下一步
下一章整理账户、网络、Token 和费用模型,并准备后续 CLI 章节使用的工作目录。
第 3 章 账户、网络、Token 与费用
学习目标
- 分清 Sui address、Sui object、Blob ID 的职责。
- 知道上传 Blob 时 SUI 和 WAL 分别支付什么。
- 能在 Testnet/Mainnet 之间显式切换。
前置条件
- 已完成第 2 章安装。
- 当前 Sui 钱包已经连接 Testnet。
- Testnet 账户里有测试 SUI 和测试 WAL。
账户与对象
查看当前地址:
sui client active-address
查看本机管理的地址:
sui client addresses
创建新地址:
sui client new-address ed25519
Walrus 上传成功后,链上会出现一个 Blob 对象。这个对象归你的地址所有,后续管理操作一般由当前钱包签名。
sui client objects
如果对象很多,可以按对象 ID 查看详情:
sui client object <BLOB_OBJECT_ID>
网络切换
查看 Sui 环境:
sui client envs
sui client active-env
切到 Testnet:
sui client switch --env testnet
增加 Mainnet 环境时,使用官方 fullnode:
sui client new-env \
--alias mainnet \
--rpc https://fullnode.mainnet.sui.io:443
切到 Mainnet 前先确认:
sui client switch --env mainnet
sui client active-env
walrus info --context mainnet
本书练习命令默认继续使用:
walrus info --context testnet
Token 与费用
上传 Blob 会涉及两类资产:
| 资产 | 用途 |
|---|---|
| SUI | 支付 Sui 交易 gas,例如创建 Blob 对象、更新属性、删除对象 |
| WAL | 支付 Walrus 存储资源,按大小和 Epoch 计费 |
查看余额:
sui client balance
Testnet 领取和兑换:
sui client active-address
walrus get-wal --context testnet
sui client balance
Mainnet 不存在免费 faucet。切到 Mainnet 前必须明确你将花费真实 SUI/WAL。
准备本书工作目录
后续章节使用一个独立目录,避免把仓库或私钥文件误传:
mkdir -p ~/walrus-book-lab/files ~/walrus-book-lab/downloads
cd ~/walrus-book-lab
printf 'Walrus CLI lab\n' > files/hello.txt
printf '{"name":"walrus","network":"testnet"}\n' > files/meta.json
检查:
find files -maxdepth 1 -type f -print
输出解读
sui client balance 中看到 WAL Token 才能顺利上传。只看到 SUI 时,Walrus 存储付款会失败。
sui client active-env 和 walrus --context 是两套配置入口。为减少误操作,本书所有 Walrus 命令都显式写 --context testnet。
常见坑
- 地址字符串能跨网络复用,但资产不能跨网络复用;同一个地址在 Testnet 和 Mainnet 余额完全不同。
Blob ID不是 Sui 对象,不能用sui client object <BLOB_ID>查询。- 删除本地文件不影响 Walrus;删除 Walrus Blob 也不保证互联网上已缓存的数据消失。
- 钱包助记词和私钥绝不能上传到 Walrus。Walrus 默认公开可发现。
下一步
下一章上传第一个 Blob,并学习如何保存命令输出中的关键字段。
第 4 章 上传第一个 Blob
学习目标
- 使用
walrus store上传单文件和多文件。 - 理解
--epochs、--deletable、--permanent、--force。 - 把上传输出保存为后续命令可复用的变量。
前置条件
- 已完成 Testnet 配置。
- 当前目录为
~/walrus-book-lab。 - 余额中有测试 SUI 和测试 WAL。
创建样例文件
cd ~/walrus-book-lab
mkdir -p files downloads
printf 'hello walrus\n' > files/hello.txt
printf '{"chapter":4,"topic":"store"}\n' > files/chapter-04.json
上传单个 Blob
walrus store files/hello.txt --epochs 2 --context testnet
典型输出:
Blob ID: <BLOB_ID>
Sui object ID: <BLOB_OBJECT_ID>
把两个值写入 shell 变量,后面替换为你的真实输出:
export BLOB_ID='<BLOB_ID>'
export BLOB_OBJECT_ID='<BLOB_OBJECT_ID>'
上传多个文件
walrus store files/hello.txt files/chapter-04.json --epochs 2 --context testnet
也可以用 shell glob:
walrus store files/*.json --epochs 2 --context testnet
glob 由 shell 展开。目录里没有匹配文件时,不同 shell 的行为不同,生产脚本应先 find 检查文件列表。
生命周期参数
最常用的是相对 Epoch:
walrus store files/hello.txt --epochs 2 --context testnet
存到最大可选 Epoch:
walrus store files/hello.txt --epochs max --context testnet
指定最早过期时间:
walrus store files/hello.txt \
--earliest-expiry-time '2026-06-01T00:00:00Z' \
--context testnet
指定结束 Epoch:
walrus store files/hello.txt --end-epoch <END_EPOCH> --context testnet
Blob 在 end epoch 开始时过期;如果在 Epoch 切换前用 --epochs 1,可能很快就过期。
Deletable 与 Permanent
默认新 Blob 是 deletable。你可以显式写出:
walrus store files/hello.txt --epochs 2 --deletable --context testnet
永久到期前不可由上传者提前删除:
walrus store files/hello.txt --epochs 2 --permanent --context testnet
如果你后续要练习 delete,用 --deletable。如果你后续要练习 share,共享 Blob 只能基于 permanent Blob,上传时用 --permanent 或在 store 时直接 --share。
强制创建新对象
同内容重复上传时,Walrus 可能复用已有认证结果。需要新 Sui 对象时使用:
walrus store files/hello.txt --epochs 2 --force --context testnet
这会创建新的 Sui object ID,仍然可能得到相同 Blob ID。
输出解读
Blob ID:内容地址,用于read和blob-status --blob-id。Sui object ID:链上管理对象,用于extend、share、attributes、burn-blobs。- 如果输出提示已经存在或复用存储资源,说明客户端做了自动优化,不一定重新上传全部 sliver。
常见坑
- 忘记
--epochs:当前 CLI 要求明确生命周期。 - 上传敏感文件:Walrus Blob 公开可发现,敏感数据必须先加密。
- 只保存 Blob ID:后续无法方便地 extend 或设置属性。
- 以为
--permanent是永久保存:它表示到期前不可提前删除,不表示无限期保存。
下一步
下一章读取 Blob、下载到文件,并查询 Blob 可用状态。
第 5 章 读取、下载与状态查询
学习目标
- 使用
walrus read把 Blob 写到 stdout 或文件。 - 使用
walrus blob-status按 Blob ID 或本地文件查询状态。 - 理解一致性检查相关参数。
前置条件
- 已上传第 4 章的
files/hello.txt。 - 已设置
BLOB_ID:
echo "$BLOB_ID"
如果没有变量,先从上一章上传输出里复制。
查询状态
按 Blob ID 查询:
walrus blob-status --blob-id "$BLOB_ID" --context testnet
按本地文件查询:
walrus blob-status --file files/hello.txt --context testnet
--file 会根据本地文件内容重新计算 Blob ID,再查询链上和网络状态。它适合验证“这个具体文件是否已存储”。
读取到终端
文本 Blob 可以直接输出:
walrus read "$BLOB_ID" --context testnet
二进制文件不要直接输出到终端,使用 --out。
下载到文件
mkdir -p downloads
walrus read "$BLOB_ID" \
--out downloads/hello.downloaded.txt \
--context testnet
验证内容:
diff files/hello.txt downloads/hello.downloaded.txt
diff 没有输出表示两个文件一致。
使用自定义 RPC
一般使用配置文件即可。需要临时指定 Sui RPC 时:
walrus read "$BLOB_ID" \
--out downloads/hello.rpc.txt \
--rpc-url https://fullnode.testnet.sui.io:443 \
--context testnet
一致性检查
默认一致性检查已经适合大多数读取场景。需要更严格检查时:
walrus read "$BLOB_ID" \
--out downloads/hello.strict.txt \
--strict-consistency-check \
--context testnet
只有在你完全信任写入者且理解风险时,才考虑跳过检查:
walrus read "$BLOB_ID" \
--out downloads/hello.fast.txt \
--skip-consistency-check \
--context testnet
输出解读
blob-status 可用时会返回存储状态、可用期,以及 BlobCertified Sui event ID。这个事件表示该 Blob 的可用性已经被认证。
read --out 成功时,重点检查:
- 目标文件是否存在。
- 文件大小是否符合预期。
- 对文本或确定性文件,
diff是否一致。
ls -lh files/hello.txt downloads/hello.downloaded.txt
常见坑
read默认写 stdout,下载图片、压缩包、数据库快照时必须加--out。- Blob 过期后读取会失败;先用
blob-status看 end epoch。 blob-status --file查询的是文件内容对应的 Blob ID,文件改一个字节就是另一个 Blob。--skip-consistency-check不是性能开关的默认选择,它会降低读取验证强度。
下一步
下一章管理 Blob 生命周期:延长、删除、burn、共享和属性。
第 6 章 管理 Blob 生命周期
学习目标
- 用
extend延长 Blob 存储时间。 - 用
delete删除 deletable Blob。 - 理解
burn-blobs、share、共享 Blob 资助和属性命令。
前置条件
- 已有一个普通 Blob 的
BLOB_ID和BLOB_OBJECT_ID。 - 当前钱包拥有该 Blob 对象。
- 本章命令默认 Testnet,但仍可能改变链上对象状态,先读完再执行。
延长 Blob
extend 使用 Sui object ID,不使用 Blob ID:
walrus extend \
--blob-obj-id "$BLOB_OBJECT_ID" \
--epochs-extended 3 \
--context testnet
延长后检查:
walrus blob-status --blob-id "$BLOB_ID" --context testnet
共享 Blob 延长时加 --shared:
walrus extend \
--blob-obj-id <SHARED_BLOB_OBJECT_ID> \
--epochs-extended 3 \
--shared \
--context testnet
删除 Blob
只有 deletable Blob 才能提前删除。命令会询问确认:
walrus delete --blob-id "$BLOB_ID" --context testnet
脚本化时显式确认:
walrus delete --blob-id "$BLOB_ID" --yes --context testnet
也可以按文件或对象删除:
walrus delete --file files/hello.txt --context testnet
walrus delete --object-id "$BLOB_OBJECT_ID" --context testnet
删除后再次查询:
walrus blob-status --blob-id "$BLOB_ID" --context testnet
注意:删除不会清除缓存、历史节点、别人已经下载的副本,也不会删除别人上传的同内容 Blob。
Burn Blob 对象
burn-blobs 会销毁当前钱包拥有的 Blob 对应 Sui 对象,不删除 Walrus 数据,也不退还存储。Burn 后你会失去对该对象的控制。
walrus burn-blobs --object-ids "$BLOB_OBJECT_ID" --context testnet
批量 burn 前先谨慎列对象,不要对生产钱包运行不理解的批处理。官方还支持按条件 burn:
walrus burn-blobs --all-expired --context testnet
--all 会 burn 当前钱包拥有的所有 Blob 对象,除非你正在清理专用测试钱包,否则不要使用。
Share Blob
共享 Blob 是包装普通 Blob 的共享 Sui 对象,任何人都可以为它继续付费延长。共享 Blob 只能包含 permanent Blob,不能提前删除。
从已有 permanent Blob 创建共享对象:
walrus share --blob-obj-id "$BLOB_OBJECT_ID" --context testnet
上传时直接共享:
walrus store files/hello.txt \
--epochs 2 \
--permanent \
--share \
--context testnet
创建时资助共享 Blob:
walrus share \
--blob-obj-id "$BLOB_OBJECT_ID" \
--amount <AMOUNT_IN_WAL_OR_FROST_AS_CLI_EXPECTS> \
--context testnet
资助已有共享 Blob:
walrus fund-shared-blob \
--blob-obj-id <SHARED_BLOB_OBJECT_ID> \
--amount <AMOUNT_IN_WAL_OR_FROST_AS_CLI_EXPECTS> \
--context testnet
不同版本对金额单位和格式的帮助文本可能更细,执行前用:
walrus share --help
walrus fund-shared-blob --help
设置和读取属性
属性是挂在 Blob Sui 对象上的 key-value 元数据。设置:
walrus set-blob-attribute "$BLOB_OBJECT_ID" \
--attr "content-type" "text/plain" \
--attr "cache-control" "public, max-age=31536000" \
--context testnet
读取:
walrus get-blob-attribute "$BLOB_OBJECT_ID" --context testnet
删除指定 key:
walrus remove-blob-attribute-fields "$BLOB_OBJECT_ID" \
--keys "cache-control" \
--context testnet
删除全部属性:
walrus remove-blob-attribute "$BLOB_OBJECT_ID" --context testnet
输出解读
extend成功后,blob-status的 end epoch 应增加。delete成功后,该对象对应的 deletable 存储资源会回收给钱包复用。burn-blobs成功后,sui client object <BLOB_OBJECT_ID>不应再显示可用对象。get-blob-attribute应显示刚设置的 key-value。
常见坑
- 用 Blob ID 执行
extend、share、属性命令:这些命令要 Sui object ID。 - 对 permanent Blob 执行
delete:permanent 到期前不可提前删除。 - Burn 当成 Delete:Burn 只销毁链上对象,不删除 Walrus 数据。
- Share 普通 deletable Blob:共享 Blob 要求 permanent。
- 属性不是访问控制;不要把 secret 写进属性。
下一步
下一章学习 Quilt,把大量小文件打包成一个 Walrus 存储单元,并按 identifier 或 tag 读取其中部分文件。
第 7 章 Quilt 与批量小文件
学习目标
- 用
store-quilt批量上传小文件。 - 用
read-quilt按 identifier、tag、QuiltPatchId 读取。 - 理解 Quilt 与普通 Blob 生命周期管理的差异。
前置条件
- 已完成 Testnet 环境配置。
- 当前目录为
~/walrus-book-lab。 - 余额中有测试 WAL。
准备小文件集合
cd ~/walrus-book-lab
mkdir -p quilt-src/docs quilt-downloads
printf '# Walrus Notes\n' > quilt-src/docs/notes.md
printf '{"kind":"profile","name":"alice"}\n' > quilt-src/profile.json
printf 'id,value\n1,42\n' > quilt-src/data.csv
按路径上传 Quilt
递归上传目录和文件:
walrus store-quilt \
--epochs 2 \
--paths quilt-src \
--context testnet
输出里保存 Quilt ID 和 Sui object ID:
export QUILT_ID='<QUILT_ID>'
export QUILT_OBJECT_ID='<QUILT_OBJECT_ID>'
Quilt 内每个文件用 identifier 检索。通过 --paths 上传时,文件名会作为默认 identifier;同一个 Quilt 内 identifier 必须唯一,否则上传失败。
用 JSON 指定 identifier 和 tags
需要自定义 identifier 或 tag 时,用 --blobs:
walrus store-quilt \
--blobs '{"path":"quilt-src/docs/notes.md","identifier":"notes","tags":{"type":"markdown","chapter":"7"}}' \
'{"path":"quilt-src/profile.json","identifier":"profile","tags":{"type":"json","chapter":"7"}}' \
'{"path":"quilt-src/data.csv","identifier":"dataset","tags":{"type":"csv","chapter":"7"}}' \
--epochs 2 \
--context testnet
JSON 建议外层用单引号,内部字段用双引号,避免 shell 转义混乱。
列出 Quilt Patch
walrus list-patches-in-quilt "$QUILT_ID" --context testnet
输出会列出每个 patch 的 identifier 和 QuiltPatchId。保存需要精确读取的 patch:
export QUILT_PATCH_ID='<QUILT_PATCH_ID>'
按 identifier 读取
walrus read-quilt \
--quilt-id "$QUILT_ID" \
--identifiers notes profile \
--out quilt-downloads \
--context testnet
检查:
find quilt-downloads -maxdepth 1 -type f -print
按 tag 读取
读取 tag 为 type=json 的 patch:
walrus read-quilt \
--quilt-id "$QUILT_ID" \
--tag type json \
--out quilt-downloads \
--context testnet
读取 tag 为 chapter=7 的 patch:
walrus read-quilt \
--quilt-id "$QUILT_ID" \
--tag chapter 7 \
--out quilt-downloads \
--context testnet
按 QuiltPatchId 读取
walrus read-quilt \
--quilt-patch-ids "$QUILT_PATCH_ID" \
--out quilt-downloads \
--context testnet
多个 patch ID 可以连续传入:
walrus read-quilt \
--quilt-patch-ids <PATCH_ID_1> <PATCH_ID_2> \
--out quilt-downloads \
--context testnet
管理 Quilt 生命周期
Quilt 是一个整体存储单元。普通 Blob 的 delete、extend、share 不能作用到 Quilt 内的单个小文件;要对整个 Quilt 对象操作。
延长整个 Quilt:
walrus extend \
--blob-obj-id "$QUILT_OBJECT_ID" \
--epochs-extended 3 \
--context testnet
删除整个 deletable Quilt:
walrus delete --object-id "$QUILT_OBJECT_ID" --context testnet
输出解读
QUILT_ID用来list-patches-in-quilt和read-quilt --quilt-id。QuiltPatchId用来只读 Quilt 中一个或多个小文件。QUILT_OBJECT_ID是链上对象 ID,用于 extend、delete、share、attributes 等管理操作。
常见坑
- identifier 重复:
--paths默认用文件名,两个目录下都有index.html时会冲突;用--blobs自定义 identifier。 - 把 QuiltPatchId 当 Blob ID:Quilt 内文件用
read-quilt,不是walrus read。 - 想单独删除 Quilt 内一个文件:不能对单个 patch 做普通 Blob 生命周期操作,需要重新打 Quilt。
- 大文件批量不一定适合 Quilt:Quilt 主要优化大量小文件的开销和索引。
下一步
你已经掌握 CLI 主线。后续可以进入 Walrus Sites,把静态站点文件作为 Quilt 上传并由 Sui 链上索引发布。
第 8 章 发布第一个 Walrus Site
Walrus Sites 把静态网站发布成一个 Sui 对象:HTML、CSS、JavaScript、图片等文件存储在 Walrus,站点对象在 Sui 上保存资源索引和所有权。浏览器不会直接访问 Walrus 节点,而是通过 portal 把普通 HTTP 请求解析为 Sui 查询和 Walrus 读取。
本章目标是发布一个最小静态站点,并学会用同一条命令更新它。
准备工作
本书以 2026-05-06 核对到的稳定版本为准:Walrus Mainnet / Testnet release 均为 v1.47.1,Devnet v1.48.0 是预发布版本。除非你明确在测试预发布功能,本章建议使用 testnet-v1.47.1 或 mainnet-v1.47.1 对应的工具链。
你需要已经完成:
sui client active-env
sui client active-address
walrus --version
site-builder --help
Testnet 发布还需要测试网 SUI 和 WAL。Mainnet 发布需要真实资产,并且通过 https://wal.app 访问 Mainnet 站点时需要 SuiNS 名称,第 10 章会展开。
最小站点目录
本仓库提供一个可直接部署的例子:
examples/sites/minimal-static-site/
├── assets/
│ └── site.css
├── about.html
├── index.html
└── ws-resources.json
Walrus Site 的入口文件必须是站点根目录下的 index.html。先本地预览:
cd examples/sites/minimal-static-site
python3 -m http.server 8080
浏览器打开 http://127.0.0.1:8080。这个本地服务器只用于预览;发布到 Walrus Sites 后不需要源站服务器。
第一次部署
从仓库根目录运行:
site-builder --context testnet deploy --epochs 1 examples/sites/minimal-static-site
deploy 会做两件事:
- 把目录中的静态资源打包上传到 Walrus。
- 在 Sui 上创建一个 Walrus Site 对象,保存每个路径到 Walrus 资源的索引。
第一次部署成功后,site-builder 会把新建站点对象 ID 写回 examples/sites/minimal-static-site/ws-resources.json 的 object_id 字段。这个文件应该提交到代码库,因为后续部署要靠它判断“更新已有站点”,而不是“创建新站点”。
如果你不想污染示例文件,可以先复制一份:
cp -R examples/sites/minimal-static-site /tmp/my-walrus-site
site-builder --context testnet deploy --epochs 1 /tmp/my-walrus-site
查看部署结果
部署输出会列出资源路径和站点对象 ID。常用检查命令:
site-builder sitemap --id 0xYOUR_SITE_OBJECT_ID
site-builder convert 0xYOUR_SITE_OBJECT_ID
sitemap 查看站点对象里登记了哪些资源。convert 把十六进制对象 ID 转成 Base36,用于部分 portal 的子域名解析。
访问方式取决于网络和 portal:
| 网络 | 访问方式 |
|---|---|
| Testnet | 使用本地或自托管 portal。官方文档当前不提供公共 Testnet portal。 |
| Mainnet | 使用 https://wal.app,但需要先把 SuiNS 名称指向 Walrus Site 对象。 |
| 自托管 portal | 可使用 Base36 对象 ID 或把 portal 固定到某个站点。 |
更新站点
修改 index.html 或 assets/site.css 后,再运行同一条命令:
site-builder --context testnet deploy --epochs 1 examples/sites/minimal-static-site
只要 ws-resources.json 中已有 object_id,deploy 就会更新对应站点。签名钱包必须是该 Walrus Site 对象的 owner,否则交易会失败。
如果你要显式更新另一个站点:
site-builder --context testnet deploy \
--object-id 0xYOUR_SITE_OBJECT_ID \
--epochs 1 \
examples/sites/minimal-static-site
不要部署源码根目录
对 Vite、Next.js 静态导出、Astro、Docusaurus、mdBook 等项目,site-builder 的目录参数应该指向构建产物,而不是项目根目录:
npm run build
site-builder --context testnet deploy --epochs 3 dist
错误示例:
site-builder --context testnet deploy --epochs 3 .
这样会把 node_modules、源码、临时文件甚至密钥误上传,增加费用,也扩大泄露风险。生产项目应在构建输出目录中保留 index.html、静态资源和 ws-resources.json,只发布浏览器真正需要的文件。
第 9 章 站点配置、路由与元数据
ws-resources.json 是 Walrus Sites 的部署配置文件。site-builder 在部署时读取它,但它本身不会作为网站资源公开给访客。这个文件同时承担两个角色:记录已有站点的 object_id,以及声明 headers、routes、redirects、metadata、ignore 等发布规则。
一个完整但仍然可读的配置如下:
{
"object_id": "0xYOUR_SITE_OBJECT_ID",
"headers": {
"/index.html": {
"cache-control": "no-cache",
"content-type": "text/html; charset=utf-8"
},
"/assets/site.css": {
"cache-control": "public, max-age=31536000, immutable"
}
},
"routes": {
"/app/*": "/index.html",
"/*": "/index.html"
},
"redirects": {
"/old-about": {
"location": "/about.html",
"status_code": 301
},
"/docs": {
"location": "https://docs.wal.app",
"status_code": 302
}
},
"metadata": {
"site_name": "Walrus Starter",
"description": "A minimal static site deployed with Walrus Sites.",
"link": "https://YOUR_SUINS_NAME.wal.app"
},
"ignore": [
".DS_Store",
"*.map",
"README.md"
]
}
object_id
第一次 deploy 没有 object_id 时会创建新站点;成功后 site-builder 会把对象 ID 写入 ws-resources.json。后续部署读取这个 ID 并更新同一个站点。
如果你维护 staging 和 production,建议分开构建目录或分开资源文件:
dist-testnet/ws-resources.json
dist-mainnet/ws-resources.json
不要让测试网部署覆盖主网站的 object_id。
headers
headers 为资源配置 HTTP 响应头。注意:header 规则按资源精确路径匹配,不支持 * 通配符。配置里的 header 名称建议统一小写,尤其是 content-type。构建工具生成带 hash 的文件后,应在发布脚本中为每个产物写入一条精确路径规则。
{
"headers": {
"/index.html": {
"cache-control": "no-cache",
"content-type": "text/html; charset=utf-8"
},
"/assets/app.3f1a2c.js": {
"cache-control": "public, max-age=31536000, immutable",
"content-type": "application/javascript; charset=utf-8"
},
"/assets/app.a91c04.css": {
"cache-control": "public, max-age=31536000, immutable",
"content-type": "text/css; charset=utf-8"
}
}
}
入口 HTML 不应长期强缓存,因为它通常引用最新构建产物。带 hash 的 JS、CSS、图片可以长缓存;下一次构建换文件名即可让浏览器获取新版本。
routes 与客户端路由
routes 是站点内部路由映射。它不会执行服务端代码,只是告诉 portal:当某个 URL 路径没有同名资源时,应该返回哪个已有资源。
单页应用最常用的配置:
{
"routes": {
"/*": "/index.html"
}
}
这样用户直接打开 /dashboard/settings 时,portal 返回 /index.html,再由 React Router、Vue Router、SvelteKit 静态前端等客户端路由接管。
多入口站点可以写更具体的规则:
{
"routes": {
"/admin/*": "/admin.html",
"/docs/*": "/docs.html",
"/*": "/index.html"
}
}
注意两点:
- 路由目标必须是已经存在于站点对象中的资源,例如
/index.html。 - 通配符只能表达路径前缀匹配,复杂动态逻辑应放到前端应用里处理。
redirects
redirects 返回 HTTP 3xx。它适合迁移旧链接或跳转到外部文档:
{
"redirects": {
"/v1": {
"location": "/docs/getting-started.html",
"status_code": 301
},
"/github": {
"location": "https://github.com/MystenLabs/walrus-sites",
"status_code": 302
},
"/legacy/**/*": {
"location": "/index.html",
"status_code": 308
}
}
}
常用状态码:
| 状态码 | 用途 |
|---|---|
301 | 永久迁移,搜索引擎和浏览器可能长期缓存。 |
302 | 临时跳转,适合活动页或未定稿迁移。 |
308 | 永久迁移并保留请求方法。静态站点里常用于严格替换。 |
如果只是为了 SPA 能刷新任意路径,优先用 routes,不要用 redirects。routes 保持浏览器地址不变;redirects 会改变地址栏。
metadata
metadata 写入站点对象,钱包和浏览器工具可以用它展示站点信息:
{
"metadata": {
"site_name": "Walrus Starter",
"description": "A minimal static site deployed with Walrus Sites.",
"link": "https://walrusstarter.wal.app"
}
}
建议生产站点至少填写:
| 字段 | 建议 |
|---|---|
site_name | 用户看到的正式名称。 |
description | 一句话说明站点用途。 |
link | 最终公开访问地址,优先填 SuiNS 或自定义域名。 |
ignore
ignore 防止无关文件进入站点:
{
"ignore": [
".DS_Store",
"*.map",
"*.log",
"README.md",
"node_modules/**/*"
]
}
即使配置了 ignore,也应从构建产物目录部署,而不是从源码目录部署。ignore 是最后一道防线,不是发布流程的主隔离机制。
验证配置
部署前至少做三项检查:
test -f dist/index.html
test -f dist/ws-resources.json
site-builder --context testnet deploy --epochs 1 dist
部署后查看资源索引:
site-builder sitemap --id 0xYOUR_SITE_OBJECT_ID
如果 routes 指向不存在的资源,部署交易会失败。优先在 CI 中让构建产物固定、可检查,再交给 site-builder deploy。
第 10 章 SuiNS、Portal 与自定义域名
Walrus Sites 没有传统源站。portal 是浏览器和 Walrus/Sui 之间的 HTTP 网关:它根据域名或子域名找到站点对象,从 Sui 读取资源索引,再从 Walrus 读取资源字节并返回 HTTP 响应。
portal 如何解析站点
一次请求大致经过这些步骤:
- 浏览器请求
https://NAME.wal.app/path/to/page或你的自托管 portal 域名。 - portal 解析主机名,得到 SuiNS 名称或 Base36 站点对象 ID。
- portal 查询 Sui 上的 Walrus Site 对象和动态字段。
- portal 根据路径、
routes、redirects和资源索引找到目标资源。 - portal 从 Walrus aggregator 读取资源,并带上配置的 headers 返回给浏览器。
这意味着静态资源可以完全去中心化存储,但 portal 仍是访问体验的一部分。生产项目要明确使用公共 portal、第三方 portal,还是自托管 portal。
使用 SuiNS 访问 wal.app
官方 Mainnet portal 是 https://wal.app。当前规则是:通过 wal.app 访问 Walrus Site 需要 SuiNS 名称;Base36 对象 ID 子域名不再用于 wal.app 访问。Base36 仍可用于本地或其他 portal。
SuiNS 设置流程:
-
部署站点并拿到十六进制对象 ID:
site-builder --context mainnet deploy --epochs max dist -
记录输出中的 Walrus Site object ID,或从
dist/ws-resources.json读取:jq -r .object_id dist/ws-resources.json -
在 SuiNS 注册或管理页面设置名称:
网络 入口 Mainnet https://suins.ioTestnet https://testnet.suins.io -
把 SuiNS 名称的 Walrus Site 目标设置为你的站点对象 ID。
-
通过
https://YOURNAME.wal.app访问。
SuiNS 名称应提前规划。wal.app 对可解析名称长度有限制,官方文档当前说明最多 21 个字符;名称字符也应保持在小写字母和数字范围内,避免把品牌名设计成无法解析的域名。
Base36 对象 ID
自托管 portal 或本地 portal 通常需要 Base36 形式的对象 ID:
site-builder convert 0xYOUR_SITE_OBJECT_ID
输出值可用于 portal 子域名或 portal-config.yaml 中的固定站点配置。十六进制对象 ID 是链上对象标识;Base36 是面向域名子域名的编码形式,不要混用。
自定义 DNS 域名
如果你要把站点放到 https://example.com 或 https://docs.example.com,当前官方方案不是把 DNS CNAME 指到 wal.app。wal.app 公共 portal 不支持任意自定义 DNS 域名。
正确架构是:
- 部署 Walrus Site,拿到对象 ID。
- 部署自己的 Walrus Sites portal。
- 在 portal 配置中固定服务你的站点。
- 把 DNS A/AAAA/CNAME 记录指向你的 portal。
- 在 portal 前配置 TLS,例如 Caddy、Nginx、Cloudflare 或云厂商负载均衡。
最小配置示意:
# portal-config.yaml
landing_page_oid_b36: "BASE36_SITE_OBJECT_ID"
allowed_domains:
- "example.com"
- "www.example.com"
不同版本的 portal 配置字段可能随仓库演进调整,生产前以 MystenLabs/walrus-sites 仓库中的模板和 README 为准。关键原则不变:你的 DNS 域名指向你控制的 portal,portal 再固定解析到 Walrus Site。
DNS 与 TLS
以 docs.example.com 为例:
docs.example.com. 300 CNAME portal.example.net.
或直接指向服务器:
docs.example.com. 300 A 203.0.113.10
docs.example.com. 300 AAAA 2001:db8::10
TLS 可以由反向代理终止:
server {
listen 443 ssl http2;
server_name docs.example.com;
ssl_certificate /etc/letsencrypt/live/docs.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docs.example.com/privkey.pem;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:3000;
}
}
portal 应只服务你允许的域名。不要把一个没有限制的 portal 暴露为任意站点网关,除非你明确要运营公共 portal。
外部链接
Walrus Site 内部的相对链接指向同一个站点对象中的资源:
<a href="/about.html">About</a>
外部网站、API、CDN、其他 Walrus Site 都应该使用绝对 URL:
<a href="https://docs.wal.app" target="_blank" rel="noopener noreferrer">
Walrus Docs
</a>
使用 target="_blank" 时保留 rel="noopener noreferrer",避免新页面拿到原页面的 window.opener。
生产检查清单
发布前确认:
| 项目 | 检查 |
|---|---|
| 站点对象 | ws-resources.json 中的 object_id 是否属于当前钱包。 |
| 网络 | Sui、Walrus、site-builder context 是否同为 Mainnet 或同为 Testnet。 |
| 访问名 | Mainnet wal.app 是否已设置 SuiNS。 |
| 自定义域名 | 是否部署了自托管 portal,而不是直接 CNAME 到 wal.app。 |
| TLS | 证书是否覆盖根域名和 www/子域名。 |
| 缓存 | index.html 不长缓存,hash 静态资源长缓存。 |
第 11 章 Walrus Sites CI/CD
Walrus Sites 的 CI/CD 目标很简单:每次合并到主分支后,构建静态产物,调用官方 GitHub Action 运行 site-builder deploy,并保存更新后的 ws-resources.json。
官方 Action 是:
MystenLabs/walrus-sites-github-actions/deploy
它负责下载 site-builder、写入 Sui keystore,并对你指定的静态目录执行部署。它不会替你构建前端项目,所以 npm run build、mdbook build、astro build 等步骤必须放在 Action 之前。
凭据
在 GitHub 仓库中配置:
| 类型 | 名称 | 内容 |
|---|---|---|
| Secret | SUI_KEYSTORE | 部署钱包的 Sui keystore JSON 内容。 |
| Variable | SUI_ADDRESS | 部署钱包地址。 |
部署钱包必须拥有目标 Walrus Site 对象。生产项目建议单独创建部署钱包,只给它站点更新所需资产,不要复用个人主钱包。
最小静态站点部署 workflow
本仓库提供可复制模板:examples/sites/github-actions/deploy-walrus-site.yml。
如果你的仓库直接包含静态目录 site/:
name: Deploy Walrus Site
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Deploy to Walrus Sites
uses: MystenLabs/walrus-sites-github-actions/deploy@main
with:
DIST: site
EPOCHS: 5
SUI_ADDRESS: ${{ vars.SUI_ADDRESS }}
SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SITE_BUILDER_VERSION: v2
第一次部署没有 ws-resources.json 或其中没有 object_id 时,Action 会创建新站点。如果传入了 GITHUB_TOKEN 且权限足够,Action 可以把新生成或更新后的 ws-resources.json 通过 pull request 带回仓库。
Vite / React 示例
Vite 的部署目录是 dist,但 ws-resources.json 通常放在项目根目录或 public/ 中。为了让构建产物带上配置文件,可以把它放到 public/ws-resources.json:
my-app/
├── public/
│ └── ws-resources.json
├── src/
├── index.html
└── package.json
workflow:
name: Deploy Vite App to Walrus Sites
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Deploy to Walrus Sites
uses: MystenLabs/walrus-sites-github-actions/deploy@main
with:
DIST: dist
EPOCHS: 10
SUI_ADDRESS: ${{ vars.SUI_ADDRESS }}
SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SITE_BUILDER_VERSION: v2
如果第一次部署后 Action 创建了更新 ws-resources.json 的 PR,先合并它,再继续后续自动部署。否则每次 CI 都会缺少 object_id,不断创建新站点。
mdBook 示例
mdBook 的构建输出默认是 book/。把 Walrus Sites 配置复制到输出目录:
name: Deploy mdBook to Walrus Sites
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install mdBook
run: |
curl -LsSf https://github.com/rust-lang/mdBook/releases/download/v0.5.2/mdbook-v0.5.2-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz
sudo mv mdbook /usr/local/bin/mdbook
- run: mdbook build
- run: cp ws-resources.json book/ws-resources.json
- name: Deploy to Walrus Sites
uses: MystenLabs/walrus-sites-github-actions/deploy@main
with:
DIST: book
EPOCHS: 10
SUI_ADDRESS: ${{ vars.SUI_ADDRESS }}
SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SITE_BUILDER_VERSION: v2
分环境发布
建议把 Testnet 和 Mainnet 的配置分开:
config/
├── ws-resources.mainnet.json
└── ws-resources.testnet.json
部署前复制到构建目录:
cp config/ws-resources.testnet.json dist/ws-resources.json
GitHub Actions 可以用 workflow_dispatch 参数控制环境:
on:
workflow_dispatch:
inputs:
target:
type: choice
required: true
options: [testnet, mainnet]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm ci
- run: npm run build
- run: cp config/ws-resources.${{ inputs.target }}.json dist/ws-resources.json
- uses: MystenLabs/walrus-sites-github-actions/deploy@main
with:
DIST: dist
EPOCHS: 10
SUI_ADDRESS: ${{ vars.SUI_ADDRESS }}
SUI_KEYSTORE: ${{ secrets.SUI_KEYSTORE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SITE_BUILDER_VERSION: v2
Mainnet 发布建议保留人工触发,Testnet 可以在 push 后自动发布。
CI 失败排查
| 现象 | 处理 |
|---|---|
| 每次都创建新站点 | 检查 ws-resources.json 是否被提交或 Action 是否能创建 PR。 |
owner 或权限错误 | 部署钱包不是 Walrus Site 对象 owner,换钱包或转移对象。 |
找不到 index.html | DIST 指错目录,应该指向构建产物。 |
| Mainnet 部署成功但打不开 | wal.app 需要 SuiNS 名称;先把名称指向站点对象。 |
| 路由刷新 404 | 在 ws-resources.json 加 "routes": { "/*": "/index.html" }。 |
| 静态资源旧版本 | 检查 headers,入口 HTML 不要长期缓存,hash 资源可以长缓存。 |
CI 的核心约束是可重复:同一份源码、同一份 lockfile、同一个 ws-resources.json,应该更新同一个站点对象。只要这个约束成立,Walrus Sites 的发布流程就能稳定接入常规前端工程。
第 12 章 HTTP API:Publisher 与 Aggregator
Walrus 的 HTTP API 把常用读写操作拆成两个角色:
Publisher:接收原始文件或字节,代替客户端完成编码、上传 sliver、收集证明和链上写入。Aggregator:按 blob ID、对象 ID 或 quilt patch ID 读取数据,重建 blob 后用普通 HTTP 响应返回原始字节。
如果你在做 Web 服务、后端批处理、CI 上传或内容分发,优先使用 HTTP API。它的调用模型最简单,也最容易接入已有网关、缓存、鉴权和日志系统。TypeScript SDK 更适合需要用户自己签名、自付存储费,或需要直接控制写入流程的应用。
准备端点
本章示例使用环境变量保存端点:
export PUBLISHER="https://publisher.walrus-testnet.walrus.space"
export AGGREGATOR="https://aggregator.walrus-testnet.walrus.space"
生产环境建议把这些端点放在服务端配置里,不要让前端直接依赖单个自运营节点。你可以为读路径配置多个 aggregator 或 CDN 缓存,写路径则根据业务选择可信 publisher、自运营 publisher,或改用第 13-15 章的 SDK 用户签名流程。
上传字符串
最小写入请求是一个 PUT /v1/blobs:
curl -X PUT "$PUBLISHER/v1/blobs" \
-H "content-type: text/plain; charset=utf-8" \
-d "hello walrus"
未指定 epochs 时,publisher 默认存储 1 个 epoch。测试可以这样做,业务数据应显式指定保存周期。
curl -X PUT "$PUBLISHER/v1/blobs?epochs=5" \
-H "content-type: text/plain; charset=utf-8" \
-d "hello walrus"
响应有两类常见形态。第一次写入新内容时,返回 newlyCreated,其中 blobObject.blobId 是读取用的 blob ID,blobObject.id 是链上 Blob 对象 ID:
{
"newlyCreated": {
"blobObject": {
"id": "0x...",
"blobId": "M4hsZGQ1oCktdzegB6HnI6Mi28S2nqOPHxK-W7_4BUk",
"size": 17,
"certifiedEpoch": 34,
"storage": {
"startEpoch": 34,
"endEpoch": 39
},
"deletable": true
},
"cost": 132300
}
}
如果同样内容已经有满足期限的 certified blob,publisher 可能返回 alreadyCertified:
{
"alreadyCertified": {
"blobId": "M4hsZGQ1oCktdzegB6HnI6Mi28S2nqOPHxK-W7_4BUk",
"event": {
"txDigest": "4XQHFa9S324wTzYHF3vsBSwpUZuLpmwTHYMFv9nsttSs",
"eventSeq": "0"
},
"endEpoch": 39
}
}
应用代码不要假设只会出现一种字段。应统一提取 blob ID:
function getBlobId(result) {
return result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId;
}
上传文件
文件上传使用同一个接口,只是请求体改为文件字节:
curl -X PUT "$PUBLISHER/v1/blobs?epochs=5&deletable=true" \
--upload-file ./photo.jpg
deletable=true 表示 Blob 对象所有者可以在过期前删除该 blob。permanent=true 表示在过期前不可删除。Walrus 官方 HTTP API 当前说明中,新写入 blob 默认是 deletable;面向长期引用、审计、模型产物和公开资产时,建议显式传 permanent=true,避免读者误判可用性。
curl -X PUT "$PUBLISHER/v1/blobs?epochs=10&permanent=true" \
--upload-file ./release.tar.zst
如果希望 Blob 对象发送给另一个 Sui 地址,使用 send_object_to:
curl -X PUT "$PUBLISHER/v1/blobs?epochs=5&send_object_to=$SUI_ADDRESS" \
--upload-file ./report.pdf
这个参数只影响链上 Blob 对象的接收地址,不会改变 blob ID。blob ID 由内容决定,相同内容会得到相同 blob ID。
下载 blob
按 blob ID 读取:
curl "$AGGREGATOR/v1/blobs/<BLOB_ID>" -o ./download.bin
直接打印文本:
curl "$AGGREGATOR/v1/blobs/<BLOB_ID>"
按链上对象 ID 读取:
curl "$AGGREGATOR/v1/blobs/by-object-id/<OBJECT_ID>" -o ./download.bin
按对象 ID 读取时,aggregator 可以根据 Blob 对象属性返回部分 HTTP 头,例如 content-type、content-disposition、content-language、content-encoding、content-location 和 link。如果你在构建下载服务,优先保存对象 ID,因为它能承载更多链上元数据;如果你只需要内容寻址,保存 blob ID 就够。
一致性检查
Aggregator 默认执行与 CLI 一致的读取校验。特殊场景可以通过查询参数调整:
curl "$AGGREGATOR/v1/blobs/<BLOB_ID>?strict_consistency_check=true" \
-o ./checked.bin
只在写入者可信、且你愿意牺牲校验换取性能时才跳过一致性检查:
curl "$AGGREGATOR/v1/blobs/<BLOB_ID>?skip_consistency_check=true" \
-o ./fast.bin
默认不要跳过。Walrus 的价值在于可验证读取,应用层为了几百毫秒优化关闭校验,通常得不偿失。
Quilt:一次上传多个小文件
多个小文件应使用 quilt。Publisher 接收 multipart/form-data,每个字段名是 quilt 内 identifier:
curl -X PUT "$PUBLISHER/v1/quilts?epochs=5" \
-F "readme=@README.md;type=text/markdown" \
-F "logo=@logo.png;type=image/png"
带 Walrus 原生 tags:
curl -X PUT "$PUBLISHER/v1/quilts?epochs=5" \
-F "readme=@README.md;type=text/markdown" \
-F "logo=@logo.png;type=image/png" \
-F '_metadata=[
{"identifier":"readme","tags":{"content-type":"text/markdown"}},
{"identifier":"logo","tags":{"content-type":"image/png"}}
]'
响应里的 blobStoreResult 描述整个 quilt blob,storedQuiltBlobs 给出每个文件的 quiltPatchId:
{
"blobStoreResult": {
"newlyCreated": {
"blobObject": {
"blobId": "6XUOE-Q5-nAXHRifN6n9nomVDtHZQbGuAkW3PjlBuKo"
}
}
},
"storedQuiltBlobs": [
{
"identifier": "readme",
"quiltPatchId": "6XUOE-Q5-nAXHRifN6n9nomVDtHZQbGuAkW3PjlBuKoBAQDQAA"
}
]
}
读取 quilt 内单个文件有两种方式:
curl "$AGGREGATOR/v1/blobs/by-quilt-patch-id/<QUILT_PATCH_ID>" \
-o ./readme.md
curl "$AGGREGATOR/v1/blobs/by-quilt-id/<QUILT_ID>/readme" \
-o ./readme.md
当前 HTTP API 一次请求只取 quilt 内一个 blob;批量读取请在应用层并发请求,或使用第 13 章的 SDK getFiles 批量读取。
Node fetch 示例
examples/http 里有可运行脚本。核心上传逻辑如下:
import { readFile } from 'node:fs/promises';
const publisher = process.env.PUBLISHER;
const file = await readFile(process.argv[2]);
const url = new URL('/v1/blobs', publisher);
url.searchParams.set('epochs', process.env.EPOCHS ?? '3');
url.searchParams.set('deletable', 'true');
const response = await fetch(url, {
method: 'PUT',
body: file,
});
if (!response.ok) {
throw new Error(`publisher failed: ${response.status} ${await response.text()}`);
}
const result = await response.json();
console.log(JSON.stringify(result, null, 2));
下载逻辑就是 GET 后把响应体写入文件:
import { writeFile } from 'node:fs/promises';
const aggregator = process.env.AGGREGATOR;
const blobId = process.argv[2];
const out = process.argv[3] ?? 'blob.bin';
const response = await fetch(`${aggregator}/v1/blobs/${blobId}`);
if (!response.ok) {
throw new Error(`aggregator failed: ${response.status} ${await response.text()}`);
}
await writeFile(out, new Uint8Array(await response.arrayBuffer()));
什么时候不用 HTTP API
HTTP publisher 会替用户完成链上动作,因此它天然是一个受信任写入服务。以下场景更适合 SDK:
- 用户必须用自己的钱包签名并支付 SUI/WAL。
- 应用要把
register、upload、certify分成多个用户交互。 - 你要自己选择 upload relay、storage node 请求策略或恢复上传进度。
- 你要在前端直接读取 blob,并接受 SDK 读取会发起大量 storage node 请求的成本。
实践中常见组合是:公开读走 aggregator/CDN,用户付费写走 SDK,后端托管写走 publisher。
第 13 章 TypeScript SDK 快速上手
Walrus TypeScript SDK 当前以 Mysten SDK 文档为准。本书固定使用:
{
"@mysten/walrus": "1.1.4",
"@mysten/walrus-wasm": "0.2.0",
"@mysten/sui": "2.16.0"
}
最新 SDK 的基本形态是:先创建 SuiGrpcClient,再通过 walrus() 扩展得到 client.walrus。不要再使用旧教程里的 new WalrusClient({ suiRpcUrl, network })。
安装
cd examples/sdk
npm install
示例工程使用 tsx 运行 TypeScript:
npm run write:blob -- ./README.md
npm run read:blob -- <BLOB_ID> ./download.bin
写入需要私钥、SUI gas 和 WAL。设置 WALRUS_PRIVATE_KEY 为 Sui CLI 或钱包导出的 suiprivkey...:
export WALRUS_PRIVATE_KEY="suiprivkey..."
创建客户端
import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
export function createWalrusClient() {
return new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(walrus());
}
walrus() 会根据 Sui client 的 network 使用默认 Testnet 包和对象配置。只有在连接自定义部署、私有网络或官方配置更新尚未进入 SDK 时,才需要手动传 packageConfig。
读取 blob
已知 blob ID 时,最直接的 API 是 readBlob:
const bytes = await client.walrus.readBlob({ blobId });
完整脚本见 examples/sdk/src/read-blob.ts:
import { writeFile } from 'node:fs/promises';
import { createWalrusClient } from './client.js';
const blobId = process.argv[2];
const out = process.argv[3] ?? 'blob.bin';
if (!blobId) {
throw new Error('Usage: npm run read:blob -- <BLOB_ID> [OUT_FILE]');
}
const client = createWalrusClient();
const bytes = await client.walrus.readBlob({ blobId });
await writeFile(out, bytes);
console.log(`wrote ${bytes.length} bytes to ${out}`);
SDK 直连 storage nodes 读取会发起大量请求。面向普通网页下载,aggregator HTTP API 往往更合适;面向需要端到端校验、钱包内操作或自定义恢复逻辑的应用,再使用 SDK 读取。
写入原始 blob
写单个大文件时优先用 writeBlob:
import { readFile } from 'node:fs/promises';
import { createWalrusClient, loadKeypair } from './client.js';
const path = process.argv[2];
if (!path) {
throw new Error('Usage: npm run write:blob -- <FILE>');
}
const client = createWalrusClient();
const signer = loadKeypair();
const blob = await readFile(path);
const result = await client.walrus.writeBlob({
blob,
epochs: Number(process.env.EPOCHS ?? 3),
deletable: process.env.DELETABLE !== 'false',
signer,
});
console.log(JSON.stringify(result, null, 2));
返回值里最常用的是 blobId。保存它用于后续读取:
{
"blobId": "..."
}
写入多个文件
多个小文件用 WalrusFile + writeFiles。当前 SDK 会把传入文件写成一个 quilt,适合页面资源、配置包、小型数据集和批量 JSON。
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { WalrusFile } from '@mysten/walrus';
import { createWalrusClient, loadKeypair } from './client.js';
const paths = process.argv.slice(2);
if (paths.length === 0) {
throw new Error('Usage: npm run write:files -- <FILE...>');
}
const files = await Promise.all(
paths.map(async (path) =>
WalrusFile.from({
contents: await readFile(path),
identifier: basename(path),
tags: {
'content-type': guessContentType(path),
},
}),
),
);
const client = createWalrusClient();
const results = await client.walrus.writeFiles({
files,
epochs: Number(process.env.EPOCHS ?? 3),
deletable: true,
signer: loadKeypair(),
});
console.log(JSON.stringify(results, null, 2));
读取时可以用 getFiles:
const [file] = await client.walrus.getFiles({ ids: [blobIdOrQuiltId] });
const text = await file.text();
const identifier = await file.getIdentifier();
const tags = await file.getTags();
WalrusFile 的读取体验接近 Fetch API 的 Response:可以调用 bytes()、text()、json(),如果来自 quilt,还能读取 identifier 和 tags。
处理可恢复写入
高层 writeBlob 和 writeFiles 支持 onStep 和 resume。把每一步持久化到数据库或本地文件,进程崩溃后可以从上次完成的位置继续:
const result = await client.walrus.writeBlob({
blob,
epochs: 3,
deletable: true,
signer,
resume: await loadSavedStep(fileId),
onStep: async (step) => {
await saveStep(fileId, step);
},
});
如果你只是在脚本里上传小文件,可以先不用恢复逻辑。后端生产服务、浏览器大文件上传和移动网络场景,必须保存 step。
错误重试
Epoch 切换、storage node 临时不可用、网络超时都可能导致可恢复错误。SDK 暴露 RetryableWalrusClientError:
import { RetryableWalrusClientError } from '@mysten/walrus';
try {
await client.walrus.readBlob({ blobId });
} catch (error) {
if (error instanceof RetryableWalrusClientError) {
client.walrus.reset();
await client.walrus.readBlob({ blobId });
} else {
throw error;
}
}
高层 API 已内置部分重试。你只有在自己写外层任务队列、批量上传或低层 flow 时,才需要显式处理。
自定义请求
Node 默认连接超时可能不适合 Walrus storage node 访问。可以给 walrus() 传 storageNodeClientOptions:
import type { RequestInfo, RequestInit } from 'undici';
import { Agent, fetch } from 'undici';
const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
walrus({
storageNodeClientOptions: {
timeout: 60_000,
onError: (error) => console.error(error),
fetch: (url, init) =>
fetch(url as RequestInfo, {
...(init as RequestInit),
dispatcher: new Agent({ connectTimeout: 60_000 }),
}) as unknown as Promise<Response>,
},
}),
);
这属于生产硬化,不是入门必需项。先把读写跑通,再根据日志增加超时、限流和重试。
第 14 章 SDK 上传流程拆解
writeBlob 和 writeFiles 是高层 API,适合脚本和服务端任务。浏览器钱包、断点续传、任务队列和大文件上传通常需要拆开流程:
encode:编码文件,计算 blob ID。register:构造注册交易,把 blob 元数据写到 Sui。upload:把数据上传到 storage nodes 或 upload relay。certify:构造认证交易,提交可用性证明。listFiles:对writeFilesFlow,列出 quilt 里的文件。
SDK 提供 writeBlobFlow 和 writeFilesFlow。它们让你控制每一步什么时候执行、由谁签名,以及如何保存进度。
服务端完整 flow
服务端持有 signer 时,可以使用 run():
import { readFile, writeFile } from 'node:fs/promises';
import { createWalrusClient, loadKeypair } from './client.js';
const blob = await readFile('./dataset.jsonl');
const client = createWalrusClient();
const signer = loadKeypair();
const flow = client.walrus.writeBlobFlow({ blob });
let lastStep;
for await (const step of flow.run({
signer,
epochs: 3,
deletable: true,
})) {
lastStep = step;
await writeFile('walrus-upload-step.json', JSON.stringify(step, null, 2));
console.log(step);
}
console.log('last step', lastStep);
run() 是异步迭代器,每完成一步就产出一个 step。生产服务应把 step 写入数据库,而不是只写本地文件。任务重启后把最后一步传给 resume:
const saved = JSON.parse(await readFile('walrus-upload-step.json', 'utf8'));
const flow = client.walrus.writeBlobFlow({
blob,
resume: saved,
});
for await (const step of flow.run({ signer, epochs: 3, deletable: true })) {
await saveStep(uploadId, step);
}
SDK 会跳过已完成步骤,并只补传缺失数据。这个特性对长文件和不稳定网络非常关键。
手动执行 register/upload/certify
如果你要在每一步之间更新业务状态,可以调用显式方法:
const flow = client.walrus.writeBlobFlow({ blob });
const encoded = await flow.encode();
console.log('blob id', encoded.blobId);
const registered = await flow.executeRegister({
signer,
owner: signer.toSuiAddress(),
epochs: 3,
deletable: true,
});
await flow.upload({
digest: registered.txDigest,
});
const certified = await flow.executeCertify({ signer });
console.log(certified);
executeRegister 和 executeCertify 会用 signer 直接签名并执行交易。它们适合 Node 后端和 CLI。
浏览器钱包为什么要拆步
浏览器钱包签名通常会打开弹窗。浏览器只允许用户点击触发的窗口稳定弹出;如果你在一个长异步链里自动调用第二次签名,弹窗可能被拦截。
所以浏览器里推荐:
- 文件选择后立即
encode,不需要签名。 - 用户点击“注册”按钮时创建
register交易并调用钱包签名。 - 注册交易成功后执行
upload。 - 用户点击“认证”按钮时创建
certify交易并调用钱包签名。 - 认证成功后
listFiles或保存 blob ID。
核心代码如下:
import { WalrusFile } from '@mysten/walrus';
const flow = client.walrus.writeFilesFlow({
files: [
WalrusFile.from({
contents: new Uint8Array(await selectedFile.arrayBuffer()),
identifier: selectedFile.name,
tags: {
'content-type': selectedFile.type || 'application/octet-stream',
},
}),
],
});
await flow.encode();
async function handleRegister() {
const tx = flow.register({
epochs: 3,
owner: currentAccount.address,
deletable: true,
});
const result = await signAndExecuteTransaction({ transaction: tx });
if (result.$kind === 'FailedTransaction') {
throw new Error(result.FailedTransaction.status.error?.message);
}
await flow.upload({ digest: result.Transaction.digest });
}
async function handleCertify() {
const tx = flow.certify();
const result = await signAndExecuteTransaction({ transaction: tx });
if (result.$kind === 'FailedTransaction') {
throw new Error(result.FailedTransaction.status.error?.message);
}
const files = await flow.listFiles();
console.log(files);
}
真实 UI 不要把这些步骤隐藏成一个按钮。用户需要理解为什么会签两次:第一次是注册存储,第二次是认证可用性。
Upload relay
SDK 直接写 storage nodes 需要大量请求。Upload relay 把上传 slivers 的工作交给一个服务端,客户端仍然负责链上资产和签名。
配置方式:
import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
walrus({
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
max: 1_000,
},
},
}),
);
sendTip.max 的单位是 MIST。SDK 会读取 relay 的 /v1/tip-config 并在上限内支付 tip。你也可以手动配置固定 tip:
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
address: '0x...',
kind: {
const: 105,
},
},
}
线性 tip:
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
address: '0x...',
kind: {
linear: {
base: 105,
perEncodedKib: 10,
},
},
},
}
前端大文件上传优先考虑 upload relay。它减少浏览器请求数量,但不是免费信任:relay 可以要求 tip,也可能限流或不可用。应用需要显示失败状态,并允许用户重试或切换 relay。
writeFilesFlow 与 quilt
多文件拆步写入使用 writeFilesFlow:
const flow = client.walrus.writeFilesFlow({
files: [
WalrusFile.from({
contents: new TextEncoder().encode('# README\n'),
identifier: 'README.md',
tags: {
'content-type': 'text/markdown',
},
}),
WalrusFile.from({
contents: JSON.stringify({ version: 1 }),
identifier: 'manifest.json',
tags: {
'content-type': 'application/json',
},
}),
],
});
await flow.encode();
const reg = await flow.executeRegister({
signer,
owner: signer.toSuiAddress(),
epochs: 5,
deletable: true,
});
await flow.upload({ digest: reg.txDigest });
await flow.executeCertify({ signer });
const files = await flow.listFiles();
console.log(files);
writeFiles 和 writeFilesFlow 当前会写成 quilt。读取时可以按 quilt ID 获取文件集合,也可以保存每个文件的 ID 供 HTTP aggregator 单独下载。
状态机建议
不要在数据库里只保存 blobId。生产上传任务至少保存:
uploadId:业务上传任务 ID。owner:支付和拥有 Blob 对象的地址。sourceSha256:原始文件校验。blobId:encode后得到。lastStep:SDK 返回的 step 原文。registerDigest:注册交易 digest。certifyDigest:认证交易 digest。status:encoding、registered、uploading、certified、failed。
这样排查问题时可以区分链上失败、relay 失败、storage node 写入失败和业务回调失败。
第 15 章 前端与后端集成模式
Walrus 应用通常不是只选 HTTP API 或只选 SDK,而是按读写责任组合:
- 公开下载:aggregator HTTP API,加 CDN 或多 aggregator fallback。
- 后端托管上传:publisher HTTP API,服务端统一付费和治理。
- 用户自付上传:TypeScript SDK,钱包签名,必要时接 upload relay。
- 批量小文件:SDK
writeFiles或 HTTP quilt。 - 浏览器大文件:SDK flow + upload relay + 持久化进度。
本章给出几种可以直接落地的集成方式。
模式一:后端代理 publisher
适合内容平台、管理员上传、CI 发布、AI 任务产物归档。用户不直接接触 Sui/WAL,费用由服务端账户或 publisher 承担。
sequenceDiagram
participant UI as Web UI
participant API as App Backend
participant Pub as Walrus Publisher
participant Agg as Walrus Aggregator
UI->>API: POST /api/uploads(file)
API->>API: 鉴权、限额、扫描、记录任务
API->>Pub: PUT /v1/blobs?epochs=...
Pub-->>API: blobId / objectId
API-->>UI: blobId / downloadUrl
UI->>Agg: GET /v1/blobs/{blobId}
后端接口示例:
export async function uploadToWalrus(file: Uint8Array, contentType: string) {
const url = new URL('/v1/blobs', process.env.PUBLISHER);
url.searchParams.set('epochs', '5');
url.searchParams.set('permanent', 'true');
const response = await fetch(url, {
method: 'PUT',
headers: {
'content-type': contentType,
},
body: file,
});
if (!response.ok) {
throw new Error(`publisher failed: ${response.status} ${await response.text()}`);
}
const result = await response.json();
const blobId = result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId;
const objectId = result.newlyCreated?.blobObject?.id;
if (!blobId) {
throw new Error(`publisher response missing blobId: ${JSON.stringify(result)}`);
}
return { blobId, objectId, raw: result };
}
这个模式最容易运营,但 publisher 是写入信任边界。你需要限制文件大小、内容类型、用户额度和保存周期,避免开放接口被刷成存储账单。
模式二:用户钱包直写
适合 NFT 元数据、用户拥有的数据资产、去中心化网盘、付费数据集。用户钱包签名,用户地址拥有 Blob 对象。
sequenceDiagram
participant UI as Browser
participant Wallet as Sui Wallet
participant Relay as Upload Relay
participant Sui as Sui
UI->>UI: encode file
UI->>Wallet: sign register tx
Wallet->>Sui: execute register
UI->>Relay: upload blob bytes/slivers
UI->>Wallet: sign certify tx
Wallet->>Sui: execute certify
关键点:
- 不要把私钥放进浏览器代码;使用钱包适配器签名。
- 注册和认证分成两个用户动作,避免钱包弹窗被浏览器拦截。
- 大文件使用 upload relay,减少浏览器直接访问 storage nodes 的请求数量。
- 保存
lastStep到 IndexedDB 或后端,用户刷新页面后可以恢复。
Vite WASM 配置
Walrus SDK 需要 WASM 编解码。Vite 中导入 WASM URL 后传给 walrus():
import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';
export const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
walrus({
wasmUrl: walrusWasmUrl,
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
max: 1_000,
},
},
}),
);
如果构建系统不能解析 ?url,可以自托管 WASM 文件,或固定 CDN 版本:
walrus({
wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.0/web/walrus_wasm_bg.wasm',
});
不要在生产代码里使用 @latest CDN URL。书中固定版本是为了可复现。
Next.js 配置
Next.js 服务端 API routes 或 server actions 使用 Walrus SDK 时,配置外部包:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['@mysten/walrus', '@mysten/walrus-wasm'],
};
export default nextConfig;
如果你只在浏览器组件使用 SDK,仍然要确认 WASM 文件能被打包或从固定 URL 加载。服务端上传更推荐 HTTP publisher 或 Node SDK;浏览器上传更推荐钱包 flow。
模式三:服务端签名 SDK
适合你不想运行 publisher,但希望服务端自己直接控制 Walrus 写入。服务端保存 WALRUS_PRIVATE_KEY,用 SDK 写 blob:
const result = await client.walrus.writeBlob({
blob: fileBytes,
epochs: 7,
deletable: false,
signer,
onStep: (step) => uploadStore.save(uploadId, step),
resume: await uploadStore.load(uploadId),
});
相比 publisher,这个模式更灵活:
- 可以接 upload relay,也可以直连 storage nodes。
- 可以把 flow step 写进自己的任务表。
- 可以统一处理 retry、审计和业务状态。
代价是服务端需要管理 Sui/WAL 余额、私钥安全和并发上传。
模式四:读路径多 aggregator fallback
Blob 是内容寻址的,读路径天然适合 fallback:
const aggregators = [
'https://aggregator.walrus-testnet.walrus.space',
'https://walrus.example.com',
];
export async function readWithFallback(blobId: string) {
let lastError: unknown;
for (const baseUrl of aggregators) {
try {
const response = await fetch(`${baseUrl}/v1/blobs/${blobId}`);
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return new Uint8Array(await response.arrayBuffer());
} catch (error) {
lastError = error;
}
}
throw lastError;
}
公共资产可以把 aggregator 放在 CDN 后面。注意缓存键要包含完整 URL;如果使用 strict_consistency_check 或 skip_consistency_check,查询参数也是缓存键的一部分。
前端展示下载链接
最简单的下载 URL:
export function walrusDownloadUrl(blobId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/${encodeURIComponent(blobId)}`;
}
对象 ID 下载:
export function walrusObjectDownloadUrl(objectId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-object-id/${objectId}`;
}
Quilt patch 下载:
export function walrusPatchDownloadUrl(patchId: string) {
return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-quilt-patch-id/${patchId}`;
}
浏览器会尝试推断内容类型,但 aggregator 会阻止危险可执行类型的嗅探。需要稳定文件名、下载行为和 MIME 时,用对象属性或自己的后端代理设置响应头。
安全边界
Walrus blob 是公开可发现的。不要直接上传密钥、未加密个人数据、访问令牌、内部日志或商业敏感数据。需要保密时先在应用层加密,再上传密文。
后端代理必须做:
- 鉴权:谁能上传、能保存多久。
- 限额:单文件大小、每日总量、并发数。
- 内容治理:MIME 白名单、病毒扫描、业务审核。
- 成本保护:按用户、项目或 API key 计量。
- 结果校验:保存 blob ID、对象 ID、epoch、交易 digest 和响应原文。
前端钱包直写必须做:
- 明确展示费用、epochs、deletable/permanent。
- 拆分签名步骤。
- 保存恢复状态。
- 给出 aggregator 下载验证入口。
推荐默认架构
大多数产品可以从这个组合开始:
- 管理员和后端任务上传:HTTP publisher。
- 普通用户下载:HTTP aggregator + CDN。
- 用户自拥有资产上传:SDK
writeFilesFlow+ upload relay。 - 小文件批量:quilt。
- 大文件任务:SDK flow + 后端任务表 + 重试。
这个架构把复杂度放在需要控制的地方,把普通读写交给 HTTP 接口,后续再按业务要求逐步替换成更底层的 SDK flow。
第 16 章 Sui 对象模型中的 Walrus Blob
Walrus 把大文件留在存储网络里,把可编程状态放在 Sui 上。开发者真正需要掌握的是两层 ID:
blob_id:Walrus 内容标识,用来向 aggregator 读取数据。- Sui 对象 ID:链上对象标识,用来证明某个 Blob 已注册、归属谁、还能保存多久、是否可以被应用引用。
不要把这两者混成一个字段。blob_id 适合做内容寻址,Sui 对象 ID 适合做权限、索引和生命周期管理。
写入后会得到什么
一次正常上传通常包含三个阶段:
- 客户端把原始字节切片、编码,交给 publisher 或 SDK 写入 Walrus。
- Walrus 存储节点返回可用性相关的签名或确认信息。
- Sui 交易注册 Blob 元数据,产生或更新链上对象。
应用层至少要保存下面这些值:
| 字段 | 来源 | 用途 |
|---|---|---|
blob_id | Walrus 写入结果 | 下载、校验、跨系统引用 |
blob_object_id | Sui 交易结果 | Move 合约引用、索引、权限判断 |
network | 配置文件或 CLI profile | 避免把 testnet ID 用到 mainnet |
epoch / end_epoch | 链上 Blob 元数据 | 判断是否仍在存储期内 |
size | 写入请求或链上事件 | 计费、展示、限额 |
object_digest / tx_digest | Sui 交易结果 | 审计和排错 |
生产系统里不要只存 blob_id。如果后续要在 Move 里验证“这个业务对象引用的是一个真实 Walrus Blob”,你还需要链上对象 ID 或由链上事件构建的索引记录。
Blob 是对象,不是普通 URL
HTTP 读取路径看起来像普通下载:
GET https://<aggregator-host>/v1/blobs/<blob_id>
但这个 URL 本身不表达所有权、租期和应用语义。它只说明某个 aggregator 愿意按 blob_id 返回字节。应用里的业务记录应该指向链上对象或自建索引,再把 blob_id 当作读取地址的一部分。
一个常见的业务对象可以这样建模:
Article object
id: 0x...
title: "Walrus 实战"
walrus_blob_object: 0x... # 链上 Blob 对象 ID
walrus_blob_id: "..." # HTTP 读取用
content_hash: 0x... # 可选:业务层哈希
owner: 0x...
如果你的应用允许替换文件,推荐让业务对象保持稳定,把 Walrus 引用字段更新到新 Blob,而不是复用旧 Blob 的含义。Blob 代表一次写入的内容,业务对象代表应用里的实体。
当前网络的包 ID 和系统对象
Walrus 的链上合约和系统对象 ID 会随网络而不同。写 Move、脚本或索引器时必须从当前网络官方配置读取:
- Mainnet:以
mainnet-v1.47.1对应官方配置为准。 - Testnet:以
testnet-v1.47.1对应官方配置为准。 - Devnet:
devnet-v1.48.0是预发布,不要把它的 ID 写进 mainnet/testnet 代码。
不要从教程、截图或旧项目复制包 ID。正确做法是:
walrus --version
walrus info
sui client active-env
sui client object <WALRUS_SYSTEM_OBJECT_ID>
如果你使用 TypeScript SDK,把网络配置集中在一个模块里,不要把包 ID 分散写在页面组件和后端 handler 中。
对象所有权如何影响集成
Sui 对象常见所有权模型会影响 Walrus 集成:
| 所有权 | 适合场景 | 注意事项 |
|---|---|---|
| 地址拥有对象 | 用户上传的文件、个人资料、NFT 附件 | 更新需要用户签名 |
| 共享对象 | 公共索引、应用级注册表 | 并发和 gas 成本更敏感 |
| 不可变对象 | 发布后不再变的元数据 | 适合做审计锚点 |
| 动态字段 | 按 key 管理大量 Blob 引用 | 需要设计分页和清理策略 |
如果业务记录要频繁更新,不要把每个读操作都依赖共享对象交易。读路径应该优先走索引器或缓存,写路径再回到链上结算状态。
最小设计清单
建一个会引用 Walrus 数据的 Sui 应用时,先回答这些问题:
- Blob 写入由谁付费:用户钱包、后端 sponsor,还是应用金库?
- 链上记录保存
blob_id、Blob 对象 ID,还是二者都保存? - 文件过期后业务对象如何表现:隐藏、提示续期,还是允许重新上传?
- 旧 Blob 是否需要保留审计记录?
- 敏感数据是否在上传前加密?
- 索引器落库时是否保存了
network和tx_digest?
最稳妥的默认方案是:链上业务对象保存 Blob 对象 ID、blob_id、大小和业务哈希;后端索引器监听对象变更和事件;读取时通过 aggregator 拉取字节,并用业务哈希或 SDK 校验结果。
第 17 章 Move 合约引用 Walrus 数据
Move 合约不应该存大文件本体。它应该保存可验证引用:哪个 Walrus Blob、哪个业务对象、谁能更新、什么时候过期。真正的字节读取发生在客户端、后端或 indexer 中。
引用模型
一个实用的引用结构通常包含:
public struct BlobRef has copy, drop, store {
blob_object_id: ID,
blob_id_hash: vector<u8>,
size: u64,
end_epoch: u32,
}
这里故意不直接把长字符串塞进每个对象。Move 里可以保存 vector<u8>,但大字段会推高 gas 和对象大小。生产应用常用两种方案:
- 链上保存短哈希或 bytes 形式的
blob_id,完整字符串由事件和索引器保存。 - 链上保存完整
blob_idbytes,适合对象数量少、读链上状态方便的应用。
如果你要让前端直接从链上对象拼 aggregator URL,第二种更方便;如果你要管理大量资源,第一种更节省。
最小 Move 示例
本仓库的 examples/move 提供了一个最小示例。它不直接依赖官方 Walrus Move 模块,因为不同网络的包 ID、对象类型和入口函数需要以官方最新配置替换。示例重点是业务对象如何持有 Walrus 引用。
核心结构如下:
public struct Asset has key, store {
id: UID,
owner: address,
blob: BlobRef,
label: String,
}
发布前你必须替换:
| 占位项 | 替换来源 |
|---|---|
walrus = "0x0" | 当前网络官方 Walrus package ID |
WALRUS_SYSTEM_OBJECT_ID | 当前网络 Walrus system object |
blob_object_id | 上传 Blob 后的 Sui 对象 ID |
blob_id_hash 或 blob_id_bytes | 上传返回的 blob_id 派生值 |
Mainnet 使用 mainnet-v1.47.1 对应配置,Testnet 使用 testnet-v1.47.1 对应配置。Devnet devnet-v1.48.0 是预发布,只适合实验。
合约边界
Move 能做:
- 记录某个业务对象引用哪个 Blob。
- 限制谁可以创建、替换、删除引用。
- 发事件,让 indexer 建立链上到链下的读取索引。
- 检查引用字段的基本格式、大小和过期 epoch。
Move 不适合做:
- 从 Walrus HTTP aggregator 下载字节。
- 在链上解码大文件。
- 在每次读内容时发交易。
- 把完整文件内容写进对象字段。
如果你需要“内容必须符合某个格式”,把校验放在上传前或后端任务里,再把校验结果写成链上状态。例如:图片宽高、MIME、JSON schema 版本、AI 数据集摘要。
创建和更新工作流
推荐的创建流程:
- 客户端选择文件并计算业务哈希。
- 后端或 SDK 上传到 Walrus,拿到
blob_id和链上对象 ID。 - 客户端发 Sui 交易调用
create_asset,把 Walrus 引用写入业务对象。 - 合约发
AssetCreated事件。 - 索引器监听事件,保存完整
blob_id、业务 ID、owner、tx digest。
更新流程不要直接覆盖历史:
- 上传新 Blob。
- 调用
update_blob_ref更新业务对象。 - 事件里带上 old/new 引用。
- 后端决定旧 Blob 是否续期、保留或等待自然过期。
这样做可以把业务状态更新和存储生命周期分开。用户替换头像时,头像对象是一个业务实体;旧图片 Blob 是一次历史内容写入。
权限模式
常见权限模式如下:
| 模式 | 实现 | 适用场景 |
|---|---|---|
| 用户自管 | assert!(tx_context::sender(ctx) == asset.owner, E_NOT_OWNER) | 个人文件、个人主页 |
| 管理员能力 | 发布时铸造 AdminCap | 内容审核、应用配置 |
| 集合拥有者 | 业务对象挂在 collection 下 | NFT、项目资料库 |
| 多签或治理 | 由外层合约控制入口 | 协议参数、公共数据集 |
不要用“知道 blob_id”当权限。Walrus Blob 默认公开可读;访问控制应由加密、Seal、后端授权或业务合约控制。
事件设计
事件是链上索引的主接口。至少包含:
public struct AssetCreated has copy, drop {
asset_id: ID,
owner: address,
blob_object_id: ID,
size: u64,
end_epoch: u32,
}
如果完整 blob_id 不写入对象,也可以放进事件。但要记住事件不是对象状态,合约内部后续无法读取历史事件。凡是合约逻辑以后还要判断的字段,必须存在对象里。
测试重点
Move 单测应该覆盖:
- 非 owner 不能更新引用。
end_epoch早于当前 epoch 的引用不能创建,或者会被标记为已过期。size超过业务限制时拒绝。- 更新事件包含旧引用和新引用。
- 删除业务对象不会假装删除 Walrus Blob。
最后一条很重要:链上业务对象删除不等于 Walrus 存储立即删除。Walrus 的生命周期由 Blob 存储期和官方合约规则控制。
第 18 章 链上索引与可验证数据工作流
Walrus 应用通常有三条数据线:
- Walrus 数据面:
blob_id -> bytes。 - Sui 状态面:对象、交易、事件、epoch。
- 应用索引面:数据库、搜索、缓存、权限视图。
可靠系统不是只读一个 aggregator,也不是只扫链上对象。它会把三条线对齐:链上状态说明“应该有什么”,Walrus 返回“实际能读到什么”,索引器提供“产品需要怎样查询”。
推荐索引表
一个通用表结构:
| 字段 | 说明 |
|---|---|
network | mainnet、testnet、devnet |
asset_id | 业务对象 ID |
blob_object_id | Walrus Blob 对象 ID |
blob_id | HTTP 读取 ID |
owner | 业务 owner |
size | 字节数 |
end_epoch | 存储到期 epoch |
tx_digest | 最近一次写入交易 |
object_version | Sui 对象版本 |
status | active、expired、replaced、missing |
updated_at | 索引器更新时间 |
network + asset_id 应该唯一。不要让 mainnet 和 testnet 数据共用同一个主键空间。
事件驱动流程
推荐用事件作为主入口:
- 合约在创建或更新 Walrus 引用时发事件。
- 索引器按 checkpoint 拉取事件。
- 索引器再读取相关对象,确认对象当前版本。
- 后台任务调用 aggregator 做抽样读取或完整校验。
- 前端只查应用 API,不直接扫链。
事件驱动的好处是成本稳定、延迟可控,而且容易恢复。索引器重启后从最后处理的 checkpoint 继续即可。
可验证读取
读取路径不要只做“HTTP 200 就成功”。至少校验:
- 返回的内容长度是否等于记录的
size。 - SDK 或协议层是否能验证 Blob 身份。
- 如果业务层保存了
sha256或 BLAKE2/3 摘要,下载后重新计算。 - 读取的
blob_id是否来自当前网络的索引记录。
后端示意流程:
GET /assets/:id
-> 查 DB 得到 blob_id、size、hash、end_epoch
-> 若 end_epoch 已过期,返回需要续期
-> 向 aggregator 读取
-> 校验大小和业务哈希
-> 返回内容或签名 URL / 代理流
如果是大文件,不要在 API 层一次性读入内存。用流式代理、Range 请求或让前端直接读 aggregator,再把校验任务放在前端 worker 或后台任务中。
重组、延迟和幂等
Sui checkpoint 提供稳定的处理边界,但你的应用仍要处理重复投递和延迟:
- 事件处理必须幂等,以
tx_digest + event_seq或对象版本去重。 - 同一对象的旧事件晚到时,不应覆盖新版本。
- 索引器落库前先比较
object_version。 - 任何“已上传但未上链”的临时记录都要有超时清理。
一个安全的更新规则:
if incoming.object_version > stored.object_version:
update row
else:
ignore stale event
如果只按时间戳更新,节点时钟、重试和队列延迟都会制造脏数据。
链上索引对象还是链下数据库
共享对象注册表适合保存协议级目录,例如某个 collection 的根索引、治理控制的官方数据集列表。它不适合承担所有查询需求。
链上索引适合:
- 少量、必须被合约读取的状态。
- 需要权限和资产语义的记录。
- 需要不可抵赖的审计锚点。
链下索引适合:
- 全文搜索、标签、排序、分页。
- 大量派生字段。
- 内容可用性探测结果。
- 多 aggregator 健康评分。
实践上,两者要组合:链上保存最小可信事实,链下保存产品视图。
续期和过期处理
过期不是偶发错误,而是生命周期的一部分。索引器应定期扫描:
end_epoch - current_epoch <= renew_threshold
然后把记录标记为 renew_required,通知 owner 或触发 sponsor 流程。不要等用户读取失败后才发现文件过期。
如果应用允许自动续期,必须设置预算边界:
- 单用户每日续期上限。
- 单 Blob 最大保存期。
- 项目级 WAL/SUI 余额告警。
- 续期失败的降级展示。
审计日志
每次引用变化都应保留审计记录:
| 字段 | 用途 |
|---|---|
old_blob_object_id | 回滚和争议处理 |
new_blob_object_id | 当前内容定位 |
actor | 谁发起变更 |
tx_digest | 链上证据 |
reason | 可选,业务原因 |
不要为了“节省表”只保留最新值。Walrus 存的是内容,业务系统还要解释内容为什么变了。
第 19 章 Walrus 架构全景
Walrus 可以按三层理解:
- 控制层:Sui 上的合约、对象、epoch、费用和委员会状态。
- 数据层:存储节点保存 Blob 的编码片段,并参与可用性确认。
- 接入层:publisher 负责写入入口,aggregator 负责读取聚合,SDK/CLI 负责客户端流程。
这三层解耦是 Walrus 的核心。Sui 不存大文件,只存可验证状态;存储节点不决定你的业务权限;publisher 和 aggregator 是方便接入的服务,不应该被当成唯一可信源。
组件职责
| 组件 | 职责 | 生产边界 |
|---|---|---|
| Walrus CLI | 上传、下载、查询、调试 | 适合运维和脚本,不适合高并发服务入口 |
| TypeScript SDK | 应用内写入、读取、证明处理 | 适合前后端集成,注意版本和网络配置 |
| Publisher | 接收写请求,协调编码和注册 | 可自建,也可用公共服务;要做限流和鉴权 |
| Aggregator | 根据 blob_id 聚合片段并返回字节 | 可横向扩展;要监控延迟和错误率 |
| Storage node | 保存编码片段并响应协议请求 | 需要稳定磁盘、网络和密钥管理 |
| Sui 合约 | 记录 Blob 状态、费用、委员会信息 | 包 ID 和系统对象按网络区分 |
写入路径
写入不是简单上传到一台服务器:
- 客户端准备数据和存储参数。
- 数据被编码成多个片段。
- 片段分发给存储节点。
- 节点返回可用性相关确认。
- Sui 交易把 Blob 注册到链上状态。
- 客户端得到
blob_id和链上对象引用。
这意味着写入失败可能发生在不同阶段。工程上要区分:
- 编码或本地文件错误。
- publisher 请求失败。
- 存储节点确认不足。
- Sui 交易失败。
- 交易成功但客户端超时。
最后一种最容易误判。遇到超时时先查 Sui 交易或对象状态,不要立刻重复扣费上传。
读取路径
读取路径通常是:
- 客户端或后端拿到
blob_id。 - 请求 aggregator。
- aggregator 从足够多的存储节点取回片段。
- aggregator 重构原始字节并返回。
- 客户端做可选校验。
读取高峰时,瓶颈通常在 aggregator 到存储节点的网络、重构 CPU、出口带宽和缓存命中率。对公开热点内容,可以在 aggregator 前加 CDN,但不要让 CDN 成为唯一数据来源;源站仍要能从 Walrus 读取并校验。
Epoch 和委员会
Walrus 的存储承诺与 epoch 相关。你的应用不需要理解每个协议细节,但必须知道:
- Blob 有存储期,不是永久自动保存。
- 存储节点集合和参数会随 epoch 变化。
- 续期、过期、费用估算都要看当前 epoch。
- 运行节点或服务时,版本必须跟目标网络兼容。
主线稳定版本以 Mainnet mainnet-v1.47.1、Testnet testnet-v1.47.1 为准。Devnet devnet-v1.48.0 是预发布,适合提前测试,不适合作为生产依赖。
信任模型
Walrus 的目标不是让你信任某一个 HTTP 端点。正确的信任边界是:
- 链上对象和事件提供可审计状态。
- 存储协议提供数据可用性基础。
- aggregator 提供便利读取,但返回结果应可校验。
- 应用层负责加密、鉴权、业务哈希和合规删除策略。
如果数据敏感,上传前加密。Walrus 默认公开可读,blob_id 泄露就应视为内容可被读取。
架构选择
小型应用可以从公共 publisher/aggregator 起步:
frontend -> app backend -> public publisher
frontend -> public aggregator -> bytes
backend -> Sui RPC/indexer -> metadata
规模上来后,推荐自建接入层:
frontend -> app API -> private publisher -> Walrus
frontend -> CDN -> private aggregator -> Walrus
indexer -> Sui fullnode/RPC -> DB
自建 publisher/aggregator 的价值不是“更去中心化”,而是可控的 SLA、限流、审计、缓存策略和故障隔离。
第 20 章 Red Stuff、纠删码与可用性
Walrus 的数据不是简单复制 N 份。它使用纠删码把原始 Blob 编码为多个片段,只要取回足够多的片段就能重构数据。Red Stuff 是 Walrus 论文和实现中围绕可用性证明、编码片段和存储节点确认的一组协议设计。
本章不推导公式,只讲开发者要用到的结论。
纠删码解决什么问题
复制的思路是“多存几份完整文件”。纠删码的思路是“把文件变成更多片段,丢一部分也能恢复”。它的优势:
- 在同等可靠性下节省存储成本。
- 单个节点离线不一定影响读取。
- 可以让可用性证明围绕片段而不是整文件展开。
代价是:
- 写入前需要编码,读取时可能需要重构。
- 小文件会有固定开销,适合用 Quilt 或批处理降低成本。
- 读取延迟取决于足够多片段的响应速度,而不是最快一台节点。
开发者看到的现象
纠删码会带来几个工程现象:
- 同一个 Blob 的读取延迟可能波动,因为 aggregator 要联系多个节点。
- 某些存储节点失败时,读取仍可能成功。
- 如果不足够多片段可用,读取会失败,而不是返回“部分文件”。
- 大文件比小文件更能摊薄元数据和编码开销。
所以不要用一次读取失败判断 Blob 已经丢失。先换 aggregator、重试、查链上状态,再判断是否过期或不可用。
小文件策略
大量小 JSON、图片缩略图、配置片段直接逐个上传,会把固定开销放大。推荐:
- 使用 Quilt 把多个小文件打包。
- 在业务索引里保存 Quilt 内部路径或 key。
- 对热点小文件做应用级缓存。
- 把频繁变化的小状态留在数据库或 Sui 对象里,不要每次都写 Walrus。
Walrus 适合保存 Blob,不适合替代键值数据库。
可用性和持久性
可用性是“现在能不能读到足够片段”,持久性是“未来一段时间内数据不会丢”。它们有关但不是同一个指标。
应用要分别监控:
- 当前读取成功率。
- 写入成功率。
- Blob 到期时间。
- 多 aggregator 读取一致性。
- 存储节点或服务版本是否落后。
如果只监控 HTTP 200,会漏掉续期失败、慢读、数据校验失败和网络分区。
和业务哈希配合
Walrus 的 blob_id 是协议层标识。业务系统仍建议保存自己的内容摘要:
sha256(original_bytes) -> business_hash
用途:
- 防止上传前后拿错文件。
- 支持业务去重。
- 支持审计和用户可见校验。
- 支持加密场景下区分明文哈希和密文哈希。
敏感数据要注意:明文哈希可能泄露已知文件的存在性。隐私场景更适合保存密文哈希或带盐摘要。
调优方向
如果你的读取慢,排查顺序通常是:
- aggregator 是否过载或离用户太远。
- Blob 是否为冷数据,缓存命中率低。
- 存储节点响应是否长尾严重。
- 客户端是否串行读取大量小 Blob。
- 应用是否没有使用 Range、批量索引或 CDN。
不要先怀疑纠删码本身。多数延迟问题来自接入层、网络和请求形态。
第 21 章 一致性、证明与故障模型
Walrus 应用要面对两类一致性:
- 链上一致性:Sui 对象和事件最终落在 checkpoint 中。
- 数据可用性一致性:Blob 片段被足够多存储节点保存并可读取。
用户看到的“上传成功”只有在这两类状态都达到应用要求后才应该展示。
成功状态的定义
工程上建议把写入状态拆成:
| 状态 | 含义 |
|---|---|
prepared | 本地文件已读取,参数已确认 |
stored | Walrus 数据写入流程完成 |
registered | Sui 交易成功,链上对象可查 |
indexed | 应用索引器已处理事件 |
verified | 后台读取和哈希校验通过 |
前端可以在 registered 后显示“已上传”,但后台仍应推进到 indexed 和 verified。如果产品对可靠性要求高,比如审计日志或数据集登记,应等校验完成后再显示最终成功。
证明和校验
开发者常用三层校验:
- 协议/SDK 校验:确认读取到的是目标
blob_id的内容。 - 业务哈希校验:确认内容等于用户上传或后端生成的原始字节。
- 链上状态校验:确认业务对象引用的 Blob 与当前 owner、版本、epoch 匹配。
不要省掉第三层。否则用户可能拿一个真实存在的 Blob 替换业务对象语义,造成“内容有效但引用非法”。
常见故障
| 故障 | 表现 | 处理 |
|---|---|---|
| publisher 超时 | 上传接口无响应 | 查 Sui 交易和 Blob 状态,避免重复写 |
| aggregator 失败 | 下载 5xx 或超时 | 换 aggregator,重试,检查是否过期 |
| Blob 过期 | 读取失败或状态不可用 | 触发续期或展示已过期 |
| Sui RPC 落后 | 查不到刚提交对象 | 换 RPC 或等待 checkpoint |
| 索引器延迟 | 前端列表没更新 | 直接查 tx/object 兜底,后台补索引 |
| 版本不匹配 | CLI/服务报协议错误 | 升级到目标网络对应 release |
| 网络混用 | testnet Blob 在 mainnet 查询失败 | 所有记录带 network 字段 |
重试策略
重试要按阶段设计:
- 本地编码失败:不重试,提示文件或内存问题。
- HTTP 429:指数退避,尊重服务限流。
- HTTP 5xx:换 endpoint 或短退避重试。
- Sui 交易不确定:先按 digest 查询,不要盲目重发。
- 校验失败:不要返回内容,记录严重告警。
推荐保存一个 upload job:
job_id
file_hash
network
blob_id
blob_object_id
tx_digest
state
retry_count
last_error
这样服务重启后可以继续推进,不会因为内存状态丢失而重复上传。
故障隔离
生产环境至少准备两个读取 endpoint。前端可以配置主 aggregator 和备用 aggregator;后端可以根据健康检查动态选择。
隔离原则:
- 写入服务和读取服务分开扩容。
- 索引器失败不应影响已知 Blob 的直接读取。
- CDN 失败时能回源 aggregator。
- 单个 RPC provider 落后时能切换。
- 后台校验失败不应默默覆盖成功状态。
最终一致的用户体验
最终一致不是让用户猜。界面和 API 应明确状态:
processing:交易或索引还没完成。available:可读取并通过校验。renew_required:快过期。expired:超过存储期。unverified:已上链但后台校验未完成。failed:写入或校验失败。
把这些状态暴露给产品,比把所有异常都显示成“网络错误”更容易运营。
第 22 章 成本估算、安全与隐私边界
Walrus 的生产成本来自三类资源:存储期、数据大小、接入层流量。Sui 交易 gas 也要算,但通常不是大文件应用的唯一成本。
成本模型
估算前先收集:
- 平均文件大小和 P95 文件大小。
- 每天新增 Blob 数量。
- 保存期:按 epoch 或产品周期换算。
- 读取次数和出口流量。
- 小文件比例。
- 是否需要自建 publisher/aggregator。
粗略公式:
总成本 = Walrus 存储成本 + Sui gas + 接入服务成本 + CDN/带宽 + 运维人力
不要只看上传价格。读取热点内容时,aggregator、CDN 和出口带宽会成为主要成本。
降成本策略
- 小文件用 Quilt 或批量打包。
- 对重复内容做业务哈希去重。
- 对可再生成内容设置较短保存期。
- 热点公开内容加 CDN。
- 冷数据延迟读取,不做无意义预热。
- 索引器只保存必要派生字段,避免反复扫链。
不要把数据库快照、日志分片、临时构建产物全部长期上传。先按数据价值分级,再决定保存期。
安全边界
Walrus 负责去中心化 Blob 存储和可用性,不负责你的应用权限。默认假设:
- 知道
blob_id的人可以尝试读取内容。 - 公共 aggregator 不会替你做用户鉴权。
- 删除业务对象不等于立即删除已上传数据。
- 链上元数据公开可见。
因此,敏感数据必须先加密再上传。访问控制应在加密密钥、Seal 策略、后端授权或 Move 业务权限中实现。
加密实践
推荐模式:
- 客户端或可信后端生成数据密钥。
- 用数据密钥加密文件。
- 上传密文到 Walrus。
- 用 Seal 或应用自己的密钥系统控制谁能拿到数据密钥。
- 链上只保存密文 Blob 引用和必要策略对象。
不要把明文哈希、文件名、用户邮箱、身份证明等敏感元数据直接写到链上。即使内容加密,元数据也可能泄露业务关系。
隐私清单
上传前检查:
- 文件内容是否加密。
- 文件名是否包含敏感信息。
- MIME、大小、标签是否会泄露业务含义。
- 链上事件是否包含用户私密字段。
- 日志是否记录了完整
blob_id、访问 token 或明文摘要。 - CDN cache key 是否会暴露租户 ID。
对企业应用,建议把 blob_id 当成敏感引用处理:可以内部记录,但不要随意打印到公开日志和前端错误信息。
权限和滥用防护
自建 publisher 必须做:
- 用户认证。
- 单用户大小和频率限制。
- 文件类型和最大尺寸限制。
- 预算上限。
- 恶意内容处理流程。
- 请求审计。
自建 aggregator 至少做:
- 请求限流。
- Range 和大文件下载策略。
- 缓存控制。
- 禁止把内部错误栈返回给客户端。
- 对异常
blob_id输入做格式检查。
公共 Blob 读取不等于公共 API 无限流量。应用后端仍要保护自己的出口成本。
第 23 章 服务提供者、Publisher 与 Aggregator 运维
Walrus 生产运维分三种角色:
- 应用接入运维:维护 publisher、aggregator、缓存和索引器。
- 存储节点运维:运行 Walrus storage node,参与网络存储。
- 协议服务提供者:为团队或客户提供稳定的写入和读取 endpoint。
大多数应用团队先从第一种开始。不要一上来就运行 storage node,除非你已经准备好长期维护磁盘、网络、密钥、监控和升级。
Publisher 运维边界
publisher 是写入入口,生产上要把它当成有成本的交易服务:
- 只对认证用户开放。
- 配置请求体大小上限。
- 记录 upload job 和 Sui tx digest。
- 对 429、5xx、Sui 交易不确定状态做分阶段重试。
- 监控写入成功率、P95/P99 延迟、失败原因。
- 保护私钥和 sponsor 账户。
如果 publisher 代用户付费,要把预算控制写进服务逻辑,而不是只靠钱包余额自然耗尽。
Aggregator 运维边界
aggregator 是读取入口,主要压力来自热点内容和大文件:
- 横向扩展读取服务。
- 对热点 Blob 做缓存。
- 支持 Range 或分片下载策略。
- 配置超时、重试和备用 upstream。
- 监控读取成功率、重构耗时、出口带宽。
- 对异常流量做限速。
如果你在前面加 CDN,要明确缓存失效策略。用户替换业务对象后,旧 Blob 仍可能被缓存;前端 URL 应包含业务版本或内容 ID,避免拿到旧内容。
Storage node 运维边界
storage node 是长期承诺,不只是启动一个二进制。你需要:
- 稳定磁盘和容量规划。
- 可靠网络和公网可达性。
- 钱包、节点密钥、备份和权限隔离。
- 跟随 Mainnet/Testnet release 升级。
- 监控磁盘、CPU、内存、网络、协议错误。
- 明确参与网络的经济和惩罚规则。
生产主网以 mainnet-v1.47.1 为稳定版本事实;测试环境以 testnet-v1.47.1 为准。Devnet devnet-v1.48.0 是预发布,节点运维只能用于试验升级路径。
配置管理
所有服务都应该显式配置网络:
WALRUS_NETWORK=mainnet
WALRUS_CONFIG=/etc/walrus/client.yaml
SUI_RPC_URL=https://...
PUBLISHER_BIND=0.0.0.0:...
AGGREGATOR_BIND=0.0.0.0:...
不要靠“当前 CLI active env”决定生产服务连哪个网络。容器重建、运维手工命令和 CI 环境都可能让 active env 变成错误值。
发布策略
升级 publisher/aggregator:
- 在 testnet 用同版本配置跑集成测试。
- 新实例启动后只接健康检查。
- 小流量切换。
- 观察写入/读取错误率。
- 完成切换后保留旧实例一段时间。
升级 storage node 更谨慎:
- 先读官方 release note。
- 确认目标网络版本。
- 备份配置和密钥。
- 避免在 epoch 边界或业务高峰做无准备升级。
- 升级后检查节点同步、存储服务和日志。
实操边界
应用团队常见误区:
- 把公共 publisher 当成永久 SLA。
- 没有保存 tx digest,上传失败后无法判断是否已上链。
- aggregator 没有限流,被大文件下载打满出口。
- 所有环境共用一个 sponsor 钱包。
- 日志记录明文文件名和敏感
blob_id。 - 只在前端配置网络,后端索引器连了另一个网络。
先把这些基础问题解决,再考虑更复杂的节点运维。
第 24 章 监控、排错与升级
Walrus 应用的监控要覆盖写入、读取、链上状态和索引器。只监控 Web API 存活,会在真正出问题时没有定位信息。
指标清单
publisher:
- 上传请求数、成功率、失败率。
- P50/P95/P99 写入延迟。
- 单用户上传大小和频率。
- Sui 交易提交失败数。
- 不确定状态 job 数量。
aggregator:
- 读取请求数、成功率、失败率。
- P50/P95/P99 首字节和总耗时。
- 重构失败数。
- 出口带宽。
- cache hit ratio。
索引器:
- 当前处理 checkpoint。
- 落后 checkpoint 数。
- 事件处理失败数。
- DB 写入延迟。
- 重放和去重计数。
业务:
- 即将过期 Blob 数量。
- 校验失败 Blob 数量。
- 用户可见下载失败率。
- sponsor 钱包余额。
日志字段
每条关键日志至少带:
request_id
network
blob_id
blob_object_id
tx_digest
asset_id
user_id 或 tenant_id
state
error_code
duration_ms
敏感应用里,blob_id、文件名、用户 ID 要按内部安全规范脱敏或限制访问。不要为了排错把明文字段打进公开日志系统。
排错顺序
上传失败:
- 看本地文件是否可读、大小是否超过限制。
- 查 publisher 返回码和错误体。
- 查 Sui tx digest 是否存在。
- 查 Blob 对象是否创建。
- 查用户或 sponsor 余额。
- 查服务版本是否匹配目标网络。
读取失败:
- 确认
network正确。 - 确认
blob_id没有截断或编码错误。 - 换一个 aggregator。
- 查 Blob 是否过期。
- 查业务对象是否已替换引用。
- 做哈希校验,确认不是缓存旧内容。
索引异常:
- 查 checkpoint lag。
- 查失败事件和重试队列。
- 用
tx_digest直接查 Sui RPC。 - 比较对象版本,避免旧事件覆盖新状态。
- 必要时从某个 checkpoint 重放。
告警阈值
起步阈值可以这样设:
- 写入成功率 5 分钟低于 99% 告警。
- 读取成功率 5 分钟低于 99.5% 告警。
- 索引器落后超过 100 个 checkpoint 告警。
- sponsor 余额低于 3 天预算告警。
- 7 天内到期且未续期 Blob 数超过阈值告警。
- 校验失败出现任意非零值都告警。
阈值要按业务调,不要照搬。校验失败是高严重度,因为它可能表示数据错配、缓存污染或实现 bug。
升级流程
Walrus 版本要跟网络走:
- Mainnet:
mainnet-v1.47.1。 - Testnet:
testnet-v1.47.1。 - Devnet:
devnet-v1.48.0预发布。
升级步骤:
- 阅读官方 release note,确认是否有配置、合约或数据格式变化。
- 在 testnet 重放核心流程:上传、读取、续期、Move 引用、索引。
- 升级 SDK 和服务端依赖,锁定版本。
- 灰度 publisher/aggregator。
- 观察 24 小时关键指标。
- 再升级生产批量任务和后台索引器。
如果 SDK 升级但服务端仍旧,最容易出现“本地编译通过,协议请求失败”。集成测试必须连真实 testnet endpoint。
Runbook 模板
每个故障类型写一份 runbook:
故障:读取成功率下降
影响:用户下载失败、页面资源加载失败
先看:aggregator 5xx、超时、出口带宽、cache hit ratio
快速动作:切备用 aggregator、降低大文件并发、绕过异常 CDN 节点
确认恢复:读取成功率回到阈值,校验失败为 0
事后:补容量、调超时、更新告警
runbook 要能让值班人员执行,不要只写“检查网络”。
第 25 章 项目一:去中心化文件网盘
本章实现一个最小可生产化的文件网盘:用户在前端选择文件,后端使用 Walrus Publisher 上传内容,把 blobId、文件名、MIME、大小、epoch 等元数据写入应用数据库,读取时通过 Aggregator 下载。它不是把所有状态都塞进 Walrus,而是把大文件交给 Walrus,把可搜索、可分页、可授权的索引留在应用层。
项目目标
- 支持单文件上传、列表、下载和删除索引。
- 上传成功后保存可恢复的
blobId与业务元数据。 - 前端不直接接触服务端钱包、Publisher 凭证或私有 API。
- 明确区分“删除索引”和“让 Walrus Blob 到期”。
- 给出可以直接复制到 Next.js、Express 或 Hono 后端的核心代码。
架构
Browser
| 1. multipart/form-data
v
App API /api/files
| 2. PUT bytes to Publisher
v
Walrus Publisher
| 3. store blob on Walrus
v
Walrus storage network
Browser
| 4. GET /api/files/:id/download
v
App API
| 5. redirect/proxy Aggregator URL
v
Walrus Aggregator
应用数据库只需要保存索引:
create table files (
id text primary key,
owner text not null,
name text not null,
mime text not null,
size_bytes integer not null,
blob_id text not null,
end_epoch integer,
created_at text not null
);
create index files_owner_created_at on files(owner, created_at desc);
目录结构
walrus-drive/
app/
page.tsx
api/
files/route.ts
files/[id]/download/route.ts
lib/
auth.ts
db.ts
walrus.ts
.env.local
package.json
环境变量
WALRUS_PUBLISHER_URL=https://publisher.walrus-testnet.walrus.space
WALRUS_AGGREGATOR_URL=https://aggregator.walrus-testnet.walrus.space
WALRUS_EPOCHS=5
MAX_UPLOAD_BYTES=104857600
生产环境建议把 Publisher 放在服务端私网或加访问控制;公开 Publisher 等于允许任何人借你的预算上传。
关键代码
lib/walrus.ts 负责和 Publisher、Aggregator 对接:
export type StoredBlob = {
blobId: string;
endEpoch?: number;
};
const publisherUrl = process.env.WALRUS_PUBLISHER_URL!;
const aggregatorUrl = process.env.WALRUS_AGGREGATOR_URL!;
export async function storeBlob(bytes: Uint8Array): Promise<StoredBlob> {
const epochs = Number(process.env.WALRUS_EPOCHS ?? "5");
const res = await fetch(`${publisherUrl}/v1/blobs?epochs=${epochs}`, {
method: "PUT",
body: bytes,
});
if (!res.ok) {
throw new Error(`Walrus store failed: ${res.status} ${await res.text()}`);
}
const json = await res.json();
const blobId =
json.newlyCreated?.blobObject?.blobId ??
json.alreadyCertified?.blobId ??
json.blobId;
if (!blobId) {
throw new Error(`Walrus response did not include blobId: ${JSON.stringify(json)}`);
}
return {
blobId,
endEpoch: json.newlyCreated?.blobObject?.storage?.endEpoch,
};
}
export function readBlobUrl(blobId: string): string {
return `${aggregatorUrl}/v1/blobs/${encodeURIComponent(blobId)}`;
}
上传接口只接受认证用户,并在写入数据库前完成大小检查:
import { randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { requireUser } from "@/lib/auth";
import { db } from "@/lib/db";
import { storeBlob } from "@/lib/walrus";
export async function POST(req: NextRequest) {
const user = await requireUser(req);
const form = await req.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "missing file" }, { status: 400 });
}
const maxBytes = Number(process.env.MAX_UPLOAD_BYTES ?? "104857600");
if (file.size > maxBytes) {
return NextResponse.json({ error: "file too large" }, { status: 413 });
}
const bytes = new Uint8Array(await file.arrayBuffer());
const stored = await storeBlob(bytes);
const row = {
id: randomUUID(),
owner: user.id,
name: file.name,
mime: file.type || "application/octet-stream",
sizeBytes: file.size,
blobId: stored.blobId,
endEpoch: stored.endEpoch ?? null,
createdAt: new Date().toISOString(),
};
await db.files.insert(row);
return NextResponse.json(row, { status: 201 });
}
下载接口建议默认重定向到 Aggregator;如果文件需要强鉴权或审计,再改成后端代理流式转发:
import { NextRequest, NextResponse } from "next/server";
import { requireUser } from "@/lib/auth";
import { db } from "@/lib/db";
import { readBlobUrl } from "@/lib/walrus";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const user = await requireUser(req);
const { id } = await params;
const file = await db.files.findById(id);
if (!file || file.owner !== user.id) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
return NextResponse.redirect(readBlobUrl(file.blobId));
}
前端上传组件保持简单:
"use client";
import { useState } from "react";
export function UploadBox() {
const [busy, setBusy] = useState(false);
async function upload(file: File) {
setBusy(true);
const body = new FormData();
body.set("file", file);
const res = await fetch("/api/files", { method: "POST", body });
setBusy(false);
if (!res.ok) throw new Error(await res.text());
location.reload();
}
return (
<input
disabled={busy}
type="file"
onChange={(event) => {
const file = event.currentTarget.files?.[0];
if (file) void upload(file);
}}
/>
);
}
运行命令
npm install
npm run dev
手工验证上传链路:
curl -F "file=@./README.md" http://localhost:3000/api/files
curl -L http://localhost:3000/api/files/<file-id>/download -o restored.md
安全与边界
- 不要把 Publisher 管理密钥、Sui 私钥或付费上传能力放到浏览器。
- 数据库删除只会删除应用索引,不会立刻从 Walrus 网络擦除已经存储的 Blob。
- 私密文件应先在客户端或可信服务端加密,再上传 Walrus;Walrus 提供可用性与可验证存储,不等于默认内容保密。
- 对上传接口做用户级配额、MIME 白名单、大小限制和频率限制。
- 对下载接口做所有权检查;公开分享链接应使用独立的 share token,而不是暴露内部文件 ID。
验收标准
- 上传 1 个小于
MAX_UPLOAD_BYTES的文件后,数据库出现一条含blobId的记录。 - 使用 Aggregator URL 能恢复原始文件,哈希与本地文件一致。
- 未登录用户无法上传或下载。
- 删除文件索引后,列表不再显示该文件,并且文档明确说明 Blob 会按 Walrus 生命周期自然到期。
- Publisher、Aggregator、epoch、上传大小上限都来自环境变量。
第 26 章 项目二:Walrus Sites 内容站
本章把一个静态内容站发布为 Walrus Site。目标不是替代所有传统 CDN,而是把可公开访问、版本化、抗单点故障的内容放到 Walrus,并用 Walrus Sites 的路由配置交付给用户。
项目目标
- 使用静态站点生成器构建内容站。
- 配置 SPA fallback、缓存策略和错误页。
- 通过 Walrus Sites 发布、更新、回滚。
- 用 CI/CD 固化构建与发布命令。
- 给出上线前验收标准。
架构
Markdown / CMS export
v
Static site generator
v
dist/
v
site-builder deploy
v
Walrus + Sui site object
v
Portal / SuiNS / custom domain
内容站适合放这些资产:
- 产品文档、博客、白皮书、开源项目主页。
- 纯前端应用的静态构建产物。
- 需要公开读、低频写、可版本化的站点。
不适合直接放这些内容:
- 必须服务端渲染的页面。
- 带用户私密数据的动态接口。
- 每秒频繁变动的大量小对象;这类数据应先做批处理或放后端 API。
目录结构
walrus-content-site/
content/
posts/
hello-walrus.md
public/
logo.svg
src/
layouts/
pages/
dist/
ws-resources.json
package.json
Walrus Sites 配置
示例 ws-resources.json:
{
"object_id": "0x<site-object-id-written-after-first-deploy>",
"headers": {
"/index.html": {
"cache-control": "public, max-age=300"
},
"/assets/app.3f1a2c.js": {
"cache-control": "public, max-age=31536000, immutable"
},
"/assets/app.a91c04.css": {
"cache-control": "public, max-age=31536000, immutable"
}
},
"routes": {
"/app/*": "/index.html"
},
"metadata": {
"site_name": "walrus-content-site",
"description": "A content site deployed with Walrus Sites"
}
}
如果站点是传统多页内容站,可以移除 routes;如果是 React/Vue/Svelte 单页应用,应保留 fallback 路由,避免刷新深层路由时返回 404。headers 必须写精确资源路径,不支持 /assets/* 这类通配符;真实项目可以在构建后扫描 dist/assets/ 自动生成这些条目。
关键代码
内容站构建脚本只关心输出 dist/:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"deploy:site": "site-builder --context=testnet deploy --epochs 10 ./dist"
},
"dependencies": {},
"devDependencies": {
"astro": "6.2.2"
}
}
基础页面示例:
---
const posts = Object.values(
import.meta.glob("../content/posts/*.md", { eager: true }),
);
---
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Walrus 内容站</title>
</head>
<body>
<main>
<h1>Walrus 内容站</h1>
<ul>
{posts.map((post) => (
<li>
<a href={post.url}>{post.frontmatter.title}</a>
</li>
))}
</ul>
</main>
</body>
</html>
CI 发布流程需要把构建和发布分开。构建产物可重复生成,发布步骤需要 Sui/Walrus 凭证:
name: publish-walrus-site
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Configure Sui key
run: |
mkdir -p ~/.sui/sui_config
printf '%s' "$SUI_CLIENT_YAML" > ~/.sui/sui_config/client.yaml
env:
SUI_CLIENT_YAML: ${{ secrets.SUI_CLIENT_YAML }}
- name: Publish site
run: npm run deploy:site
上线前可以把构建产物做一次静态检查:
npm run build
test -f dist/index.html
find dist -type f -size +50M -print
运行命令
首次发布:
npm install
npm run build
npm run deploy:site
后续更新:
npm run build
npm run deploy:site
本地预览:
npm run preview
运维要点
- 把
site object id写入配置和发布日志;它是回滚、迁移、绑定域名的核心标识。 - 静态资源文件名应带内容哈希,并配置长缓存。
- HTML 用短缓存,避免用户长期停留在旧入口。
- 自定义域名和 SuiNS 绑定应在发布完成后再切流量。
- CI 中的 Sui 配置属于高权限凭证,只放在 secret,不提交进仓库。
验收标准
npm run build生成dist/index.html、404.html和静态资源。site-builder deploy成功输出 site object id;第一次部署后ws-resources.json写入了object_id。- Portal URL 可以打开首页、文章页、404 页和任意前端深层路由。
- 刷新深层路由不会丢失 SPA 页面。
- 浏览器网络面板中 HTML 与静态资源的缓存策略符合配置。
- CI 只在
main分支发布,且凭证全部来自 secret。
第 27 章 项目三:MemWal AI Agent 记忆库
MemWal 是 Mysten Labs 推出的 AI Agent 记忆层,目前仍处于 beta。它把记忆内容加密后存到 Walrus,用 Sui 上的账户和 delegate key 表达所有权与授权,并通过 relayer 处理 embedding、SEAL 加密、Walrus 上传下载和向量搜索。本章实现一个可运行的 TypeScript 记忆库:Agent 可以写入事实、按问题召回记忆,并把记忆能力暴露为 AI SDK tool。
项目目标
- 使用
@mysten-incubation/memwal连接 MemWal relayer。 - 固定最新依赖版本:
@mysten-incubation/memwal@0.0.2、@mysten/seal@1.1.1、@mysten/walrus@1.1.4、@mysten/sui@2.16.0、ai@6.0.175、zod@4.4.3。 - 通过环境变量管理 relayer、account id、delegate key 和 namespace。
- 说明 namespace 隔离、delegate key 轮换和 relayer 信任边界。
- 提供 CLI 与 AI SDK tool 两种接入方式。
架构
Agent runtime
| remember / recall
v
MemWal SDK
| signed request with delegate key
v
MemWal relayer
| embedding + SEAL + Walrus upload/search
v
Sui account + Walrus encrypted blobs + vector index
默认推荐路径是 MemWal 客户端:应用把请求交给 relayer,relayer 负责 embedding、SEAL、Walrus 与索引。只有当你要把 embedding 和 SEAL 全部放到客户端控制时,才使用 MemWalManual。
目录结构
examples/memwal/
package.json
tsconfig.json
.env.example
README.md
src/
config.ts
memory.ts
tools.ts
cli.ts
配置
MEMWAL_PRIVATE_KEY=your-64-char-hex-delegate-private-key
MEMWAL_ACCOUNT_ID=0x...
MEMWAL_SERVER_URL=https://relayer.staging.memwal.ai
MEMWAL_NAMESPACE=lets-walrus-agent-dev
字段含义:
MEMWAL_PRIVATE_KEY:delegate private key,不是用户主钱包私钥。Agent 用它签名请求并访问被授权的记忆空间。MEMWAL_ACCOUNT_ID:Sui 上的MemWalAccount对象 ID,表达记忆账户与所有权。MEMWAL_SERVER_URL:relayer 地址。测试可用 staging relayer,生产可用托管 mainnet relayer或自托管 relayer。MEMWAL_NAMESPACE:记忆命名空间。MemWal 按owner + namespace隔离搜索和存储,建议每个产品、租户或 Agent 使用独立 namespace。
关键代码
配置读取使用 Zod 做强校验:
import { z } from "zod";
const ConfigSchema = z.object({
MEMWAL_PRIVATE_KEY: z.string().regex(/^[0-9a-fA-F]{64}$/),
MEMWAL_ACCOUNT_ID: z.string().startsWith("0x"),
MEMWAL_SERVER_URL: z.string().url().default("https://relayer.staging.memwal.ai"),
MEMWAL_NAMESPACE: z.string().min(1).default("lets-walrus-agent-dev"),
});
export const config = ConfigSchema.parse(process.env);
封装 MemWal 客户端:
import { MemWal } from "@mysten-incubation/memwal";
import { config } from "./config.js";
export const memory = MemWal.create({
key: config.MEMWAL_PRIVATE_KEY,
accountId: config.MEMWAL_ACCOUNT_ID,
serverUrl: config.MEMWAL_SERVER_URL,
namespace: config.MEMWAL_NAMESPACE,
});
export async function rememberFact(content: string) {
await memory.health();
return memory.remember(content);
}
export async function recallFacts(query: string) {
await memory.health();
const result = await memory.recall(query);
return result.results ?? [];
}
把记忆能力暴露为 AI SDK tools:
import { tool } from "ai";
import { z } from "zod";
import { recallFacts, rememberFact } from "./memory.js";
export const memoryTools = {
memory_store: tool({
description: "Store a durable encrypted memory for future conversations.",
inputSchema: z.object({
content: z.string().min(1),
}),
execute: async ({ content }) => {
await rememberFact(content);
return { ok: true };
},
}),
memory_search: tool({
description: "Search durable encrypted memories by semantic query.",
inputSchema: z.object({
query: z.string().min(1),
}),
execute: async ({ query }) => {
return { results: await recallFacts(query) };
},
}),
};
CLI 用于验收记忆写入和召回:
import { recallFacts, rememberFact } from "./memory.js";
const [command, ...rest] = process.argv.slice(2);
const text = rest.join(" ").trim();
if (!command || !text) {
console.error("Usage: npm run mem -- remember <text>");
console.error(" npm run mem -- recall <query>");
process.exit(1);
}
if (command === "remember") {
await rememberFact(text);
console.log("stored");
} else if (command === "recall") {
const results = await recallFacts(text);
console.log(JSON.stringify(results, null, 2));
} else {
throw new Error(`Unknown command: ${command}`);
}
运行命令
cd examples/memwal
npm install
cp .env.example .env
示例目录包含 .npmrc:
legacy-peer-deps=true
原因是 MemWal 0.0.2 当前仍把 Zod 3 写在 optional peer 依赖声明中,而本书按最新版本事实固定 zod@4.4.3,AI SDK 6.0.175 也支持 Zod 4。这个设置只放在示例目录,避免影响整本书其它项目。
填入 .env 后运行:
npm run mem -- remember "用户偏好:回答要直接、中文、带可复制命令。"
npm run mem -- recall "这个用户喜欢什么回答风格?"
在 Agent 中接入:
import { generateText } from "ai";
import { memoryTools } from "./tools.js";
const result = await generateText({
model,
tools: memoryTools,
prompt: "记住:我正在学习 Walrus。以后回答要结合去中心化存储。",
});
上面的 model 由你的模型 provider 提供,例如 OpenAI-compatible、Anthropic 或本地模型适配器。MemWal 不绑定具体模型供应商。
Relayer、Namespace 与 Delegate Key
relayer 是 MemWal 的关键运行边界。托管 relayer 可以最快接入;自托管 relayer适合需要控制 embedding provider、SEAL 操作、可用性和日志边界的团队。自托管时通常需要 PostgreSQL + pgvector、Redis、Sui server key、MemWal package/registry id,以及 SEAL key server 配置。
namespace 是记忆隔离单元。推荐命名:
<app>:<environment>:<tenant-or-agent>
例如:
lets-walrus:dev:writer
lets-walrus:prod:support-agent
lets-walrus:prod:tenant-42
delegate key 是给 Agent 的授权钥匙,不应等同于用户主钱包私钥。生产系统至少要做到:
- 每个环境使用不同 delegate key。
- 每个高风险 Agent 使用独立 namespace 和独立 delegate key。
- 支持撤销和轮换 delegate key。
- 不在浏览器、本地日志、CI 输出或错误上报中泄露私钥。
安全边界
- MemWal 当前是 beta,托管 relayer 不应被视为有强 SLA 的生产基础设施。
- 使用托管 relayer 时,团队需要接受 relayer 参与 embedding、加密/解密编排、Walrus 上传下载和向量搜索的信任边界。
- 需要更强控制时,自托管 relayer,并把数据库、Redis、server wallet、OpenAI-compatible embedding key 纳入自己的运维边界。
- 记忆召回结果会进入 LLM 上下文,应把它当作外部输入处理,避免把召回内容直接当系统指令执行。
- namespace 不是权限系统的全部;它是隔离维度,仍然要配合 delegate key、账户所有权和应用层授权。
验收标准
npm install后依赖版本与本章指定版本一致。.env填入 staging 或生产凭证后,npm run mem -- remember ...返回stored。- 使用同一
MEMWAL_NAMESPACE可以召回刚写入的事实。 - 更换
MEMWAL_NAMESPACE后默认召回不到另一个 namespace 的记忆。 - 日志不输出完整 delegate key。
- README 明确标注 MemWal beta、relayer 信任边界和 delegate key 安全要求。
第 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。
附录 A 版本与资料来源
本书按“最新稳定版本优先”的策略编写。以下信息在 2026-05-06 通过官方文档、GitHub release 页面和 npm registry 核对。
核心版本
| 组件 | 最新稳定版本 | 核对方式 |
|---|---|---|
| Walrus Mainnet CLI / 服务端 | mainnet-v1.47.1 | GitHub Releases |
| Walrus Testnet CLI / 服务端 | testnet-v1.47.1 | GitHub Releases |
| Walrus Devnet | devnet-v1.48.0,预发布 | GitHub Releases |
| mdBook | 0.5.2 | 本机 mdbook --version |
@mysten/walrus | 1.1.4 | npm view @mysten/walrus version |
@mysten/walrus-wasm | 0.2.0 | npm view @mysten/walrus-wasm version |
@mysten/sui | 2.16.0 | npm view @mysten/sui version |
@mysten/bcs | 2.0.3 | npm view @mysten/bcs version |
@mysten/seal | 1.1.1 | npm view @mysten/seal version |
@mysten-incubation/memwal | 0.0.2 | npm view @mysten-incubation/memwal version |
@mysten/sui-stack-messaging | 0.0.2 | npm view @mysten/sui-stack-messaging version |
@mysten/sui-groups | 0.0.1 | npm view @mysten/sui-groups version |
ai | 6.0.175 | npm view ai version |
zod | 4.4.3 | npm view zod version |
tsx | 4.21.0 | npm view tsx version |
typescript | 6.0.3 | npm view typescript version |
@types/node | 25.6.0 | npm view @types/node version |
astro | 6.2.2 | npm view astro version |
主要资料来源
- Walrus 官方仓库:https://github.com/MystenLabs/walrus
- Walrus 官方文档:https://docs.wal.app/
- Walrus Sites 仓库:https://github.com/MystenLabs/walrus-sites
- Walrus TypeScript SDK 文档:https://sdk.mystenlabs.com/walrus
- MemWal 仓库:https://github.com/MystenLabs/MemWal
- MemWal 文档:https://docs.memwal.ai/
- Sui Stack Messaging 仓库:https://github.com/MystenLabs/sui-stack-messaging
更新规则
如果你在阅读时距离上方核对日期已经过去较久,先运行:
walrus --version
sui --version
npm view @mysten/walrus version
npm view @mysten/sui version
npm view @mysten/bcs version
npm view @mysten/seal version
npm view @mysten-incubation/memwal version
npm view @mysten/sui-stack-messaging version
npm view @mysten/sui-groups version
如果 npm 或 CLI 输出高于本书记录版本,优先参考最新官方文档,并把示例中的依赖版本更新到最新稳定版。
附录 B 常用命令速查
本附录默认 Testnet。生产环境把 --context testnet 改成 --context mainnet 前,先确认钱包、余额和版本。
版本与安装
curl -sSfL https://raw.githubusercontent.com/Mystenlabs/suiup/main/install.sh | sh
suiup install sui
suiup install walrus
suiup show
suiup which
sui --version
walrus --version
配置
curl --create-dirs \
https://docs.wal.app/setup/client_config.yaml \
-o ~/.config/walrus/client_config.yaml
sui client
sui client envs
sui client switch --env testnet
sui client active-env
walrus info --context testnet
账户与余额
sui client active-address
sui client addresses
sui client new-address ed25519
sui client balance
walrus get-wal --context testnet
Store
walrus store file.txt --epochs 2 --context testnet
walrus store *.png --epochs 2 --context testnet
walrus store file.txt --epochs max --context testnet
walrus store file.txt --epochs 2 --deletable --context testnet
walrus store file.txt --epochs 2 --permanent --context testnet
walrus store file.txt --epochs 2 --force --context testnet
walrus store file.txt --epochs 2 --permanent --share --context testnet
Read 与 Status
walrus blob-status --blob-id <BLOB_ID> --context testnet
walrus blob-status --file file.txt --context testnet
walrus read <BLOB_ID> --context testnet
walrus read <BLOB_ID> --out downloaded.bin --context testnet
walrus read <BLOB_ID> --out downloaded.bin --strict-consistency-check --context testnet
生命周期管理
walrus extend --blob-obj-id <BLOB_OBJECT_ID> --epochs-extended 3 --context testnet
walrus extend --blob-obj-id <SHARED_BLOB_OBJECT_ID> --epochs-extended 3 --shared --context testnet
walrus delete --blob-id <BLOB_ID> --context testnet
walrus delete --blob-id <BLOB_ID> --yes --context testnet
walrus delete --file file.txt --context testnet
walrus delete --object-id <BLOB_OBJECT_ID> --context testnet
walrus burn-blobs --object-ids <BLOB_OBJECT_ID> --context testnet
walrus burn-blobs --all-expired --context testnet
Shared Blob
walrus share --blob-obj-id <BLOB_OBJECT_ID> --context testnet
walrus share --blob-obj-id <BLOB_OBJECT_ID> --amount <AMOUNT> --context testnet
walrus fund-shared-blob --blob-obj-id <SHARED_BLOB_OBJECT_ID> --amount <AMOUNT> --context testnet
Attributes
walrus set-blob-attribute <BLOB_OBJECT_ID> \
--attr "content-type" "text/plain" \
--attr "cache-control" "public, max-age=31536000" \
--context testnet
walrus get-blob-attribute <BLOB_OBJECT_ID> --context testnet
walrus remove-blob-attribute-fields <BLOB_OBJECT_ID> \
--keys "cache-control" \
--context testnet
walrus remove-blob-attribute <BLOB_OBJECT_ID> --context testnet
Quilt
walrus store-quilt \
--epochs 2 \
--paths ./site-assets ./metadata.json \
--context testnet
walrus store-quilt \
--blobs '{"path":"a.txt","identifier":"a","tags":{"kind":"text"}}' \
'{"path":"b.json","identifier":"b","tags":{"kind":"json"}}' \
--epochs 2 \
--context testnet
walrus list-patches-in-quilt <QUILT_ID> --context testnet
walrus read-quilt \
--quilt-id <QUILT_ID> \
--identifiers a b \
--out ./downloads \
--context testnet
walrus read-quilt \
--quilt-id <QUILT_ID> \
--tag kind json \
--out ./downloads \
--context testnet
walrus read-quilt \
--quilt-patch-ids <PATCH_ID_1> <PATCH_ID_2> \
--out ./downloads \
--context testnet
排错入口
walrus --help
walrus store --help
walrus read --help
walrus extend --help
walrus store-quilt --help
sui client --help
suiup doctor
附录 C 配置模板
本附录收录书中项目会反复用到的配置模板。复制前请按目标网络、托管方式和权限边界替换占位符,不要把私钥、助记词、keystore 或 relayer 密钥提交到仓库。
Walrus CLI 检查模板
sui client active-env
sui client active-address
walrus --version
walrus info --context testnet
如果你同时使用 Testnet 和 Mainnet,命令中显式传入 --context:
walrus store ./data/hello.txt --epochs 2 --context testnet
walrus read "$BLOB_ID" --out ./downloads/hello.txt --context testnet
Walrus Sites:ws-resources.json
ws-resources.json 放在要部署的静态目录根部。第一次 site-builder deploy 成功后,工具会把 object_id 写回这个文件,后续再运行同一条 deploy 命令就会更新已有站点。
{
"object_id": "0xYOUR_SITE_OBJECT_ID_AFTER_FIRST_DEPLOY",
"headers": {
"/index.html": {
"cache-control": "no-cache",
"content-type": "text/html; charset=utf-8"
},
"/assets/app.3f1a2c.js": {
"cache-control": "public, max-age=31536000, immutable"
},
"/assets/app.a91c04.css": {
"cache-control": "public, max-age=31536000, immutable"
}
},
"routes": {
"/app/*": "/index.html"
},
"redirects": {
"/old": {
"location": "/",
"status_code": 301
}
},
"metadata": {
"site_name": "Your Walrus Site",
"description": "Static site served through Walrus Sites",
"link": "https://YOUR_SUINS_NAME.wal.app"
},
"ignore": [
".DS_Store",
"*.map"
]
}
部署命令:
site-builder --context=testnet deploy --epochs 5 ./dist
site-builder --context=testnet sitemap --id 0xYOUR_SITE_OBJECT_ID
site-builder convert 0xYOUR_SITE_OBJECT_ID
项目:去中心化文件网盘
WALRUS_PUBLISHER_URL=https://publisher.walrus-testnet.walrus.space
WALRUS_AGGREGATOR_URL=https://aggregator.walrus-testnet.walrus.space
WALRUS_EPOCHS=5
MAX_UPLOAD_BYTES=104857600
项目:MemWal Agent 记忆库
MEMWAL_PRIVATE_KEY=your-64-char-hex-delegate-private-key
MEMWAL_ACCOUNT_ID=0x...
MEMWAL_SERVER_URL=https://relayer.staging.memwal.ai
MEMWAL_NAMESPACE=lets-walrus-agent-dev
生产 mainnet 托管 relayer:
MEMWAL_SERVER_URL=https://relayer.memwal.ai
MEMWAL_NAMESPACE=lets-walrus-agent-prod
自托管 MemWal relayer 常用环境变量:
SUI_NETWORK=testnet
PORT=8000
SIDECAR_URL=http://localhost:9000
DATABASE_URL=postgres://memwal:memwal@localhost:5432/memwal
REDIS_URL=redis://localhost:6379
OPENAI_API_KEY=sk-...
OPENAI_API_BASE=https://api.openai.com/v1
MEMWAL_PACKAGE_ID=0xcf6ad755a1cdff7217865c796778fabe5aa399cb0cf2eba986f4b582047229c6
MEMWAL_REGISTRY_ID=0xe80f2feec1c139616a86c9f71210152e2a7ca552b20841f2e192f99f75864437
SERVER_SUI_PRIVATE_KEYS=suiprivkey1...,suiprivkey2...
SEAL_KEY_SERVERS=0x...
RATE_LIMIT_REQUESTS_PER_MINUTE=60
RATE_LIMIT_REQUESTS_PER_HOUR=500
RATE_LIMIT_DELEGATE_KEY_PER_MINUTE=30
RATE_LIMIT_STORAGE_BYTES=1073741824
配置边界:
MEMWAL_PRIVATE_KEY是 delegate key,只给 Agent 使用,不等同于用户主钱包私钥。- namespace 按
owner + namespace参与隔离,建议按应用、环境、租户或 Agent 拆分。 - 托管 relayer 适合快速接入;自托管 relayer 适合需要控制 embedding、SEAL、Walrus 上传下载、日志和 SLA 的生产系统。
- MemWal 当前处于 beta,生产使用前应先完成 delegate key 轮换、限流、审计日志和故障降级设计。
项目:Sui Stack Messaging
SUI_GRPC_URL=https://fullnode.testnet.sui.io:443
MESSAGING_RELAYER_URL=https://messaging-relayer.example.com
SEAL_KEY_SERVER_1=0x...
SEAL_KEY_SERVER_2=0x...
WALRUS_PUBLISHER_URL=https://publisher.walrus-testnet.walrus.space
WALRUS_AGGREGATOR_URL=https://aggregator.walrus-testnet.walrus.space
WALRUS_ATTACHMENT_EPOCHS=5
自托管 relayer 常用变量:
SUI_RPC_URL=https://fullnode.testnet.sui.io:443
GROUPS_PACKAGE_ID=0x...
PORT=3000
REQUEST_TTL_SECONDS=300
WALRUS_STORAGE_EPOCHS=5
WALRUS_SYNC_INTERVAL_SECS=3600
WALRUS_SYNC_BATCH_SIZE=100
WALRUS_SYNC_MESSAGE_THRESHOLD=50
Sui Stack Messaging 当前处于 beta。生产环境应自托管 relayer 或把 relayer 逻辑接入已有后端,并为 relayer storage、Walrus discovery indexer、Seal key server 和 key rotation 设计运维策略。
附录 D 常见错误排查
网络和版本
| 现象 | 常见原因 | 处理 |
|---|---|---|
blob_id 在 aggregator 上 404 | 网络混用、Blob 过期、ID 截断 | 确认 network,查链上对象和 end_epoch |
| CLI 报协议或序列化错误 | CLI 与目标网络版本不匹配 | Mainnet 用 mainnet-v1.47.1,Testnet 用 testnet-v1.47.1 |
| devnet 示例在 mainnet 失败 | 使用了预发布配置 | Devnet devnet-v1.48.0 不要迁移到稳定网络 |
| Sui 查不到对象 | RPC 落后或 active env 错误 | sui client active-env,换 RPC 后重查 |
上传
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 上传超时 | publisher 忙、文件过大、网络慢 | 查 upload job 和 tx digest,避免盲目重传 |
| 余额不足 | sponsor 或用户钱包缺 WAL/SUI | 补余额,降低保存期或文件大小 |
| 小文件成本偏高 | 每个文件单独上传 | 用 Quilt 或应用层打包 |
| 上传成功但列表不显示 | 索引器落后 | 用 tx digest 查链,检查 checkpoint lag |
读取
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 偶发 5xx | aggregator 或上游节点波动 | 重试、切备用 aggregator |
| 下载内容和预期不符 | 业务对象引用已更新、缓存旧内容 | 校验业务哈希,URL 带版本或内容 ID |
| 大文件下载中断 | 超时、代理限制、出口带宽 | 使用 Range、流式代理、调整超时 |
| 前端 CORS 报错 | aggregator 或网关未配置跨域 | 通过应用后端代理或配置允许来源 |
Move 和索引
| 现象 | 常见原因 | 处理 |
|---|---|---|
| Move 发布失败 | Move.toml 包地址占位未替换 | 从当前网络官方配置替换 Walrus package ID |
交易 abort E_NOT_OWNER | 调用者不是业务对象 owner | 用正确钱包签名或走授权入口 |
| 事件重复入库 | 重试或重放 checkpoint | 用 tx_digest + event_seq 幂等去重 |
| 旧 Blob 覆盖新 Blob | 没比较对象版本 | 落库前检查 object_version |
隐私和安全
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 私密文件被公开访问 | 明文上传到公开 Blob | 上传前加密,使用 Seal 或密钥服务控制解密 |
| 日志泄露敏感信息 | 打印文件名、blob_id、明文哈希 | 脱敏日志,限制日志访问 |
| API 费用异常 | publisher/aggregator 无限制 | 用户鉴权、限流、预算上限、异常流量封禁 |
最短排错命令
walrus --version
walrus info
sui --version
sui client active-env
sui client object <BLOB_OBJECT_ID>
sui client tx-block <TX_DIGEST>
如果命令输出的网络、版本、对象 ID 不属于同一环境,先修正环境,不要继续排应用代码。
附录 E 术语表
Aggregator
读取入口服务。它根据 blob_id 从 Walrus 存储网络聚合足够片段,重构并返回原始字节。可以公共使用,也可以自建。
Blob
Walrus 保存的一段二进制数据。它可以是图片、网页资源、JSON、模型文件、加密后的私密数据等。
blob_id
Walrus 内容标识,用于读取 Blob。它不是访问权限;敏感内容不能依赖隐藏 blob_id 保护。
Blob object
Sui 上记录 Walrus Blob 状态的对象。业务合约通常引用它的对象 ID,而不是把大文件写进 Move。
Checkpoint
Sui 链上数据的稳定处理边界。索引器通常按 checkpoint 顺序处理事件和对象变更。
Devnet
开发网络。当前事实中 devnet-v1.48.0 是预发布,适合试验新功能,不适合生产。
Epoch
协议按时间推进的周期。Walrus 的存储期、委员会和部分费用逻辑与 epoch 相关。
Erasure coding
纠删码。把原始数据编码成多个片段,只要取回足够多片段即可恢复数据。
FROST
Walrus 费用相关的最小计价单位。实际费用和换算以当前网络 walrus info 输出为准。
Index
业务系统为 Walrus 数据建立的检索层。它通常保存业务对象 ID、blob_id、Blob object ID、大小、哈希、过期时间和状态。
Mainnet
主网。当前稳定版本事实为 mainnet-v1.47.1。
MemWal
Mysten Labs incubation 项目,用 Walrus、Seal 和 relayer 为 AI Agent 提供长期记忆、加密存储和语义检索能力。当前仍应按 beta 项目对待。
Messaging relayer
Sui Stack Messaging 的离线投递服务。它验证钱包签名和群组权限,存储密文消息,提供实时拉取/订阅,并可把消息批量归档到 Walrus。
Move
Sui 使用的智能合约语言。Walrus 集成中,Move 主要保存 Blob 引用、权限和事件。
Portal
Walrus Sites 的 HTTP 访问入口。portal 根据 SuiNS 名称或 Base36 对象 ID 找到站点对象,再从 Walrus 读取资源并返回给浏览器。
Publisher
写入入口服务。它接收上传请求,协调数据写入和链上注册。生产环境要做鉴权、限流和预算控制。
Quilt
Walrus 面向大量小文件的打包能力。用它可以降低小文件逐个上传带来的固定开销。
QuiltPatchID
Quilt 内单个文件的读取标识。它不同于普通 Blob ID,适合从一个 Quilt 中读取指定文件。
Red Stuff
Walrus 围绕数据可用性、编码片段和存储节点确认的协议设计。开发者通常通过 SDK、CLI 和服务端组件间接使用它。
Seal
Mysten 生态中的加密和访问控制工具。隐私数据上传 Walrus 前可以先加密,再用 Seal 控制解密权限。
Storage node
Walrus 存储节点,负责保存编码片段并响应协议请求。运行 storage node 需要稳定硬件、网络、密钥和升级流程。
SuiNS
Sui Name Service。Walrus Sites 可用 SuiNS 名称映射到站点对象,并通过 wal.app 形成可读访问地址。
Sui Stack Messaging
Mysten Labs 的端到端加密消息工具。它组合 Sui Groups、Seal、relayer 和 Walrus,适合构建 dApp 内私密群聊、成员制社区、游戏公会聊天和 Agent 加密通信。
Sui object ID
Sui 对象标识。不要和 blob_id 混用。对象 ID 用于链上状态和权限,blob_id 用于 Walrus 读取。
Testnet
测试网络。当前稳定版本事实为 testnet-v1.47.1。
WAL
Walrus 网络相关 token。具体费用和用途以当前网络官方配置为准。
Walrus Sites
把静态站点资源发布到 Walrus 的工具和工作流。适合托管前端页面、文档站和静态资源。
Upload relay
帮助浏览器或低带宽客户端完成写入的服务。relay 负责处理大量 storage node 请求,可能要求 tip,也可能限流或不可用。
ws-resources.json
Walrus Sites 部署配置文件。它记录站点 object_id,并声明路由、headers、redirects、metadata 和 ignore 规则。