Вики ZooBC
Данные и хранение

Пул децентрализованного хранения

Децентрализованное хранилище держит файлы, которые слишком велики для транзакции. Цепочка записывает только манифест и депозит каждого файла; фрагменты живут на зарегистрированных узлах, по пять копий каждого, размещённых жеребьёвкой, которую может повторить любой узел. Аренда файлов оплачивает хранителей, которые каждую эпоху доказывают, что байты всё ещё у них.

Фрагменты, корень и манифест

Хранимый файл разрезается на фрагменты по 1 МиБ (1 048 576 байт). Каждый фрагмент называется своим хешем SHA3-256, а идентичность файла, его корень, — хеш от размера фрагмента, общего размера и списка идентификаторов фрагментов:

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 идентификаторов фрагментов и необязательный блок доступа. Сами фрагменты никогда не попадают в цепочку. StoreFile для корня, который уже активен, — оплаченная пустая операция; корень, который был удалён, можно сохранить снова.

Для загрузки кошелёк отправляет файл на POST /api/v1/storage/upload узла, который возвращает корень, размеры и идентификаторы фрагментов для StoreFile. Узел отказывает, когда хранение выключено (503), когда тело пустое (400), когда у него меньше 2 ГиБ свободного места или больше 256 МиБ ещё не закреплённых фрагментов (507) и когда клиент превышает 10 загрузок за 600 секунд или 32 МиБ в час (429).

Файл разрезается на фрагменты по 1 МиБ; манифест попадает в цепочку, а каждый фрагмент — на пять узлов.
Файл разрезается на фрагменты по 1 МиБ; манифест попадает в цепочку, а каждый фрагмент — на пять узлов.

Кто хранит фрагмент

Размещение использует рандеву-хеширование. Время делится на эпохи хранения по 240 блоков, около часа, в mainnet. Для одного фрагмента в одной эпохе каждый активный зарегистрированный узел получает вес:

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)

Хранитель, оставшийся в меньшинстве, ничего не зарабатывает за эту эпоху, но не теряет оценки участия: поскольку цепочка никогда не держит байты, согласие между хранителями — это всё, о чём она может судить. Хранители, которые ничего не отправили, записываются в таблицу аудита. Узел, который ещё не зрел, ничего не зарабатывает, и его вознаграждение за хранение остаётся в пуле (зрелость и доходы).

Хранители каждую эпоху отвечают на новый вызов; когда согласны не меньше трёх и ни один ответ не сравнялся с другим, каждому согласному хранителю платят.
Хранители каждую эпоху отвечают на новый вызов; когда согласны не меньше трёх и ни один ответ не сравнялся с другим, каждому согласному хранителю платят.

Аренда

Раз в период, 172 800 блоков (около 30 дней) в mainnet, цепочка переводит аренду за один период из депозита файла в пул хранения (ZBCSTORE):

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

Цена та же, что и для наборов данных, — 0,05 ZBC за МиБ в начале. Аренда взимается за целые мебибайты, не меньше одного: файл в 42 байта платит 0,05 ZBC за период, а файл в 1 048 577 байт, на один байт больше 1 МиБ, — 0,1 ZBC. GET /api/v1/storage/quote?bytes=&blocks= узла рассчитывает депозит по той же формуле.

Любой аккаунт может добавить деньги в депозит активного файла транзакцией FundStoredFile (тип 296), не менее 0,01 ZBC за раз. Когда депозит не может покрыть полный период, файл удаляется: его запись помечается как не хранимая, а остаток уходит в пул долголетия.

Приватные файлы

Блок доступа StoreFile может пометить файл как приватный (бит флага 0) и добавить список допуска из 1–16 адресов (бит флага 1). Тогда API узла отклоняет неподписанный запрос содержимого (401) и подписанный запрос от кого-либо, кроме владельца или адресов из списка (403). Приватные файлы не попадают в публичные списки, и публичный файл, который делит фрагмент с активным приватным файлом, защищается так же.

«Приватный» — это режим доступа, а не шифрование. Фрагменты по-прежнему копируются хранителям в незашифрованном виде, а узлы отдают фрагменты друг другу, поэтому оператор узла-хранителя может прочитать байты на своём диске. Если содержимое важно, зашифруйте его перед загрузкой: тогда содержимое защищает ваш ключ, а приватный флаг добавляет второй, более лёгкий слой.

См. также