贡献者点下提交,三秒后拿到一个永久地址。日后正是凭这个地址,他才能证明这份贡献出自他手、据此主张所有权,并在数据产生收益时拿到所有权应得的那一份。而此刻,地址指向的文件还没有到 Arweave;它却已经写进了链上交易,再也改不了。
这个地址指向的是一张收据:谁做的、做了什么、什么时候、凭什么可信。收据是一份 metadata 文档,存在链下;链上只留这个地址和一份哈希,用来证明这份收据此后没有被改过。
XnY 把数据的来源、组装关系和收益分配锚定在链上¹,但完整的 metadata 文档不进合约。链上只存两样东西:
bytes32 metadataHash; // keccak256(metadata JSON 原始字节)
string metadataUri; // "arweave://<dataItemId>"
arweave://<id> 是不绑定具体 gateway 的地址,读取时映射为某个 Arweave gateway 的 HTTP URL,例如 https://arweave.net/<id>。任何人以后都能按它取回文档、重算 hash、和链上比对,不需要信任任何一个 Arweave gateway。
五类链下 metadata 都用这一对字段锚定,紧凑序列化后实测 0.5–1.2 KB²:
| kind | 装什么 |
|---|---|
frontier-metadata | 一个数据领域的定义:名称、默认策略、提交规则 |
campaign-metadata | 一次征集活动:报酬模型、预算、资格门槛、分成 |
task-metadata | 一个具体任务:类型、执行方式、验收规则、依赖 |
cf-metadata | 一次贡献:贡献者 DID、所属任务、载荷位置、字节数 |
dataset-metadata | 一个数据集版本:CF 清单、贡献者 Merkle root、加密套件 |
五种共享同一个 envelope。
URI 必须在创建链上对象的交易里写进 calldata。交易提交后不能再补,也不能换。写错了没有第二次机会:链上那条记录会永远指向一个取不到的地址,这份贡献的来源就无法验证,贡献者对它的收益主张也就证明不了。
Arweave 这边却是异步的:DataItem 交给 Turbo(接收已签名 DataItem、打包后提交到 Arweave 的上传服务),再等网络确认,要几分钟。而且 ANS-104 DataItem 的 ID 由签名算出³,RSA-PSS 签名带随机 salt⁴,同一份内容重新签一次就是另一个 ID。
(下文「网关服务」指 XnY 自己这个服务,「Arweave gateway」指 Arweave 网络的公共读取入口,两者不是一回事。)
网关服务在请求内做的事,于是只剩创建身份:本地构造并签名 DataItem,算出 ID,把已签名的完整字节写进 PostgreSQL 和独立的 BackupStore(对象存储),返回 URI。上传和确认交给后台。
URI 能提前定下来,靠的是把身份和交付拆开。下面先说这个身份有多硬——201 证明了什么、ID 为什么由签名决定;再说交付怎么保证送到——队列和失败路径;最后说为什么这份文档不进合约。
201 证明了什么
| 状态 | 已经成立 | 尚未成立 |
|---|---|---|
accepted | 身份已创建,URI 已固定,已签名 DataItem 已写入 PostgreSQL 和独立的 BackupStore | API 没有等待 Turbo 接收 |
uploaded | Turbo 已接收,进入打包流程 | 还没观察到 Arweave 网络确认 |
confirmed | 已观察到 DataItem 或其所在 bundle 进入 Arweave 区块,按当前确认策略视为已落链 | 公共 Arweave gateway 可能还要几小时才能取到 |
201 返回时状态是 accepted。它证明一个有稳定身份、由网关服务负责继续交付的资源已经创建,不证明内容已经永久保存。对外的说法要跟着分:在 uploaded 时告诉用户「已永久保存」,是替 Turbo 承诺一件它还没做完的事。调用方用 GET /v1/arweave/items/{dataItemId} 查当前状态。
confirmed 之后,写进去的内容撤不掉。schema 里每个自由文本字段的注释都带同一句警告²:
Permanently written to Arweave — do not put personal data in it.
签名决定 ID,所以要保存签名结果
dataItemId 是对签名值做 SHA-256 再 base64url 编码³。输入是签名那一小段字节,不是整个 DataItem。签名在网关服务本地完成,ID 也就在本地算出,全程不用碰 Arweave 网络。这就是 201 能当场返回终态 URI 的原因。
代价是网关服务只能签一次。RSA-PSS 的 salt 是随机的,重签就换 ID,而链上可能已经锚定了旧的那个。所以返回 201 之前持久化的是完整的已签名 DataItem,不只是 metadata JSON。丢了它,重新签名得不到同一个 ID。
持久化是双写:先写 BackupStore,再写 PostgreSQL 行,两步都成功才返回 201,任一步失败返回 500。worker 取字节时会校验能否重新推导出同一个 ID,PostgreSQL 那份缺失或损坏就从 BackupStore 恢复。副本在 confirmed 之后也保留,因为 Turbo 的 bundle 解包和索引要滞后几小时,这期间公共 Arweave gateway 取不到。
keccak256 的输入和 DataItem 里的 payload 是同一份收到的原始 JSON 字节,网关服务不重新序列化²。字段顺序、空格、Unicode 转义任何一处变化都会产生另一个 hash。
调用方重发同一个请求也不会产生第二个身份。POST 带 Idempotency-Key 请求头,服务端按调用方和 key 去重,命中就返回第一次的 dataItemId 和 URI。
队列在 PostgreSQL 里
记录 commit 之后,这一行开始走状态机:
accepted → pushing(被 worker claim)→ uploaded → confirmed
任务不在内存 channel 里,进程重启后状态还在表里,worker 起来接着上传。API 提交事务后、写 HTTP 响应之前,向一个 channel 发一个空信号,告诉 worker 库里可能有新任务。worker 不需要等响应完成。信号不携带内容,十次唤醒和一次唤醒没有区别,所以 channel 的 buffer 容量是 1。轮询保留为兜底扫描,负责 wake 丢失时把漏掉的任务扫出来。
多个 worker 各 claim 一件,用 FOR UPDATE SKIP LOCKED:别的 worker 正占着的行直接跳过,不在锁上排队⁵。它只挡住两个 worker 同时拿到同一行。整个系统是 at-least-once,允许重复执行,每次重发的都是同一份已签名 DataItem。
失败路径
下面几条路径的共同点是:交付中断了,但身份没有丢。每一条要回答的都是同一个问题——这份已签名的 DataItem 接下来由谁负责送出去。
failed 是唯一不会自动排空的状态。进程崩溃后,行会卡在 pushing,超时后被回收、退回队列。回收和 worker 的写操作共用一个时间戳守卫:claim 时拿到的时间戳出现在后续每条 UPDATE 的 WHERE 里,被回收过的行匹配不到,过期的 worker 写不进任何数据。
Turbo 收了但确认迟迟不来的情况,由 uploaded 之后的独立确认器处理:先查 Arweave 索引有没有收录证据,再以 Turbo 的 settled 状态兜底,已收录但未 mined 的给一段宽限期。超过 deadline 仍没有可验证的收录证据,才进入有次数上限的重传,次数用完转 failed。
4xx 里只有 402 和 429 留在重试路径上。402 是钱包免费额度耗尽,充值买入 Turbo Credits 后转入付费路径;429 是限流。这两种下一次尝试可能成功,其他 4xx 原样重试也不会变,直接转 failed,不消耗重试预算。
failed 是状态机里唯一不会自己排空的状态。原因可恢复的(预算耗尽、回执落库失败),用管理工具退回 accepted 重新交付;放弃的关闭为 retired。
免费额度只减不增,402 迟早出现。网关服务独立轮询钱包剩余额度并输出为监控指标,好提前发现。
为什么不直接写进合约
1 KB 在 Base 上约 64 万 gas⁶,按 2026-08-28 的 gas 价(0.006 gwei)和 ETH 价($2,497)折算约 1 美分。拦住这个方案的不是成本。
event log 和 calldata 比 storage 便宜得多,但属于历史数据。普通节点可以裁剪历史区块和 receipts(以太坊正沿 EIP-4444 推进⁷⁸),以后要读它们得靠还保留完整历史的节点。只有 storage 能按当前状态读取,而它按字节线性计费。读这份 metadata 的下游是 HuggingFace、训练框架和 agent⁹,它们通过 HTTP 取文件,不会调合约再 ABI-decode。
storage 还会锁死字段。五种 metadata 各自按 major.minor 独立加字段、升版本²,合约的 storage layout 上线后冻结成 append-only,改一次要走完整的 UUPS 升级流程¹⁰。
链上那对字段是定长的,bytes32 加一个 53 字节的 URI,约 8 万 gas,跟文档多大无关。锚定成本是常量,多条锚定才能合并进同一笔交易。变长内容进合约就做不到这一点。
Arweave 这一侧¹¹的条件恰好匹配:metadata 是 KB 级小文件,Turbo 对这个尺寸的 DataItem 目前免费¹²;Arweave 一次写入、长期持久¹³,并能通过 gateway 用 HTTP 取到¹⁴。免费上传是 Turbo 当前的策略,不是存储模型的一部分。
URI 能提前返回,是因为系统先保存了那个无法确定性重建的身份,上传本身并没有变快。后面的队列、重试和确认,都只是在把这份身份送到 Arweave。
参考
- XnY Docs — Architecture. 协议总览:Contribution → Assetification → Usage;去中心化存储为 source-of-truth。docs.xny.ai/docs/concepts/core-systems/architecture
- XnY Docs — Metadata documents. 五种 metadata schema;上传到 metadataUri 的字节与算入 metadataHash 的字节完全相同,中间没有规范化步骤。docs.xny.ai/docs/developer/schemas
- ANS-104: Bundled Data v2.0. §2「DataItem signature and id」:id 是签名的 SHA-256 摘要。github.com/ArweaveTeam/arweave-standards
- RFC 8017, PKCS #1 v2.2. §8.1 / §9.1.1 RSASSA-PSS 编码带随机 salt。rfc-editor.org/rfc/rfc8017
- PostgreSQL Documentation — SELECT.「The Locking Clause」:FOR UPDATE … SKIP LOCKED。postgresql.org/docs/current/sql-select.html
- evm.codes — SSTORE (0x55). 零值槽位首次写入 20,000 gas。evm.codes/#55
- EIP-4444: Bound Historical Data in Execution Clients. 历史区块与 receipts 的裁剪。eips.ethereum.org/EIPS/eip-4444
- Ethereum Foundation Blog — Partial History Expiry. 主网历史裁剪的实际推进(2025-07-08)。blog.ethereum.org/2025/07/08/partial-history-exp
- XnY Docs — Key Concepts. Storage / Compute / Serving 与主流 AI 工具链的集成。docs.xny.ai/docs/concepts/core-systems/key-concepts
- OpenZeppelin Contracts — UUPSUpgradeable. UUPS 代理升级模式(EIP-1822)。docs.openzeppelin.com/contracts/5.x/api/proxy
- XnY Docs — Storage, Compute & Serving. 内容寻址存储为 source-of-truth;云缓存不替代去中心化记录。docs.xny.ai/docs/concepts/core-systems/storage-compute-serving
- ar.io Docs — Upload to Arweave (Turbo).「Free uploads under 105 KiB」。docs.ar.io/build/upload
- Arweave Yellow Paper. 一次付费、长期持久的存储捐赠模型。arweave.org/yellow-paper.pdf
- ar.io Docs — Gateways. 永久性来自 Arweave 网络;gateway 负责 HTTP 访问、索引与缓存。docs.ar.io/learn/gateways
链接访问日期:2026-09-02。