Розробка системи кеширування контенту на блокчейні
Постановка задачи звучить так: «хочемо хранить контент децентрализовано, але потрібно щоб завантажувався швидко». Це протиріччя у самому запиті. IPFS — повільний, Arweave — повільний, Filecoin — повільний. Блокчейн взагалі не передбачений для зберігання великих обсягів даних. Система кеширування контенту на блокчейні — це не «хранити контент у блокчейні», це «використовувати блокчейн для управління розподіленим кешем», де сам контент знаходится у decentralized storage, а права доступу, стан кеша й економіка кеширування — на цепі.
Архітектурна модель: що де зберігається
Правильне розділення відповідальності:
Контент (файли, відео, дані) → IPFS / Arweave / Filecoin
Метаданні й CID → On-chain або у calldata
Права доступу й DRM → Smart contracts
Економіка кеширування → Token incentives (on-chain)
CDN / edge кеш → Традиційна інфраструктура або decentralized (Fleek, Spheron)
Блокчейн — не база даних для контенту. Зберігати 1MB on-chain на Ethereum mainnet коштує ~$3000+ (при газі 30 gwei, 16 gas/byte для calldata). EIP-4844 blobs дешевше — ~$0.01 за 128KB — але дані доступні тільки ~18 днів.
Content Registry: контракт управління
Центральний елемент — реєстр контенту, який відслідковує що де зберігається й хто має права:
contract ContentRegistry {
struct ContentItem {
bytes32 contentId; // keccak256(originalUrl або uuid)
string ipfsCid; // IPFS CID (CIDv1 base32)
string arweaveTxId; // опціонально: Arweave для постійного зберігання
address publisher;
uint256 publishedAt;
uint256 size; // у байтах
ContentType contentType;
AccessModel accessModel;
bool active;
}
enum ContentType { Image, Video, Document, Data, Code }
enum AccessModel { Public, TokenGated, Subscription, PaidPerView }
mapping(bytes32 => ContentItem) public content;
mapping(bytes32 => mapping(address => bool)) public accessGrants;
event ContentPublished(bytes32 indexed contentId, string ipfsCid, address publisher);
event ContentAccessed(bytes32 indexed contentId, address user, uint256 timestamp);
function publishContent(
bytes32 contentId,
string calldata ipfsCid,
string calldata arweaveTxId,
uint256 size,
ContentType contentType,
AccessModel accessModel
) external {
require(content[contentId].publisher == address(0), "Already exists");
content[contentId] = ContentItem({
contentId: contentId,
ipfsCid: ipfsCid,
arweaveTxId: arweaveTxId,
publisher: msg.sender,
publishedAt: block.timestamp,
size: size,
contentType: contentType,
accessModel: accessModel,
active: true
});
emit ContentPublished(contentId, ipfsCid, msg.sender);
}
}
Token-gated доступ
ERC-721 або ERC-1155 як пропуск до контенту — стандартна практика для NFT-гейтед контенту:
interface IAccessController {
function hasAccess(bytes32 contentId, address user) external view returns (bool);
}
contract NFTGatedAccess is IAccessController {
ContentRegistry public registry;
mapping(bytes32 => address) public contentGates; // contentId => NFT contract
mapping(bytes32 => uint256) public requiredTokenId; // 0 = any token from collection
function hasAccess(bytes32 contentId, address user) external view override returns (bool) {
ContentRegistry.ContentItem memory item = registry.content(contentId);
if (item.accessModel == ContentRegistry.AccessModel.Public) return true;
address gateContract = contentGates[contentId];
if (gateContract == address(0)) return item.publisher == user;
IERC721 nft = IERC721(gateContract);
uint256 tokenId = requiredTokenId[contentId];
if (tokenId == 0) {
return nft.balanceOf(user) > 0;
} else {
return nft.ownerOf(tokenId) == user;
}
}
}
Децентралізована CDN з токен-інцентивами
Ідея: узли кеша (cache nodes) отримують награду за зберігання й раздачу контенту. Це модель Filecoin, але для hot cache (швидкий доступ), не cold storage.
Cache Node Registry
contract CacheNetwork {
struct CacheNode {
address operator;
string endpoint; // URL API ноди
uint256 stake; // Стейк для участі
uint256 bandwidthServed; // Байт, обслужених нодою
uint256 reputationScore;
bool active;
}
struct CacheJob {
bytes32 contentId;
address requester;
uint256 rewardPerGB;
uint256 duration; // секунди
uint256 deadline;
}
mapping(address => CacheNode) public nodes;
mapping(bytes32 => CacheJob) public jobs;
uint256 public constant MIN_STAKE = 0.1 ether;
function registerNode(string calldata endpoint) external payable {
require(msg.value >= MIN_STAKE, "Insufficient stake");
nodes[msg.sender] = CacheNode({
operator: msg.sender,
endpoint: endpoint,
stake: msg.value,
bandwidthServed: 0,
reputationScore: 100,
active: true
});
}
function postCacheJob(
bytes32 contentId,
uint256 rewardPerGB,
uint256 duration
) external payable {
// Escrow для оплати кеш-нод
jobs[contentId] = CacheJob({
contentId: contentId,
requester: msg.sender,
rewardPerGB: rewardPerGB,
duration: duration,
deadline: block.timestamp + duration
});
}
}
Proof of Bandwidth: як доказати що нода раздавала контент
Основна складність: верифікувати off-chain роботу on-chain. Кілька підходів:
Challenge-response (оптимістичний): нода заявляє про доставку X GB. Challenger може оспорити, запросивши proof. Нода повинна надати log з підписаними запитами користувачів (merkle tree з request logs). Якщо не може — slashing.
Signed receipts: кожен користувач при отриманні файлу підписує receipt (timestamp + contentHash + userAddress). Нода аккумулює receipts, періодично публікує merkle root on-chain. Дозволяє честиво атрибутувати bandwidth.
// Off-chain логіка cache node
interface ServingReceipt {
contentId: string;
userAddress: string;
bytesServed: number;
timestamp: number;
userSignature: string; // підпись користувача
}
async function claimBandwidthReward(receipts: ServingReceipt[]) {
// Собираємо merkle tree з receipts
const leaves = receipts.map(r =>
ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
["bytes32", "address", "uint256", "uint256"],
[r.contentId, r.userAddress, r.bytesServed, r.timestamp]
))
);
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getHexRoot();
// Публікуємо root on-chain
await cacheContract.submitBandwidthClaim(root, totalBytesServed, receipts.length);
}
IPFS Pinning і доступність контенту
IPFS без pinning service — ненадійний. Контент видаляється з локального кеша нод після garbage collection. Для production системи потрібен явний пінінг.
Децентралізований pinning через смарт-контракт
contract PinningMarket {
struct PinRequest {
string cid;
address requester;
uint256 payment; // total payment в escrow
uint256 replicationFactor; // скільки нод повинні зберігати
uint256 duration;
uint256 activeUntil;
address[] pinners; // хто пінніт
}
mapping(bytes32 => PinRequest) public requests;
function requestPin(
string calldata cid,
uint256 replicationFactor,
uint256 duration
) external payable {
bytes32 requestId = keccak256(abi.encodePacked(cid, msg.sender, block.timestamp));
requests[requestId] = PinRequest({
cid: cid,
requester: msg.sender,
payment: msg.value,
replicationFactor: replicationFactor,
duration: duration,
activeUntil: block.timestamp + duration,
pinners: new address[](0)
});
}
function acceptPin(bytes32 requestId) external {
PinRequest storage req = requests[requestId];
require(req.pinners.length < req.replicationFactor, "Fully replicated");
// Нода берет задання на пінніг
req.pinners.push(msg.sender);
}
// Періодично нода доказує що файл доступний через Proof of Storage
function submitStorageProof(bytes32 requestId, bytes calldata proof) external {
// Верифікуємо через verifiable delay function або challenge-response
_verifyStorageProof(requestId, proof);
// Розблокуємо частину payment
_releasePartialPayment(requestId, msg.sender);
}
}
Готові рішення: Filecoin/Estuary для довгостроквього зберігання, web3.storage (Storacha), Pinata або NFT.Storage з API. Кастомна реалізація виправдана якщо потрібен специфічний контроль над економікою.
Content Addressing й дедупілікація
IPFS CIDv1 — content-addressed: одинакові дані дають одинаковий CID. Автоматична дедупілікація на рівні зберігання. Але потрібно правильно чанковувати:
import { create } from "ipfs-http-client";
const ipfs = create({ url: "https://ipfs.infura.io:5001" });
async function uploadWithChunking(data: Buffer): Promise<string> {
const result = await ipfs.add(data, {
chunker: "rabin-262144-524288-1048576", // rabin chunking для кращої дедупілікації
cidVersion: 1,
hashAlg: "sha2-256",
});
return result.cid.toString();
}
Rabin chunking розбиває файли по content-defined boundaries — одне змінення в файлі міняє тільки невелику частину chunks, а не весь файл. Важливо для великих файлів з інкрементальними оновленнями.
Продуктивність: гібридна архітектура
Для реального приложения потрібен шар швидкого доступу поверх децентралізованого зберігання:
Користувач
↓
Edge CDN (Cloudflare / Akamai) — hot кеш, <100ms
↓ cache miss
IPFS Gateway кластер (власні ноди) — warm кеш, <1s
↓ cache miss
IPFS Network / Arweave — cold storage, 2-30s
On-chain компонент: користувач запитує доступ → смарт-контракт верифікує права → видає signed URL або access token → клієнт йде на CDN з цим токеном.
Стек розробки
| Компонент | Технологія |
|---|---|
| Content storage | IPFS (Kubo) + Arweave для perma |
| Pinning | web3.storage API або Estuary |
| Registry контракт | Solidity + Foundry |
| Access control | ERC-721 gating + Lit Protocol для encryption |
| Edge cache | Cloudflare Workers + R2 |
| Bandwidth proof | Merkle receipts + optimistic verification |
| Node SDK | TypeScript + helia (новий IPFS JS) |
Коли це має смисл
Система виправдана, якщо:
- Потрібна censorship resistance (контент не може бути видалений централізованим рішенням)
- Правоволодільці хочуть on-chain verifiable права й автоматичні royalties
- Потрібна прозора економіка для операторів кеш-нод
Якщо задача просто «швидко видавати файли» — достатньо Cloudflare + S3.
Сроки
MVP з registry, IPFS зберіганням й token-gated доступом — 4–6 тижнів. Повна система з incentivized cache nodes, bandwidth proofs, governance — 3–4 місяці.







