ZooBC 维基
数据与存储

去中心化存储池

去中心化存储保存那些对一笔交易来说过大的文件。链上只记录每个文件的清单和押金;分片存放在已注册节点上,每个分片五份副本,其位置由一次每个节点都能重复的抽签决定。文件的租金支付给那些每个纪元都证明自己仍保有这些字节的持有者。

分片、根与清单

存储文件被切成 1 MiB(1,048,576 字节)的分片。每个分片以其 SHA3-256 哈希命名,而文件的标识,即它的根,是对分片大小、总大小和分片 id 列表计算的哈希:

piece id = SHA3-256(piece)
root     = SHA3-256(piece size (u32) ‖ total size (u64) ‖ piece ids)

分片被完整复制;ZooBC 不使用纠删码。一笔 StoreFile 交易(类型 40)把清单锚定到链上:根、总大小、分片大小、至少 0.01 ZBC 的押金、1 到 100,000 个分片 id,以及一个可选的访问区段。分片本身永远不会上链。针对已处于活跃状态的根的 StoreFile 是付费空操作;已被修剪的根可以再次存储。

上传时,钱包把文件发送到节点的 POST /api/v1/storage/upload,它返回 StoreFile 所需的根、大小和分片 id。在以下情况下节点会拒绝:存储功能关闭(503);请求体为空(400);可用空间不足 2 GiB,或尚未锚定的分片超过 256 MiB(507);某个客户端每 600 秒超过 10 次上传或每小时超过 32 MiB(429)。

文件被切成 1 MiB 的分片;清单上链,每个分片发往五个节点。
文件被切成 1 MiB 的分片;清单上链,每个分片发往五个节点。

谁持有分片

分片的放置使用会合哈希(rendezvous hashing)。在 mainnet 上,时间被划分为每 240 个区块(约一小时)一个的存储纪元。对于某个纪元中的某个分片,每个活跃的已注册节点都会得到一个权重:

weight = SHA3-256(piece id ‖ node public key ‖ epoch seed)

纪元种子是开启该纪元的区块的哈希,该区块位于高度 epoch × 240。权重最高的五个节点就是预期持有者,五这个数来自链参数 storage_replication;节点少于五个时,所有节点都持有该分片。公式中的一切都是链上数据,因此每个节点无需任何公告就能算出同样的五个节点。向持有者付酬的清点读取的是前一区块时的注册表,因此不会有两个节点对这组节点产生分歧。

持有者随纪元而变,交接过程是重叠的。每个纪元,节点获取分配给自己的分片:先从当前持有者处获取,再从先前的持有者处,然后从任意节点处。只有当哈希相符、且它在本纪元或上一纪元是预期持有者时,它才接受别人推送来的分片。只有当它在上一纪元也不是持有者、且该文件已存在至少一小时时,它才删除未分配给自己的分片。

存储默认开启:与持有归档的分块一样,持有分片是已注册节点席位的一项职责。运营者可以在 node.conf 中关闭它([storage] enabled = false),此时只是失去存储奖励。这个开关是链看不到的本地设置,因此抽签不会参考它:关闭了存储的节点和其他节点一样会被抽中,但它从不提交证明,在该分片上一无所得,也不损失分数,而该分片就少了一个可用的副本。无论 node.conf 怎么写,副本数和纪元长度始终取自链的参数。

存储证明

链从未见过分片,因此无法自行检查持有者。取而代之的是,每个纪元,每个持有者都要回答一个只有持有这些字节的人才能算出的挑战值:

challenge = SHA3-256(epoch seed ‖ piece id ‖ epoch (u64 LE))
proof     = SHA3-256(piece bytes ‖ challenge)

在纪元开始之前,没有人能知道挑战值。持有者通过 SubmitStorageProof 交易(类型 41)发送答案:这是用已注册节点的密钥签名的免手续费职责交易,一笔交易就携带一个节点在该纪元的整批答案。

在每个纪元边界,链会比较这些答案。由最多不同节点提交的值被视为真值,但前提是没有其他值与之票数相同,而且只有当至少三个持有者一致时才算数(oracle_quorum)。随后,每个答案一致的持有者的所有者都会从存储池获得报酬:

reward = the active files' rent for one epoch ÷ the expected proofs (pieces × 5)
paid   = min(reward, storage pool balance)

被多数否决的持有者在该纪元得不到任何报酬,但也不会损失参与分数:由于链从不持有这些字节,它能判断的只有持有者之间是否一致。什么都不发送的持有者会被记录在一张审计表中。尚未成熟的节点得不到任何报酬,它的存储奖励留在池中(成熟与收益)。

持有者每个纪元回答一个新的挑战值;当至少三个一致且没有答案票数相同时,每个答案一致的持有者都会获得报酬。
持有者每个纪元回答一个新的挑战值;当至少三个一致且没有答案票数相同时,每个答案一致的持有者都会获得报酬。

租金

每个周期一次(mainnet 上为 172,800 个区块,约 30 天),链把一个周期的租金从文件的押金转入存储池(ZBCSTORE):

rent = max(1, ceil(size ÷ 1,048,576)) × price per MiB

价格与数据集相同,起始为每 MiB 0.05 ZBC。租金按整 MiB 计算,至少一个:42 字节的文件每个周期支付 0.05 ZBC,而 1,048,577 字节、比 1 MiB 多一个字节的文件支付 0.1 ZBC。节点的 GET /api/v1/storage/quote?bytes=&blocks= 用同一公式为押金报价。

任何账户都可以用 FundStoredFile(类型 296)向活跃文件的押金追加资金,每次至少 0.01 ZBC。当押金不足以支付一个完整周期时,文件会被修剪:其记录被标记为未存储,余额进入长效池。

私密文件

StoreFile 的访问区段可以把文件标记为私密(标志位 0),并添加一份包含 1 到 16 个地址的允许列表(标志位 1)。此后,节点的 API 会拒绝对内容的未签名请求(401),也会拒绝除所有者或列表中地址以外的任何人发出的已签名请求(403)。私密文件不会出现在公开列表中;与某个活跃私密文件共享分片的公开文件,也会受到同样的保护。

“私密”是一种访问模式,而不是加密。分片仍以未加密的形式复制给其持有者,节点之间也会互相提供分片,因此持有分片的节点的运营者可以读取其磁盘上的字节。如果内容重要,请在上传前加密:这样由你的密钥保护内容,而私密标记增加第二层较轻的保护。

另见