ZooBC wiki
Data and storage

The decentralised storage pool

You are looking at an old version of this page: revision #145 by zoobc.how, 2026-10-26 21:37 UTC. See the current version

The decentralised storage keeps files that are too large for a transaction. The chain records only each file's manifest and deposit; the pieces live on registered nodes, five copies of each, placed by a draw that every node can repeat. The files' rent pays the holders that prove, every epoch, that they still have the bytes.

Pieces, root and manifest

A stored file is cut into pieces of 1 MiB (1,048,576 bytes). Each piece is named by its SHA3-256 hash, and the file's identity, its root, is a hash over the piece size, the total size and the list of piece ids:

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

Pieces are copied whole; ZooBC uses no erasure coding. A StoreFile transaction (type 40) anchors the manifest on the chain: the root, the total size, the piece size, a deposit of at least 0.01 ZBC, 1 to 100,000 piece ids and an optional access block. The pieces themselves never go on the chain. A StoreFile for a root that is already active is a paid no-op; a root that was pruned may be stored again.

To upload, a wallet posts the file to a node's POST /api/v1/storage/upload, which returns the root, the sizes and the piece ids for the StoreFile. The node refuses when storage is off (503), when the body is empty (400), when it has under 2 GiB free or more than 256 MiB of pieces not yet anchored (507), and when a client exceeds 10 uploads per 600 seconds or 32 MiB per hour (429).

A file is cut into 1 MiB pieces; the manifest goes on the chain, and each piece goes to five nodes.
A file is cut into 1 MiB pieces; the manifest goes on the chain, and each piece goes to five nodes.

Who holds a piece

Placement uses rendezvous hashing. Time is cut into storage epochs of 240 blocks, about an hour, on mainnet. For one piece in one epoch, every active registered node gets a weight:

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

The epoch seed is the hash of the block that opens the epoch, at height epoch × 240. The five nodes with the highest weights are the expected holders, five being the chain parameter storage_replication; with fewer than five nodes, all of them hold the piece. Everything in the formula is chain data, so every node computes the same five without any announcement. The tally that pays the holders reads the registry as it was at the previous block, so no two nodes can disagree about the set.

Holders change from epoch to epoch, and the handover overlaps. Each epoch a node fetches the pieces assigned to it, first from the current holders, then from the previous ones, then from any node. It accepts a pushed piece only if the hash matches and it is an expected holder in this epoch or the previous one. It deletes an unassigned piece only if it was not a holder in the previous epoch either and the file is at least an hour old.

A node takes part only when its operator sets [storage] enabled = true in node.conf; it is off by default. The replication and the epoch length always come from the chain's parameters, whatever node.conf says.

Storage proofs

The chain never sees the pieces, so it cannot check a holder by itself. Instead, every epoch, each holder answers a challenge that only someone holding the bytes can compute:

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

Nobody can know the challenge before the epoch begins. Holders send their answers in SubmitStorageProof transactions (type 41): fee-free duties signed with a registered node's key, one transaction carrying a node's whole batch for the epoch.

At each epoch boundary the chain compares the answers. The value submitted by the most distinct nodes is taken as the truth, but only if no other value ties with it, and it counts only when at least three holders agree (oracle_quorum). Each agreeing holder's owner is then paid from the storage pool:

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

An outvoted holder earns nothing for that epoch but loses no participation score: since the chain never holds the bytes, agreement among holders is all it can judge. Holders that send nothing are recorded in an audit table. A node that is not yet mature earns nothing, and its storage reward stays in the pool (maturity and earnings).

Holders answer a fresh challenge every epoch; when at least three agree and no answer ties, each agreeing holder is paid.
Holders answer a fresh challenge every epoch; when at least three agree and no answer ties, each agreeing holder is paid.

Rent

Once per period, 172,800 blocks (about 30 days) on mainnet, the chain moves one period's rent from the file's deposit to the storage pool (ZBCSTORE):

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

The price is the same as for datasets, 0.05 ZBC per MiB at the start. Rent is charged in whole mebibytes, at least one: a 42-byte file pays 0.05 ZBC per period, and a file of 1,048,577 bytes, one byte over 1 MiB, pays 0.1 ZBC. A node's GET /api/v1/storage/quote?bytes=&blocks= prices a deposit with the same formula.

Any account can add money to an active file's deposit with FundStoredFile (type 296), at least 0.01 ZBC at a time. When the deposit cannot cover a full period, the file is pruned: its record is marked as not stored, and the remainder goes to the longevity pool.

Private files

A StoreFile's access block can mark a file private (flag bit 0) and add an allow list of 1 to 16 addresses (flag bit 1). A node's API then refuses an unsigned request for the content (401) and a signed request from anyone but the owner or the list (403). Private files are left out of public listings, and a public file that shares a piece with an active private file is guarded the same way.

"Private" is an access mode, not encryption. The pieces are still copied unencrypted to their holders, and nodes serve pieces to each other, so the operator of a holding node can read the bytes on its disk. If the content matters, encrypt it before uploading: then your key protects the content, and the private flag adds a second, lighter layer.

See also