Web3 и блокчейн Децентрализованные хранилища: IPFS, Filecoin, Arweave, контентная адресация
0%

Децентрализованные хранилища: IPFS, Filecoin, Arweave, контентная адресация

Децентрализованные хранилища: IPFS, Filecoin, Arweave, контентная адресация

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

  • целостность — я могу убедиться, что получил ровно те байты, а не подделку;
  • доступность — прямо сейчас есть кто-то, кто их отдаст;
  • долговечность — они будут доступны и через десять лет.

Контентная адресация даёт первое почти бесплатно. Второе и третье она не даёт вообще никак — их приходится покупать отдельно, деньгами или экономическими стимулами. Дальше мы разберём CID по байтам и посчитаем его руками; посмотрим, как IPFS режет файл на чанки, ищет владельцев через DHT и обменивается блоками; разберём, что именно доказывают PoRep и PoSt в Filecoin; посчитаем арифметику эндаумента Arweave и её ключевое допущение; напишем на Solidity правильное хранение ссылки. Никаких прогнозов курсов и советов, что покупать: разговор про инженерию и её честные компромиссы.

Почему данные не кладут в блокчейн

Начнём с цены. В Ethereum и EVM запись 32 байт в состояние (SSTORE с нуля) стоит 22 100 газа, килобайт — 32 слота, около 707 000 газа, а мегабайт потребовал бы порядка 700 миллионов и в блок не влезает физически. Дело не только в деньгах: состояние блокчейна реплицируется на каждую полную ноду и хранится вечно. Каждый байт, положенный туда, тысячи людей по всему миру будут таскать за собой десятилетиями. Это делает блокчейн худшим в мире хранилищем по цене за байт — и лучшим в мире реестром прав, потому что запись невозможно тихо отменить. Отсюда единственно разумная архитектура: в цепи — хеши и права, снаружи — байты.

Куда положить Цена за 1 КБ Кто хранит Как долго
Storage контракта ~707 000 газа каждая полная нода пока жива сеть
Calldata транзакции ~16 400 газа архивные ноды вся история блоков
Лог события ~8 600 газа индексаторы, архивные ноды история блоков
Blob (EIP-4844) отдельный рынок, обычно копейки консенсус-ноды ~18 суток, потом удаляется
IPFS + пиннинг 22 100 газа разово за CID тот, кто платит за пин пока платите
Filecoin 22 100 газа разово за CID провайдер под залогом срок сделки
Arweave 22 100 газа разово за CID майнеры сети по замыслу — вечно

Обратите внимание на нижние три строки: цена в цепи одинаковая и не зависит от размера файла, потому что в цепи лежат одни и те же 32 байта; различается только судьба байтов снаружи. Отдельно про блобы EIP-4844: их удаление через ~18 суток — не баг, а дизайн. Блобы решают задачу data availability для роллапов (об этом в «Масштабировании»): нужно гарантировать, что данные были опубликованы и любой мог их скачать, а не что они будут лежать вечно. Это отдельное свойство, и путать его с хранением — распространённая ошибка.

Контентная адресация: имя, вычисляемое из содержимого

Привычный веб адресует место: https://example.com/logo.png — это «спроси у машины, на которую указывает example.com, файл по такому пути». Что она вернёт, зависит от неё: захочет — вернёт другую картинку, уйдёт из бизнеса — не вернёт ничего, и проверить, что вам отдали «то самое», невозможно в принципе — у вас нет эталона. Контентная адресация переворачивает это: имя объекта — это хеш его содержимого. Адрес отвечает не «где лежит», а «что это». Свойства падают из определения:

  • Проверяемость. Скачали байты, посчитали хеш, сравнили с именем. Совпало — данные подлинные, и неважно, от кого вы их получили: от официального сервера, от соседа по Wi-Fi или с флешки. Транспорт перестаёт быть доверенным.
  • Дедупликация. Одинаковое содержимое — одно имя. Тысяча пользователей загрузила один и тот же PDF — в сети один объект.
  • Кэшируемость без инвалидации. Кэш по контентному адресу не может протухнуть: содержимое по этому имени не изменится никогда.
  • Неизменяемость как побочный эффект. Правка файла даёт новое имя. Это одновременно главная сила и главное неудобство: «обновить страницу по тому же адресу» напрямую нельзя, нужен отдельный слой изменяемых указателей.

Идея старше Web3: так устроен git (коммиты, деревья и блобы адресуются хешем содержимого — подробности в треке по git), так устроены Nix и Bazel, так работает дедупликация в бэкапах вроде restic и borg. Web3 добавил к ней сеть, которая ищет данные по такому имени у кого угодно.

Анатомия CID

В IPFS контентный адрес называется CID (Content Identifier). Это не просто хеш, а самоописывающая структура, из которой видно, чем хешировали, во сколько байт и как интерпретировать блок. Все три слоя ниже — из семейства спецификаций multiformats.

Разбор CID по байтам: multibase, версия, кодек, multihash

  • multihash = <код хеш-функции><длина дайджеста><дайджест>. Для sha2-256 это 0x12 0x20 <32 байта>. Смысл: алгоритм не зашит в формат, и переход на blake3 или на постквантовый хеш не ломает парсеры.
  • multicodec — код перед multihash, говорящий, как читать блок: 0x55 = raw (просто байты), 0x70 = dag-pb (protobuf-узел UnixFS), 0x71 = dag-cbor.
  • multibase — префикс текстовой записи: b = base32 lowercase, z = base58btc, f = hex. Нужен, чтобы по строке всегда было однозначно понятно, в какой кодировке она записана.

Посчитаем CID руками, без библиотек. Код самодостаточный — запустите и сверьте с ipfs add:

import base64
import hashlib

B58 = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def base58btc(data: bytes) -> str:
    """Кодировка CIDv0 — те самые имена Qm... O(n^2) по длине, но здесь n = 34 байта."""
    n, out = int.from_bytes(data, "big"), b""
    while n:
        n, rem = divmod(n, 58)
        out = B58[rem:rem + 1] + out
    pad = len(data) - len(data.lstrip(b"\x00"))   # ведущие нули кодируются символом '1'
    return (b"1" * pad + out).decode()


def base32mb(data: bytes) -> str:
    """multibase 'b': RFC 4648 base32, нижний регистр, без padding."""
    return "b" + base64.b32encode(data).decode().lower().rstrip("=")


def varint(value: int) -> bytes:
    """protobuf varint — им кодируются длины и коды в multiformats."""
    out = b""
    while True:
        chunk, value = value & 0x7F, value >> 7
        out += bytes([chunk | (0x80 if value else 0)])
        if not value:
            return out


payload = b"hello world"
multihash = b"\x12\x20" + hashlib.sha256(payload).digest()   # sha2-256, 32 байта

# CIDv1 raw: версия 0x01, кодек raw 0x55, дальше multihash
print(base32mb(b"\x01\x55" + multihash))
# bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e

# Те же байты, но упакованные в UnixFS-узел — так делает `ipfs add` по умолчанию.
# UnixFS Data: поле 1 = тип (2 = File), поле 2 = байты, поле 3 = размер.
unixfs = b"\x08\x02\x12" + varint(len(payload)) + payload + b"\x18" + varint(len(payload))
# PBNode: Data — поле 1 (тег 0x0a), Links — поле 2; в кодировке они идут в обратном порядке.
mh_pb = b"\x12\x20" + hashlib.sha256(b"\x0a" + varint(len(unixfs)) + unixfs).digest()

print(base58btc(mh_pb))            # Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD — CIDv0
print(base32mb(b"\x01\x70" + mh_pb))
# bafybeihykld7uyxzogax6vgyvag42y7464eywpf55gxi5qpoisibh3c5wa — тот же блок как CIDv1

Контрольная точка: тот же код на пустом UnixFS-каталоге (блок 0a 02 08 01) обязан дать QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn — самый известный CID в экосистеме.

Первая ловушка: один файл, много CID

Заметили? Одиннадцать байт hello world дали три разных законных CID. Это не ошибка, а следствие того, что CID именует не «файл», а конкретный блок в конкретной упаковке. Разойтись достаточно по любому параметру:

Параметр Варианты Кто по умолчанию как
Версия CID v0 (base58, dag-pb) / v1 (base32, любой кодек) ipfs add исторически v0; большинство JS-библиотек — v1
raw-leaves листья как raw или как dag-pb --cid-version=1 включает raw-leaves, если не выключить явно
Чанкер фиксированный 256 КиБ / rabin / buzhash фиксированный size-262144
Ссылок в узле 174 по умолчанию зависит от реализации DAG-строителя
Обёртка в каталог --wrap-with-directory выключено

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

IPFS: что это на самом деле

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

Слои децентрализованного хранения и что каждая система реально покрывает

Данные: чанки и Merkle DAG

Файл не хранится целиком: он режется на блоки (по умолчанию 256 КиБ) и собирается в Merkle DAG — направленный ациклический граф, где каждый узел ссылается на детей по их CID.

Почему DAG, а не дерево: одинаковые поддеревья схлопываются в один узел, и два файла с общим началом разделят все совпавшие чанки — если границы совпали. С фиксированным чанкером вставка одного байта в начало сдвигает все границы и убивает дедупликацию; для этого существует content-defined chunking (rabin, buzhash), ставящий границы по содержимому и переживающий вставки, ценой недетерминированных размеров и лишнего CPU. Сложность: построение DAG — O(n) по времени от размера файла и O(log_k n) по глубине при k = 174 ссылках на узел; выборка произвольного диапазона байт — O(log n) обращений к блокам, потому что размеры детей записаны в родителе и по ним можно навигировать, не читая сами чанки.

Маршрутизация: кто вообще держит этот блок

У вас есть CID. Как найти узел, у которого есть блок? Работает Kademlia DHT — распределённая хеш-таблица, где расстояние между идентификаторами есть XOR их битов, а поиск сходится за O(log n) шагов при n узлах. Ключевой момент, который многие упускают: в DHT лежат не данные, а «провайдер-записи» — сообщения вида «узел с таким PeerID заявляет, что у него есть блок с таким хешем». Записи имеют ограниченный срок жизни (в go-libp2p — порядка 48 часов), поэтому узел обязан периодически переанонсировать себя; в Kubo за это отвечает Reprovider.Interval. Ушедший в оффлайн узел через сутки-двое просто исчезает из поиска, даже если данные у него на диске.

Для крупных провайдеров DHT не масштабируется: анонсировать миллиарды CID по одному невозможно. Поэтому появился IPNI (InterPlanetary Network Indexer, публичный инстанс — cid.contact): провайдер отдаёт индексатору пачку объявлений одним куском, а клиенты спрашивают его по HTTP. Быстрее и дешевле — ценой того, что индексатор становится точкой, которую можно отключить или заставить фильтровать.

Передача: Bitswap и его наследники

Найдя провайдера, узел просит блоки протоколом Bitswap: клиент рассылает подключённым пирам список желаемого (WANT-HAVE — «есть ли у тебя?», WANT-BLOCK — «дай»), пиры отвечают HAVE/DONT_HAVE/блоком.

Важное отличие от BitTorrent: в Bitswap нет жёсткого tit-for-tat. В торренте вы обязаны раздавать, чтобы качать; в Bitswap узлы отдают блоки в основном из вежливости — низкий порог входа ценой отсутствия стимулов, отсюда и нестабильность раздачи. Помимо Bitswap есть GraphSync (запросить целое поддерево DAG одним запросом, экономя раунд-триппы) и trustless HTTP gateway (запросить сырой блок или CAR-файл обычным HTTP и проверить хеш самому). Последнее сейчас основной путь для браузеров: полноценный p2p-узел в вебе неудобен, а проверяемый HTTP даёт ту же гарантию целостности.

Пиннинг и сборка мусора: миф, который стоил денег многим

Вот здесь погибает большинство проектов. Формулировка «я загрузил файл в IPFS» не означает ничего. Буквально она означает: файл лежит на диске вашего узла и объявлен в DHT. Всё.

Что отсюда следует, дословно:

  1. Незакреплённые блоки удаляются первым же ipfs repo gc или при достижении StorageMax. Ваш собственный узел выкинет ваш файл.
  2. Чужие узлы вам ничего не должны. Кто-то прочитал ваш CID через шлюз — блок осел в кэше шлюза до ближайшей уборки. Это не репликация, это кэш.
  3. Выключили ноутбук — файл недоступен. В сети нет копий, если вы их не оплатили.
  4. Популярность не создаёт копий. Никакого «чем чаще качают, тем надёжнее хранится» в протоколе нет.

Поэтому реальные проекты платят за пиннинг: свой сервер с узлом либо сервис (Pinata, Filebase, web3.storage и другие), общающийся по стандартизованному IPFS Pinning Service API. Честная формулировка звучит так: «мой файл хранится, пока я плачу конкретной компании» — по гарантиям это не сильно отличается от S3. Зато адресация остаётся контентной, и в этом реальная ценность: вы меняете хранилище, не меняя идентификаторы.

CID неизменяем, но сайты обновляются. Нужен слой указателей:

  • IPNS — подписанная запись «ключ K сейчас указывает на CID X» с порядковым номером и TTL, публикуемая в DHT. Плюс: полностью децентрализованно. Минус: публикация и разрешение медленные (секунды-десятки секунд), запись нужно периодически переиздавать.
  • DNSLink — TXT-запись _dnslink.example.com со значением dnslink=/ipfs/<CID>. Быстро, понятно, работает везде — ценой возврата доверия к DNS и регистратору.
  • ENS contenthash (EIP-1577) — CID записан в ончейн-домен. Изменение стоит газа и видно в истории цепи, что для аудита плюс; это же поле читают браузеры с поддержкой .eth.

Общее правило: изменяемый указатель — ровно тот компонент, где возвращается доверие. Кто владеет ключом IPNS, зоной DNS или ENS-именем, тот в любой момент подменит содержимое. Нужна честная неизменность (релизы, юридические документы, метаданные NFT) — публикуйте голый CID и не давайте себе возможности его поменять.

Шлюзы: удобство ценой централизации

Браузер не умеет ipfs://, поэтому существуют шлюзы: https://ipfs.io/ipfs/<CID> или https://<cid>.ipfs.dweb.link/. Второй вариант — subdomain gateway — предпочтительнее для веб-приложений: каждый CID получает свой origin, и браузерная изоляция не даёт одному загруженному сайту лазить в хранилище другого. Чем вы платите за шлюз:

  • Шлюз видит, кто какой CID запросил. Это полноценный лог доступа, как у любого CDN.
  • Шлюз может не отдать контент: публичные шлюзы применяют denylist (проект badbits — список хешей CID для блокировки нелегального контента). Данные в сети остаются, но конкретная дверь закрыта.
  • Если вы не проверяете хеш на клиенте, шлюз может отдать что угодно, и вы не заметите. Вся ценность контентной адресации при этом испаряется.

Последний пункт лечится кодом. Минимальная проверка одноблочного raw-CID без внешних зависимостей:

const B32 = "abcdefghijklmnopqrstuvwxyz234567";

/** Достаём 32 байта дайджеста из текстовой формы CIDv1 в base32. */
function digestFromCidV1(cid: string): Uint8Array {
  if (cid[0] !== "b") throw new Error("ожидается multibase 'b' (base32)");
  let acc = 0, bits = 0;
  const out: number[] = [];
  for (const ch of cid.slice(1)) {
    const idx = B32.indexOf(ch);
    if (idx < 0) throw new Error(`недопустимый символ base32: ${ch}`);
    acc = (acc << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      bits -= 8;
      out.push((acc >> bits) & 0xff);
      acc &= (1 << bits) - 1; // иначе acc переполнит 32-битные побитовые операции JS
    }
  }
  const bytes = Uint8Array.from(out);
  // раскладка: <0x01 версия><кодек><0x12 sha2-256><0x20 длина><32 байта дайджеста>
  if (bytes[0] !== 0x01 || bytes[2] !== 0x12 || bytes[3] !== 0x20) {
    throw new Error("поддержан только профиль CIDv1 / sha2-256");
  }
  return bytes.slice(4, 36);
}

/** Скачиваем сырой блок и проверяем сами. Шлюз здесь — просто транспорт. */
export async function fetchVerified(cid: string, gateway = "https://ipfs.io"): Promise<Uint8Array> {
  // trustless-режим: отдай сырой блок, не собирай DAG за меня
  const res = await fetch(`${gateway}/ipfs/${cid}`, { headers: { Accept: "application/vnd.ipld.raw" } });
  if (!res.ok) throw new Error(`шлюз ответил ${res.status}`);

  const bytes = new Uint8Array(await res.arrayBuffer());
  const actual = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
  const expected = digestFromCidV1(cid);
  // Сравнение за постоянное время не нужно: секрета нет, проверяем только целостность.
  if (actual.length !== expected.length || actual.some((b, i) => b !== expected[i])) {
    throw new Error("шлюз отдал не тот контент — CID не сошёлся");
  }
  return bytes;
}

Для многоблочных файлов ручная проверка превращается в полноценную реализацию UnixFS, и правильный ответ здесь — библиотека: @helia/verified-fetch скачивает CAR или блоки и проверяет весь DAG локально, давая API, похожий на обычный fetch. Главное — чтобы проверка вообще была.

Filecoin: рынок долговечности

IPFS отвечает на вопрос «как назвать и как передать». Filecoin — на «как заставить незнакомца хранить и доказать, что он не врёт». Механика: клиент заключает с провайдером сделку («храни эти данные N дней за столько-то FIL»), провайдер вносит залог и обязан регулярно доказывать наличие данных; не смог — теряет часть залога. Доказательства две штуки, и разницу полезно понимать:

  • PoRep (Proof of Replication) — одноразовое, при приёме данных. Провайдер «запечатывает» (seal) сектор: прогоняет данные через намеренно медленное, привязанное к его идентификатору преобразование. Смысл — доказать, что хранится отдельная физическая копия, а не ссылка на чужую; без этого десять «провайдеров» держали бы один файл на одном диске и получали десять вознаграждений. Запечатывание занимает часы и требует серьёзного железа — это цена уникальности копии.
  • PoSt (Proof of Spacetime) — регулярное, на протяжении всей сделки. WindowPoSt требует доказать наличие каждого сектора раз в 24 часа: сутки разбиты на 48 получасовых «дедлайнов», в каждом провайдер отчитывается за свою часть секторов. WinningPoSt — быстрая разновидность, дающая право произвести блок.

Piece CID — это не CID из IPFS. Данные упаковываются в CAR-файл (Content Addressable aRchive — сериализация набора блоков с корнем), добиваются нулями до размера сектора (32 или 64 ГиБ) с особым выравниванием, и от результата считается CommP — корень Merkle-дерева над этим представлением. Payload CID (тот, что понимает IPFS) и Piece CID — разные идентификаторы одних данных, и путаница между ними — классическая ошибка новичка.

Filecoin доказывает хранение, но не быстроту отдачи. Данные лежат запечатанными; чтобы их выдать, сектор нужно «распечатать» (unseal) — это время и вычисления, отсюда отдельный слой retrieval-сделок и кэширующих сетей. Практическая рекомендация: держите горячую копию (пиннинг-сервис, свой узел, CDN), а Filecoin используйте как проверяемый архив — так и делают крупные датасеты.

Защищает вас экономика, а не криптография: залог, штрафы за пропущенные доказательства и штраф за досрочное прекращение сектора. Если хранить однажды станет дороже, чем стоит штраф за потерю, рациональный провайдер ваши данные потеряет. Модель угроз здесь экономическая, и оценивать её нужно как экономическую. И сделка конечна — обычно от полугода до нескольких лет, после чего провайдер вправе выкинуть сектор: «положил в Filecoin — лежит вечно» неверно, нужен ваш процесс продления. Программа Filecoin Plus с DataCap и нотариями даёт провайдерам десятикратный множитель мощности за общественно полезные данные, что удешевляет хранение почти до нуля, но срочности сделок не отменяет. С запуском FVM (Filecoin Virtual Machine, 2023) сделками стало можно управлять EVM-совместимыми контрактами: автопродление, коллективные хранилища, «фонды на хранение датасета» — тот же Solidity, что в «Смарт-контрактах», и те же классы уязвимостей из «Безопасности контрактов».

Arweave: заплатить один раз

Arweave решает ту же задачу иначе: платишь один раз — хранится всегда. Разберём, на чём это держится.

Структура. Вместо цепочки — blockweave: блок ссылается на предыдущий и дополнительно на псевдослучайно выбранный recall block из прошлого, и чтобы майнить, нужен доступ к данным этого старого блока. Итог: майнинг требует не только вычислений, но и хранения истории, а раз recall-блок случаен, рационально хранить как можно больше — особенно то, что хранят немногие, потому что конкуренция за такие блоки ниже. Механизм эволюционировал: Proof of AccessSPoRA (Succinct Proofs of Random Access, версия 2.4) → версия 2.6, где добавлен основанный на хеш-цепочке ограничитель скорости майнинга. Смысл последнего — сделать выгодными именно локально лежащие данные, а не быстрый доступ к чужому хранилищу или переспам вычислениями.

Экономика. Плата за загрузку делится: небольшая часть уходит майнеру сразу, основная — в эндаумент, из которого выплаты идут майнерам десятилетиями. Цена рассчитывается так, чтобы покрыть примерно 200 лет хранения при консервативном допущении, что стоимость хранения байта снижается на 0,5% в год. Посчитаем, что это означает:

def endowment_multiplier(decline_per_year: float, years: int = 200) -> float:
    """Во сколько «текущих годовых стоимостей хранения» обойдётся заданный срок.

    Если стоимость года t равна c0 * (1 - d)^t, суммарная стоимость — геометрическая
    сумма. Сложность O(years) по времени, O(1) по памяти.
    """
    return sum((1.0 - decline_per_year) ** t for t in range(years))


for decline in (0.00, 0.005, 0.02, 0.10, 0.30):
    print(f"{decline:>5.1%} в год -> {endowment_multiplier(decline):6.1f}x")

#  0.0% в год ->  200.0x   стоимость хранения не падает — нужно 200 годовых
#  0.5% в год ->  126.6x   допущение Arweave: платим ~127 годовых вперёд
#  2.0% в год ->   49.1x
# 10.0% в год ->   10.0x
# 30.0% в год ->    3.3x   близко к историческому темпу удешевления дисков

Логика такая: исторически стоимость хранения падала на десятки процентов в год, поэтому реальная сумма к выплате ближе к правой части таблицы, а собирают по левой — с огромным запасом. Но это допущение, а не теорема. Остановится удешевление дисков (физические пределы плотности записи вполне реальны) — запас проедается. Это не повод не пользоваться Arweave, это повод понимать: «вечно» здесь означает «пока держится экономическая модель и её допущения», а не математическую гарантию. Формальное описание — в жёлтой книге Arweave. Инженерные детали, с которыми столкнётесь сразу:

  • Бандлы ANS-104. Класть каждый мелкий файл отдельной транзакцией дорого и медленно; стандарт ANS-104 позволяет упаковать тысячи элементов в одну. Сервисы-бандлеры (Irys, бывший Bundlr) принимают загрузку, отдают квитанцию мгновенно и публикуют пачкой. Пока бандл не попал в цепь, бандлер — доверенная сторона: «загрузил» и «сохранено навсегда» разделены окном обычного корпоративного доверия.
  • Шлюзы. arweave.net — это шлюз, а не сеть. Шлюзы (сеть ar.io) индексируют данные, дают GraphQL-поиск и отдают HTTP; они же — точка цензуры: шлюз может не показать контент, который в сети присутствует. Как и в IPFS, цензура переезжает на край, к точке входа.
  • Неудаляемость абсолютна. Ошибочно загруженный персональный файл удалить нельзя — ни вам, ни суду. Прежде чем нажимать «загрузить», подумайте дважды, особенно если данные касаются других людей.

Сравнение: что именно гарантирует каждый вариант

Пояснения к неочевидным точкам. Calldata Ethereum неудаляема в том смысле, что входит в историю цепи, но доступность истории обеспечивают архивные ноды, а обсуждаемый механизм её истечения (EIP-4444) прямо предполагает, что рядовые узлы перестанут её хранить — забота о старых данных переедет на добровольные архивы. Объектный сторедж по SLA — самая предсказуемая вещь в таблице, и если децентрализация не нужна вам по существу задачи, это правильный ответ. Из смежных систем стоит знать Storj (стирающее кодирование и распределение по независимым узлам с централизованной координацией — фактически S3-совместимое хранилище с распределённым бэкендом), Sia, Swarm (хранилище экосистемы Ethereum со встроенным стимулированием) и Codex: дизайн-пространство шире трёх разобранных систем.

Смарт-контракты и CID: как хранить ссылку, чтобы она не умерла

Правило: в цепи — дайджест, снаружи — байты. Сначала цена вопроса: CIDv1 в base32 — строка из 59 символов, в storage это слот длины плюс два слова данных, три холодных SSTORE ≈ 66 300 газа. Тот же дайджест в bytes32 — один слот, 22 100 газа; префикс 0x01 0x55 0x12 0x20 одинаков для всех CID одного профиля и в состоянии не нужен.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title Реестр публикаций: в цепи живут 32 байта, содержимое — снаружи
/// @notice Профиль CID зафиксирован контрактом: CIDv1 / raw / sha2-256 / 32 байта.
contract ContentRegistry {
    /// Постоянный префикс CID: 0x01 (версия) 0x55 (raw) 0x12 (sha2-256) 0x20 (длина).
    /// Константа не занимает слот и ничего не стоит при чтении.
    bytes4 public constant CID_PREFIX = 0x01551220;

    struct Publication {
        bytes32 digest;      // слот 1: sha2-256 содержимого
        uint64 publishedAt;  // слот 2: 8 байт...
        address author;      // ...и 20 байт — упакованы в один слот
    }

    mapping(uint256 => Publication) private _publications;
    uint256 public total;

    event Published(uint256 indexed id, address indexed author, bytes32 digest);

    error EmptyDigest();
    error UnknownPublication(uint256 id);

    /// @notice Зафиксировать публикацию. Изменить её нельзя — в этом весь смысл.
    function publish(bytes32 digest) external returns (uint256 id) {
        if (digest == bytes32(0)) revert EmptyDigest();
        unchecked { id = ++total; }
        _publications[id] = Publication(digest, uint64(block.timestamp), msg.sender);
        emit Published(id, msg.sender, digest); // дешёвый канал для индексаторов и фронтенда
    }

    function publicationOf(uint256 id) external view returns (Publication memory p) {
        p = _publications[id];
        if (p.publishedAt == 0) revert UnknownPublication(id);
    }
}

Обратите внимание на то, чего здесь нет: функции update. Как только появляется возможность поменять дайджест, вы возвращаетесь к обычной изменяемой ссылке, и неизменяемость превращается в обещание владельца ключа. Иногда обновляемость нужна — делайте её явной, с событием и таймлоком, чтобы пользователи успели заметить подмену. Теперь классическая беда NFT — антипаттерн и лечение (сами стандарты — в «Токенах и стандартах»):

// ПЛОХО: метаданные живут на сервере компании и меняются одним вызовом.
// Компания закрылась, домен не продлён — коллекция стала набором битых ссылок.
contract FragileNFT is ERC721 {
    string private _base = "https://api.example.com/meta/";

    function setBaseURI(string calldata newBase) external onlyOwner {
        _base = newBase;                    // владелец подменяет ВСЕ метаданные разом
    }

    function _baseURI() internal view override returns (string memory) { return _base; }
}

// ЛУЧШЕ: baseURI — константа с CID каталога метаданных.
// tokenURI(1) даст ipfs://bafybei.../1, и содержимое проверяемо любым клиентом.
contract FrozenNFT is ERC721 {
    string private constant _BASE = "ipfs://bafybeihykld7uyxzogax6vgyvag42y7464eywpf55gxi5qpoisibh3c5wa/";

    function _baseURI() internal pure override returns (string memory) { return _BASE; }
}

Чек-лист «метаданные, которые переживут проект»:

  1. ipfs://<CID>, а не URL шлюза. Записанный в контракт https://gateway-компании.io/ipfs/... намертво привязывает вас к чужому домену; кошельки и маркетплейсы умеют резолвить ipfs:// сами.
  2. CID каталога, а не отдельных файлов. Один CID коммитит ко всем метаданным сразу; подменить элемент незаметно нельзя.
  3. Внутри JSON-метаданных ссылка на изображение — тоже ipfs://. Половина «замороженных» коллекций ломается именно здесь: CID метаданных зафиксирован, а image внутри указывает на живой https-домен.
  4. Порядок деплоя: сначала загрузить и запинить метаданные, потом деплоить контракт с уже известным CID. Обратный порядок неизбежно требует изменяемого setBaseURI.
  5. Оплатите хранение явно — пиннинг-сервис плюс сделка Filecoin или загрузка в Arweave. Без этого пункты 1–4 дают красивую ссылку в никуда.

Риски, о которых надо говорить прямо

  • «Загрузил в IPFS» ≠ «хранится». Самый дорогой миф в теме. Без пина или оплаченной сделки данные живут ровно столько, сколько работает ваш узел, и исчезают тихо — вы узнаете об этом от пользователей.
  • Шлюз — точка централизации. Он логирует запросы, фильтрует по denylist и может отдать не то, если вы не проверяете хеш. Публичный шлюз в проде без локальной проверки — это обычный HTTPS с лишними шагами.
  • CID нестабилен между инструментами. Разные версии, флаги и чанкеры дают разные имена одному файлу; без фиксации параметров воспроизводимость сборки иллюзорна.
  • Приватности нет. Всё, что попало в публичную сеть, публично. Хуже: CID — точный отпечаток, и любой, у кого есть файл, может проверить, раздаёте ли вы именно его. Нужна приватность — шифруйте до загрузки и решайте управление ключами отдельно (см. «Кошельки и ключи»; ключ шифрования данных не должен совпадать с ключом кошелька).
  • Удаление невозможно, и это юридическая проблема. «Право на забвение» и требования об удалении персональных данных несовместимы с неизменяемым публичным хранилищем. Рабочая стратегия одна — не класть туда персональные данные в открытом виде.
  • Сделки Filecoin истекают, бандлеры Arweave — доверенная сторона до попадания в цепь. Нужен мониторинг продлений и проверка, что транзакция подтверждена, а не просто принята.
  • Скам-паттерн: «децентрализованное хранилище» поверх одного S3-бакета. Встречается регулярно. Проверка простая: возьмите CID и достаньте данные через независимый узел или шлюз, не связанный с проектом. Не достаётся — децентрализации нет, есть маркетинг.
  • Ключ от IPNS/ENS-имени — это ключ от содержимого. Его компрометация означает подмену сайта или метаданных для всех пользователей; хранить его надо как ключ от денег.

Практика: рецепты, которые работают

# Детерминированный CID: все параметры заданы явно, никаких умолчаний.
ipfs add --cid-version=1 --raw-leaves=true --chunker=size-262144 --pin=true ./release.tar.gz

# Что именно закреплено на узле (recursive — корни, которые вы держите намеренно)
ipfs pin ls --type=recursive

# Структура DAG: сколько блоков и какого размера
ipfs dag stat bafybei...

# Кто ещё в сети анонсирует этот CID — проверка доступности СНАРУЖИ вашего узла
ipfs routing findprovs bafkrei...

# Экспорт в CAR — переносимый архив блоков (то, что едет в Filecoin)
ipfs dag export bafybei... > release.car

# Независимая проверка через публичный шлюз: сравниваем с локальным хешем
curl -sL "https://ipfs.io/ipfs/bafkrei..." | sha256sum

Как выбирать хранилище — дерево решений, покрывающее большинство случаев:

Пункт про мониторинг — не украшение. Единственный способ узнать, что данные ещё живы, — периодически доставать их из независимой точки. Заведите проверку в CI: раз в сутки скачать CID через сторонний шлюз и сравнить хеш. Десять строк и спасённая коллекция.

Мини-итог

  • Хранение — это три разных свойства: целостность, доступность, долговечность. Контентная адресация бесплатно даёт только первое.
  • CID — самоописывающая структура (multibase + версия + кодек + multihash), а не «хеш файла». Один файл законно имеет несколько CID в зависимости от упаковки; воспроизводимость требует фиксировать флаги.
  • IPFS — это адресация (CID), модель данных (UnixFS/Merkle DAG), маршрутизация (Kademlia DHT и IPNI) и передача (Bitswap, GraphSync, trustless HTTP). Хранения и стимулов в нём нет; всё держится на пиннинге, за который кто-то платит.
  • Шлюзы возвращают централизацию: логи, denylist, возможность подмены. Проверяйте хеш на клиенте — иначе смысл контентной адресации теряется.
  • Filecoin покупает долговечность экономикой: PoRep доказывает уникальность копии, WindowPoSt — её наличие каждые 24 часа, залог и штрафы делают потерю невыгодной. Сделки срочные, отдача требует распечатки сектора.
  • Arweave берёт разовую плату в эндаумент, рассчитанный на 200 лет при допущении об удешевлении хранения на 0,5% в год. Гарантия экономическая, а не математическая.
  • В контракте храните bytes32-дайджест (22 100 газа против ~66 300 за строку), схему ipfs:// вместо URL шлюза, CID каталога вместо отдельных файлов — и оплатите само хранение.
  • Публичные хранилища неудаляемы и непубличны только если вы зашифровали данные сами. Планируйте это до первой загрузки, а не после.

Источники

Что дальше

Мы разобрались, куда девать байты, которым не место в блокчейне. Осталась зеркальная проблема: что делать, когда в блокчейн не помещаются сами транзакции. Дальше — Масштабирование: L2, роллапы, шардирование, мосты и их риски: как роллап сжимает тысячи операций в одно доказательство, чем optimistic отличается от zk, почему блобы EIP-4844 сделали L2 дешевле в разы — и почему мосты между сетями остаются местом, где произошли самые крупные кражи в истории отрасли.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов