Skip to content
On this page

L2-16: IPFS Node(IPFS 节点搭建)

1. 问题

大多数 NFT 项目的"元数据存储"号称去中心化,但实际上严重依赖 Infura IPFS 网关、Pinata 等中心化 pinning 服务。你的 NFT 图片的"永久存储"实际上只是"某个公司承诺帮你 pin 数据"——如果这个公司停止服务、修改定价、或宕机,你的 NFT 元数据就不再可达。虽然数据仍然在 IPFS 网络中的某处(如果被别人 pin 过),但可达性得不到保证。

搭建自己的 IPFS 节点让你完全掌控 NFT 数据存储:内容由你的节点提供(而非第三方网管)、pin 策略由你决定(哪些内容需要永久保留)、并且你可以为社区提供公共 IPFS 网关服务。这是从"依赖别人的基础设施"到"贡献自己的基础设施"的转变。

本挑战(参考 scripts/ipfs-node.md)要求完成从零搭建 IPFS 节点的全过程——包括安装、初始配置、内容 pin 管理、垃圾回收策略、以及与 Solidity NFT 合约的集成。

2. 原因

IPFS(InterPlanetary File System,星际文件系统)是一个点对点的分布式文件系统,它使用内容寻址(Content Addressing)而非位置寻址(Location Addressing)。在 HTTP 中,你通过 URL(如 https://example.com/image.png)访问文件——URL 告诉你"文件在哪里"。在 IPFS 中,你通过 CID(Content Identifier,如 QmXxXxX...)访问文件——CID 告诉你"文件是什么",因为 CID 是文件内容的哈希值。

内容寻址意味着两个关键特性:

  1. 不可变性:相同的文件内容总是产生相同的 CID;修改文件的一字节就产生全新的 CID。这确保了 NFT 元数据的完整性——你可以验证收到的数据是否真的是原始版本。
  2. 去中心化:任何人都可以"提供"(serve)具有给定 CID 的内容——你的节点、朋友的节点、公共网关——只要能通过 DHT(分布式哈希表)找到持有该内容的节点即可。

运行自己的 IPFS 节点让你从"消费者"变为"提供者":你不仅消费 IPFS 网络的内容,你的节点还会为网络贡献存储和带宽。当你的节点持有某个 CID 的内容时,它会响应网络中其他节点的请求——你成为了 IPFS 基础设施的一部分。

在 NFT 的上下文中,自托管 IPFS 节点意味着:你的 NFT 元数据不依赖 Pinata 的服务器是否在线、不依赖 Infura 网管是否限流、不依赖任何第三方服务——只要你的节点在线,你的 NFT 数据就可达。

3. 方案

安装与初始化 IPFS Kubo

bash
# ====== 1. 安装 IPFS Kubo(原 go-ipfs)======
# Linux
wget https://dist.ipfs.tech/kubo/latest/kubo_linux-amd64.tar.gz
tar -xvzf kubo_linux-amd64.tar.gz
cd kubo
sudo bash install.sh

# macOS
brew install ipfs

# ====== 2. 初始化节点 ======
ipfs init
# 生成 ~/.ipfs/ 目录,包含:
#   config       — 节点配置文件
#   datastore    — 数据存储(默认 flatfs)
#   keystore     — 节点密钥和 IPNS 密钥
#   blocks       — 内容块存储

ipfs init 生成的 config 文件包含了节点的完整配置,包括:

  • Addresses:节点监听的网络地址(API、Gateway、Swarm)
  • Bootstrap:初始连接的引导节点列表
  • Datastore:存储配置(StorageMax 上限、GC 策略)
  • Identity:节点的 PeerID 和私钥

配置节点

bash
# ====== 3. 调整存储上限(默认仅 10 GB)======
ipfs config Datastore.StorageMax 100GB

# ====== 4. 配置垃圾回收(每小时自动清理未 pin 的内容)======
ipfs config Datastore.GCPeriod 1h

# ====== 5. 启用公共网关(可选——允许任何人通过你的节点访问 IPFS 内容)======
ipfs config --json Addresses.Gateway '"/ip4/0.0.0.0/tcp/8080"'

# ====== 6. 查看完整配置 ======
ipfs config show

启动守护进程

bash
# 方式 1:前台运行
ipfs daemon

# 方式 2:systemd 服务(生产环境推荐)
sudo tee /etc/systemd/system/ipfs.service << 'EOF'
[Unit]
Description=IPFS Daemon
After=network.target

[Service]
Type=simple
User=ubuntu
ExecStart=/usr/local/bin/ipfs daemon
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable ipfs
sudo systemctl start ipfs
sudo systemctl status ipfs

添加和 Pin 内容

bash
# ====== 7. 添加文件 ======
echo "ETH Tech Tree NFT Metadata" > metadata.json
ipfs add metadata.json
# 输出: added QmXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx metadata.json
#       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#       CID (内容哈希) — 这就是文件的永久地址

# ====== 8. Pin 内容(防止被 GC 清理)======
ipfs pin add QmXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx

# ====== 9. 添加整个目录(NFT 集合)======
ipfs add -r my-nft-collection/
# 输出: added QmYyYyYy... my-nft-collection
# 目录本身也有一个 CID(由目录内所有文件的 CID 计算得出)

# ====== 10. 查看已 pin 的内容 ======
ipfs pin ls              # 列出所有 pin(本地)
ipfs pin ls --type=recursive  # 仅显示递归 pin

# ====== 11. 通过本地网关访问 ======
curl http://localhost:8080/ipfs/QmXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx
# 返回: "ETH Tech Tree NFT Metadata"

与 Solidity NFT 合约集成

部署 NFT 合约时,tokenURI 返回 IPFS URI:

solidity
// 在合约中使用 ipfs:// 协议前缀
string memory uri = string(abi.encodePacked("ipfs://", cid));

// 或使用 HTTP 网关作为后备(市场/钱包更兼容)
string memory gatewayUri = string(abi.encodePacked(
    "https://ipfs.io/ipfs/", cid
));

// 或使用自己的节点网关(如果你有公网节点)
string memory selfHostedUri = string(abi.encodePacked(
    "https://my-ipfs-node.example.com/ipfs/", cid
));

IPNS:可变指针指向不可变内容

由于 IPFS 内容是不可变的(修改文件 = 新 CID),你需要一种方式来"指向"最新版本。IPNS (InterPlanetary Name System) 提供可变的名称系统:

bash
# ====== 12. 创建 IPNS 密钥对(用于 NFT 项目)======
ipfs key gen my-nft-project
# 输出: k51qzi5uqu5...

# ====== 13. 发布 IPNS 记录 ======
ipfs name publish --key=my-nft-project QmNewVersionCID
# 现在 /ipns/k51qzi5uqu5... → 指向 QmNewVersionCID

# ====== 14. 更新 IPNS 指向(更新 NFT 元数据后)======
ipfs add updated-metadata.json  # 获取新 CID
ipfs name publish --key=my-nft-project QmUpdatedCID

# 合约中使用 IPNS:
# string memory uri = string(abi.encodePacked("ipns://k51qzi5uqu5..."));

IPNS 的关键特性:每次 publish 更新后,所有通过 ipns://key 访问的用户都会自动获得最新指向的 CID——不需要更新智能合约中的 URI。

NFT 项目完整工作流

1. 生成 NFT 元数据 JSON(名称、描述、属性)
2. 生成 NFT 图片(SVG 或 PNG)
3. ipfs add image.png → 获得图像 CID
4. 在 metadata.json 中引用 "image": "ipfs://imageCID"
5. ipfs add metadata.json → 获得元数据 CID
6. 部署智能合约,tokenURI() 返回 "ipfs://metadataCID"
7. 在 IPFS 节点上 pin 所有 content(图像 + 元数据)
8. 可选:创建 IPNS 名称指向集合根目录
9. 可选:在 Pinata/web3.storage 上做冗余 pin(双重保险)
10. 可选:通过 Filecoin 进行去中心化长期存储

4. 遭遇的陷阱

  • "永久存储"的迷思:IPFS 不是 Filecoin——它没有内置的经济激励机制来确保内容持久性。你 pin 的内容只存在于 pin 它的节点上——如果你的节点离线且没有其他节点 pin 同一内容,内容就会从网络中消失
  • CID 不变性陷阱:修改 NFT 元数据(哪怕一字节)意味着全新的 CID——但已部署的合约中的 tokenURI 返回的是旧 CID。没有 IPNS 或可升级合约,你无法更新元数据
  • 垃圾回收意外删除:未 pin 的内容会被 GC 自动清理——如果你 ipfs add 了内容但没有 ipfs pin add,1 小时后它可能就不在你的节点上了
  • 公网可达性差:新节点需要时间发现 DHT 网络中的对等节点——NAT 后面的节点(家用路由器)可能数天都无法被其他节点找到
  • CID 版本混乱:IPFS 有两种 CID 格式——CIDv0(以 Qm 开头,Base58)和 CIDv1(以 b 开头,Base32)——不同的工具和库默认使用不同的格式
  • 公共网关不保证服务https://ipfs.io/ipfs/CID 是中心化网管——ipfs.io 可能限流、宕机或修改服务条款

5. 陷阱的原因

"IPFS 是永久存储"的误解源于 IPFS 的底层描述——"分布式文件系统"。事实上,IPFS 提供的只是内容寻址和点对点传输——持久性需要额外的激励层(Filecoin)或主动 pinning。类比:IPFS 是 BitTorrent(对等传输),Filecoin 是种子保活服务(经济激励持久化)。没有 pinning,你的内容就像没有种子的 torrent——技术上存在,但不可达。

CID 不变性是内容寻址的必然结果:CID = hash(content)。修改 content 后 hash 改变,CID 必然改变。这不是 bug,而是特性——它保证了内容的完整性(你通过 CID 请求的内容正是你期望的内容,不可能被篡改)。但这种"不可变性"在需要更新元数据时变成了劣势——你需要额外的可变层(IPNS、ENS、或可升级合约的 _baseURI)来重定向。

NAT 后面的节点连接性差是因为 IPFS 依赖去中心化的 DHT(Kademlia DHT)来进行对等发现。DHT 是双向的——其他节点需要能够直接连接到你的节点。如果路由器没有开启端口转发(TCP 4001,默认),你只能连接到已发现的节点,但其他节点无法主动连接你——这意味着你的节点不会出现在 DHT 查找结果中,你提供的 CID 对其他节点不可达。

6. 如何解决陷阱

多层冗余 pinning 策略——不要只依赖自己的节点:

bash
# 自己的节点(主要 pin)
ipfs pin add QmCID

# Pinata(备份 pin 服务)
# https://pinata.cloud — 免费 1 GB,付费计划更多
curl -X POST "https://api.pinata.cloud/pinning/pinByHash" \
  -H "Authorization: Bearer $PINATA_JWT" \
  -d '{"hashToPin": "QmCID"}'

# web3.storage(Filecoin-backed 备份)
# https://web3.storage — 免费 5 GB,自动备份到 Filecoin

使用 IPNS 实现可变元数据

bash
# 项目初始化时生成 IPNS 密钥
ipfs key gen my-nft

# 部署合约时使用 ipns:// 而非 ipfs://
# tokenURI = "ipns://k51qzi5uqu5..." 
# 以太坊库需要特殊处理 IPNS,通常使用 HTTP 网关代理

# 实际做法:使用可升级的 _baseURI
# 在 NFT 合约中:
string public baseURI;  // 可被 owner 修改

function setBaseURI(string memory _newBaseURI) external onlyOwner {
    baseURI = _newBaseURI;  // 指向新的 IPFS CID
}

确保 GC 不会删除关键数据:在添加内容后立即 pin:

bash
# 一键添加并 pin(使用 shell 函数)
add-and-pin() {
    local cid=$(ipfs add -Q "$1")
    ipfs pin add "$cid"
    echo "Added and pinned: $cid"
}

add-and-pin metadata.json

-Q 参数使 ipfs add 只输出 CID(无文件名),便于链式操作。

解决 NAT 问题

bash
# 1. 在路由器上转发端口 4001 (TCP+UDP) 到节点
# 2. 配置 IPFS 使用公网地址
ipfs config --json Addresses.Swarm '[
    "/ip4/0.0.0.0/tcp/4001",
    "/ip6/::/tcp/4001"
]'

# 3. 手动通告公网 IP(如果 UPnP 不可用)
ipfs config --json Routing.AcceleratedDHTClient true

# 4. 验证连接性
ipfs swarm peers        # 查看已连接的对等节点数量
ipfs id                 # 查看自己的 PeerID 和地址

对于 CID 版本兼容性,保持一致性:NFT 场景推荐使用 CIDv0(Qm...),因为兼容性最广——大多数钱包和市场工具默认使用 CIDv0。在需要 CIDv1 特性(如多哈希算法、多编解码器)时显式转换:

bash
# 将 CIDv0 转为 CIDv1
ipfs cid base32 QmXxXxXx...
# 输出: bafybei...

7. 技术要点

要点说明
内容寻址 (CID)CID = hash(content) — 相同内容 = 相同地址
CIDv0 vs CIDv1CIDv0: Qm... (Base58, sha256), CIDv1: b... (Base32, 支持多种哈希)
Pin vs GCPin = 手动标记永久保留;无 Pin 内容在 GC 周期 (默认 1h) 被清除
IPNS可变名称系统 → 指向不可变内容的固定标识符 (/ipns/key)
公共网关https://ipfs.io/ipfs/CID — 中心化网关,无需本地节点
本地网关http://localhost:8080/ipfs/CID — 自己的节点,零依赖
冗余 pinningPinata + web3.storage + 自己的节点 = 三重保险
DHT 发现Kademlia DHT 进行对等节点发现和内容路由
Filecoin 长期存储IPFS 的数据持久性激励层(Filecoin 上的交易记录在链上)
Kubo (go-ipfs)IPFS 的 Go 参考实现,最成熟和最广泛使用

Built with AiAda