📦 第15课:IPFS存储

DApp开发 阶段三 ✅ 验证通过

🎯 学习目标:理解IPFS去中心化存储原理,掌握CID、内容寻址、DAG等核心概念,学会在智能合约中存储IPFS哈希,实现NFT元数据与DApp数据的去中心化存储方案。

📖 一、为什么需要去中心化存储?

链上存储的限制:
• 以太坊存储成本极高——1KB数据约需~0.02 ETH($40+)
• 区块大小限制——不适合存大文件
• Gas费用——每次写入都收费

传统中心化存储的问题:
• 服务器宕机→数据丢失
• 域名到期→链接失效
• 审查→内容被删除
• 单点故障→NFT图片变成死链

IPFS的解决方案:
• 内容寻址——用哈希作为地址,不依赖服务器
• 分布式存储——数据存在多个节点
• 不可篡改——内容改变则哈希改变
• 与区块链互补——链上存哈希,链下存数据

📖 二、IPFS核心原理

2.1 内容寻址 vs 位置寻址

位置寻址(HTTP) 内容寻址(IPFS) https://server.com/img.png ipfs://QmXx...abc ┌───────────┐ ┌───────────┐ │ 客户端 │ │ 客户端 │ └─────┬─────┘ └─────┬─────┘ │ │ ▼ ▼ ┌───────────┐ ┌───────────┐ │ server.com │ ← 服务器挂了 │ 节点A │ ← 有这个哈希 │ 404! │ 就404 │ 数据✓ │ 就能返回 └───────────┘ ├───────────┤ │ 节点B │ ← 备份 │ 数据✓ │ ├───────────┤ │ 节点C │ ← 备份 │ 数据✓ │ └───────────┘

2.2 CID(Content Identifier)

CID = 内容的身份证,由哈希算法+编码方式+数据哈希组成。

// CID结构解析
CIDv0: QmXxYz...                    ← 直接是SHA-256的Base58编码
CIDv1: bafybeigdyr...               ← 更灵活的格式

CIDv1详细结构:
┌────────┬──────────┬──────────┬────────────────────┐
│  版本   │ 编码方式  │ 哈希算法  │    哈希值          │
│  0x01  │ 0x50(多) │ 0x12(多) │  SHA-256结果        │
│ CIDv1  │ dag-json │ sha2-256 │  32字节             │
└────────┴──────────┴──────────┴────────────────────┘

// 示例:NFT元数据的CID
ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylq2f6zeg3rjnl3kcqyy/

// 在浏览器中访问
https://ipfs.io/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylq2f6zeg3rjnl3kcqyy/
https://dweb.link/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylq2f6zeg3rjnl3kcqyy/

2.3 Merkle DAG

IPFS Merkle DAG(有向无环图) 大文件 → 分块 → 每块独立哈希 → 组成DAG ┌──────────┐ │ 根CID │ │ QmRoot.. │ └──┬────┬──┘ │ │ ┌────────┘ └────────┐ ▼ ▼ ┌──────────┐ ┌──────────┐ │ CID-A │ │ CID-B │ │ QmA... │ │ QmB... │ └──┬───┬──┘ └────┬─────┘ │ │ │ ┌────┘ └────┐ │ ▼ ▼ ▼ ┌──────┐ ┌──────┐ ┌──────┐ │数据块1│ │数据块2│ │数据块3│ │256KB │ │256KB │ │180KB │ └──────┘ └──────┘ └──────┘ 优势: • 去重:相同内容 = 相同CID • 分块传输:只需下载需要的块 • 验证:每块独立可验证

📖 三、智能合约 + IPFS模式

3.1 经典架构:链上哈希 + 链下数据

// contracts/IPFSRegistry.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title IPFSRegistry
 * @dev 去中心化数据注册表——链上存CID,链下存内容
 */
contract IPFSRegistry {
    address public owner;
    
    // 每条记录的CID(bytes32可以存CIDv0或截断的CIDv1)
    struct Record {
        string cid;           // 完整IPFS CID
        string name;          // 记录名称
        address creator;      // 创建者
        uint256 timestamp;    // 创建时间
        uint256 size;         // 文件大小(字节)
    }

    mapping(uint256 => Record) public records;
    uint256 public recordCount;

    event RecordAdded(uint256 indexed id, string cid, address creator);

    constructor() {
        owner = msg.sender;
    }

    /// @notice 注册一条IPFS记录
    function addRecord(
        string memory _cid,
        string memory _name,
        uint256 _size
    ) external returns (uint256) {
        require(bytes(_cid).length > 0, "CID cannot be empty");
        
        uint256 id = recordCount;
        records[id] = Record({
            cid: _cid,
            name: _name,
            creator: msg.sender,
            timestamp: block.timestamp,
            size: _size
        });
        
        emit RecordAdded(id, _cid, msg.sender);
        recordCount++;
        return id;
    }

    /// @notice 获取记录的完整IPFS URL
    function getIPFSUrl(uint256 _id) external view returns (string memory) {
        require(_id < recordCount, "Record not found");
        return string(abi.encodePacked("ipfs://", records[_id].cid));
    }

    /// @notice 获取记录的HTTP网关URL
    function getGatewayUrl(uint256 _id) external view returns (string memory) {
        require(_id < recordCount, "Record not found");
        return string(abi.encodePacked("https://ipfs.io/ipfs/", records[_id].cid));
    }
}

3.2 NFT元数据 + IPFS

// contracts/IPFSNFT.sol — 基于IPFS存储的NFT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract IPFSNFT is ERC721 {
    using Strings for uint256;
    
    uint256 public nextTokenId;
    string private _baseURI;  // IPFS CID作为baseURI
    address public owner;

    constructor(string memory _cid) ERC721("IPFS Art", "IART") {
        owner = msg.sender;
        // 设置IPFS基础URI
        _baseURI = string(abi.encodePacked("ipfs://", _cid, "/"));
    }

    function mint() external {
        _safeMint(msg.sender, nextTokenId);
        nextTokenId++;
    }

    // 返回每个token的元数据URI
    // 例如:ipfs://QmXXX/0.json
    function tokenURI(uint256 tokenId) 
        public view override 
        returns (string memory) 
    {
        _requireOwned(tokenId);
        return string(abi.encodePacked(_baseURI, tokenId.toString(), ".json"));
    }

    // 更新基础URI(用于元数据迁移)
    function setBaseURI(string memory _cid) external {
        require(msg.sender == owner, "Not owner");
        _baseURI = string(abi.encodePacked("ipfs://", _cid, "/"));
    }
}

📖 四、NFT元数据JSON格式

// 存储在IPFS上的元数据文件: 0.json
{
  "name": "IPFS Art #0",
  "description": "A fully decentralized NFT with IPFS metadata",
  "image": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/image.png",
  "attributes": [
    {
      "trait_type": "Rarity",
      "value": "Legendary"
    },
    {
      "trait_type": "Power",
      "value": 95
    },
    {
      "display_type": "date",
      "trait_type": "Created",
      "value": 1700000000
    }
  ]
}

📖 五、IPFS上传实战

5.1 使用Pinata API上传

// scripts/upload-to-ipfs.js
const axios = require("axios");
const fs = require("fs");
const FormData = require("form-data");

const PINATA_API_KEY = process.env.PINATA_API_KEY;
const PINATA_SECRET = process.env.PINATA_SECRET;

// 上传单个文件到IPFS
async function uploadFile(filePath) {
  const formData = new FormData();
  formData.append("file", fs.createReadStream(filePath));

  const response = await axios.post(
    "https://api.pinata.cloud/pinning/pinFileToIPFS",
    formData,
    {
      headers: {
        ...formData.getHeaders(),
        pinata_api_key: PINATA_API_KEY,
        pinata_secret_api_key: PINATA_SECRET,
      },
    }
  );

  console.log("上传成功!CID:", response.data.IpfsHash);
  console.log("IPFS URL:", `ipfs://${response.data.IpfsHash}`);
  return response.data.IpfsHash;
}

// 上传JSON元数据到IPFS
async function uploadMetadata(metadata) {
  const response = await axios.post(
    "https://api.pinata.cloud/pinning/pinJSONToIPFS",
    { pinataContent: metadata },
    {
      headers: {
        pinata_api_key: PINATA_API_KEY,
        pinata_secret_api_key: PINATA_SECRET,
      },
    }
  );

  console.log("元数据CID:", response.data.IpfsHash);
  return response.data.IpfsHash;
}

// 批量上传NFT元数据
async function uploadNFTCollection(count) {
  const cids = [];
  
  for (let i = 0; i < count; i++) {
    const metadata = {
      name: `IPFS Art #${i}`,
      description: "Decentralized NFT collection",
      image: `ipfs://QmImageCID/${i}.png`,
      attributes: [
        { trait_type: "ID", value: i },
        { trait_type: "Rarity", value: i % 10 === 0 ? "Legendary" : "Common" },
      ],
    };
    
    const cid = await uploadMetadata(metadata);
    cids.push({ id: i, cid });
    console.log(`Token #${i} → CID: ${cid}`);
  }
  
  return cids;
}

// 使用示例
uploadNFTCollection(10).then(console.log);

5.2 使用nft.storage免费上传

// scripts/nft-storage-upload.js
const { NFTStorage, Blob } = require("nft.storage");

const client = new NFTStorage({ token: process.env.NFT_STORAGE_TOKEN });

async function storeNFT(imagePath, name, description) {
  const imageData = fs.readFileSync(imagePath);
  
  // nft.storage会自动创建ERC-721兼容的元数据
  const metadata = await client.store({
    name,
    description,
    image: new Blob([imageData], { type: "image/png" }),
  });

  console.log("元数据CID:", metadata.ipnft);
  console.log("元数据URL:", metadata.url);
  // 元数据URL格式: ipfs://bafyreiXXX/metadata.json
  
  return metadata;
}

📖 六、Pinning:让数据持久化

IPFS数据不是永久存储的!

IPFS节点会定期清理不常访问的数据(GC)。要让数据持久存在,需要Pin(固定)。

Pinning方式:
本地Pinipfs pin add CID — 自己的节点固定
Pinning服务:Pinata、nft.storage、Infura — 付费服务
Filecoin:去中心化存储网络,付费激励节点存储

推荐组合:
• 免费项目:nft.storage(免费额度)
• 商业项目:Pinata Pro + Filecoin备份
• 自建节点:IPFS Cluster + Filecoin

📖 七、完整DApp数据流

DApp数据流:IPFS + 智能合约 1. 用户上传内容 │ ▼ ┌──────────────┐ │ 前端DApp │ ──── 文件/JSON ────→ IPFS节点 └──────┬───────┘ │ │ │ │ ┌─────┴─────┐ │ │ Pinata/ │ │ │ nft.storage│ ← Pin数据 │ └─────┬─────┘ │ │ │ 返回CID │ │◄──────────────────────────────┘ │ │ CID写入智能合约 ▼ ┌──────────────┐ │ 智能合约 │ ← 只存CID字符串(便宜!) │ addRecord() │ │ cid: "Qm..."│ └──────────────┘ │ │ 前端读取CID ▼ ┌──────────────┐ │ 前端DApp │ ──── CID ────→ IPFS网关 ────→ 获取数据 └──────────────┘ ipfs://Qm... https://ipfs.io/ipfs/Qm...

🧪 练习

1. 为什么不在智能合约中直接存储图片/大文件? 2. CID的核心特性是什么? 3. 为什么IPFS数据需要Pinning?

📖 五、IPFS与Arweave对比

IPFSArweave
共识无(需Pin服务)Proof of Access
存储临时(可能被GC)永久
付费Pin服务月费一次付费永久
速度快(多节点并行)较慢
适用NFT元数据/网站永久存档
代表Pinata/nft.storageArDrive

📖 六、去中心化存储最佳实践

  1. 媒体文件上传到IPFS, 获取CID
  2. 元数据JSON也上传到IPFS获取CID
  3. 合约中只存ipfs://CID前缀(节省Gas)
  4. 使用Pin服务确保数据持久化
  5. 关键数据考虑Arweave永久存储
  6. 实现紧急情况下的baseURI更新函数
  7. 验证CID完整性防止篡改
  8. 设置metadata冻结函数(防止后续修改)

📖 七、常见陷阱与最佳实践

  1. ❌ 不Pin导致数据被垃圾回收丢失
  2. ❌ 元数据使用中心化URL(不如IPFS可靠)
  3. ❌ 大文件直接上链(Gas爆炸)
  4. ❌ 不验证CID完整性
  5. ❌ 忽略网关可用性风险
  6. ✅ 使用nft.storage免费Pin服务
  7. ✅ 设置baseURI冻结保证不可篡改

📖 八、IPFS内容寻址深入

// CID生成过程
// 1. 文件内容 → SHA-256哈希
// 2. 添加多编解码器前缀(multicodec)
// 3. 添加多哈希前缀(multihash)
// 4. Base32编码 → CIDv1 (bafy...)

// 前端验证CID完整性
async function verifyCID(content, expectedCid) {
  const hash = await crypto.subtle.digest('SHA-256', content);
  const computedCid = encodeCID(hash);
  return computedCid === expectedCid;
}

// IPFS DAG: 目录结构
// ipfs://QmDir/   → 目录CID
//   ├── image.png → QmImg
//   └── metadata.json → QmMeta

📖 九、NFT元数据最佳实践

元数据设计原则:
1. 使用IPFS而非中心化服务器
2. 图片和元数据分开上传获取不同CID
3. 合约中只存baseURI前缀
4. 实现冻结机制防止后续修改
5. 支持Lazy Minting降低Gas
6. 考虑使用IPFS目录结构方便管理
7. 元数据JSON符合OpenSea标准
8. 属性使用snake_case命名
📦

🏆 成就解锁:星际存储者

你已掌握IPFS去中心化存储!从内容寻址原理到NFT元数据,从Pinata上传到Pinning策略,你打通了链上链下的数据桥梁。

关键收获:

✅ IPFS内容寻址与CID原理
✅ 链上哈希+链下数据的架构模式
✅ NFT元数据JSON格式与IPFS存储
✅ Pinata/nft.storage上传实战
✅ Pinning与数据持久化策略

📋 课程目录