第 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 存储期和官方合约规则控制。