ZooBC wiki
Data and storage

Datasets

You are looking at an old version of this page: revision #1063 by zoobc.how, 2026-10-27 00:42 UTC. See the current version

A dataset is the simplest way to put data on ZooBC: a small record, a property name and its value, that one account writes under an account, usually its own. Every node holds the row as part of the chain's state, so any app can read it through any node's API. Apps use rows for profiles, settings, release notices, invoices and token icons.

A row

A SetupAccountDataset transaction (type 3) writes one row, keyed by three things:

  • the setter, the account that sends the transaction;
  • the recipient, the account the row sits under, usually the setter itself (an empty recipient is also a valid key);
  • the property, a name of 1 to 256 bytes, never empty.

The value holds up to 4,096 bytes, within a transaction body of at most 65,536 bytes. Rows are versioned: writing the same key again replaces the current row, and RemoveAccountDataset (type 259) makes a row inactive.

One dataset row: who wrote it, under whom, and the property and its value, inside an object that pays rent.
One dataset row: who wrote it, under whom, and the property and its value, inside an object that pays rent.

A row is public: anyone can read it, so a dataset is no place for a secret. Nor is it a way to keep a large file. Small files go to files on the chain (DFS), large ones to the decentralised storage pool.

Who wrote it matters

Any account may write a row under any recipient, and the recipient is not asked, in the same way that anyone can send a payment to any address. A row is therefore a statement made by its setter about the recipient. The chain proves who said it and when; it cannot know whether the statement is true.

A node lists an account's rows at GET /api/v1/accounts/<address>/datasets, optionally filtered with ?property=<prefix>. The listing returns every row where the address is the setter or the recipient, each labelled with its role: setter, recipient or both. An app that finds "verified = yes" under a user must accept it only when the setter is the account it trusts, such as its own verification account, and must answer "not verified" when no such row exists (chain data is untrusted input).

Two rows say "verified = yes"; only the one written by the trusted setter counts.
Two rows say "verified = yes"; only the one written by the trusted setter counts.

Objects and owners

Every row belongs to a dataset object, named by the hash of the transaction that created it. The object has an owner (at first its creator), a pending owner and a management mode.

  • Handing over takes two steps. The owner sends TransferDataset (type 42) naming the new owner, and the chain only records a pending owner. The handover completes when that account sends AcceptDataset (type 44). An AcceptDataset from anyone else, or for an object with no pending owner, is a paid no-op: the fee is charged and nothing changes. So nobody can push an object, and the rent that comes with it, onto an account that did not ask for it, and the old owner stays the owner until the new one accepts.
  • Deleting. DeleteDataset (type 45), sent by the owner, deactivates every row of the object, and the object's deposit is swept to the longevity pool.
  • Policy. SetDatasetPolicy (type 43) stores a management mode (owner-only, whitelist, blacklist or open) and an access list on the object, but no consensus rule reads them, so they restrict nothing.

Each object also has a deposit address: an account of type 10, written ZBS_…, whose bytes are 0A000000 followed by the object id. Anyone can fund it with a plain SendZBC, so a group of users can keep a shared record alive together. It is an ordinary funded account, so it pays account rent and pruning as well. GET /api/v1/dataset-address/<hash or ZBS_ address> shows it.

Rent

Once per billing period, 172,800 blocks (about 30 days) on mainnet, the chain charges each object for the bytes of its active rows:

bytes = sum of (property + value) over the object's active rows
rent  = ceil(bytes × price per MiB ÷ 1,048,576), then × storage multiplier, rounded up

The starting price is 5,000,000 atomic units (0.05 ZBC) per MiB, and the storage multiplier starts at 1.0. One full MiB of rows pays exactly 0.05 ZBC per period. A row with a 14-byte property and a 300-byte value, 314 bytes in all, pays 1,498 units: 314 × 5,000,000 ÷ 1,048,576 is 1,497.27, and each stage of the charge rounds up.

The rent is taken from the deposit first, then from the owner's balance. If both together cannot pay, every row of the object is deactivated and any deposit left goes to the longevity pool. The recipient is never charged. All dataset rent is credited to the longevity pool, which pays the nodes a slice in every block; ZBC is never burned.

The billing period follows the snapshot interval. The price is voted by the seats and can go down to one atomic unit; it moves once per snapshot cycle, within limits, to the average of the counted seats' votes (price votes).

Reserved prefixes

A few property prefixes are reserved, and the chain checks their rows before accepting them. The test compares the raw bytes at the start of the property, so invoice: is not inv:.

PrefixRule
dfs: and dfs:dir:written only by the three DFS transaction types; a SetupAccountDataset there is refused (files on the chain (DFS))
tokenicon:a PNG or WebP image that declares a width and a height of 1 to 512 pixels, 16 to 4,096 bytes long
inv:a JSON object with no negative number anywhere, so an invoice cannot carry a negative amount; enforced from block 0

A token's icon therefore lives on the chain, inside the row, not behind a URL. None of these checks involves anyone's signature: every node applies the same rule.

See also