JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

NFT 究竟指向什麼:中繼資料、IPFS 與全鏈上藝術

大多數 NFT 根本沒把藝術品存在鏈上——它們存的是一個指標。順著這個指標從 tokenURI 走到 JSON、再走到圖片,弄懂 IPFS 連結為何無法說謊、卻仍可能消失,並看清把藝術完全放上鏈究竟需要什麼。

藝術家把所有 NFT 變成地毯的那一天

2021 年初,一個名為 neitherconfirm 的系列背後的藝術家,做了一件安靜卻具毀滅性的事:一夜之間,系列裡的每一件作品都被換成一張普通地毯的照片。收藏者依然持有他們的代幣——區塊鏈上「誰擁有什麼」的紀錄從未被動過。改變的是每個代幣所指向的東西。藝術家是故意的,為的是把一個多數買家從未想過的事實狠狠釘進腦海:絕大多數 NFT 根本沒有把藝術品存在區塊鏈上。你擁有的,是一張無法偽造的鏈上收據,它透過一個普通的網址,指向某台伺服器上的一張圖片。

把一個 NFT 想成四層堆疊,並留意其中真正有保證的有多少。第一層是代幣本身:ERC-721 合約裡的一筆紀錄,寫著 tokenId 7 屬於 0xAlice。它存在鏈上,是真正不可竄改的。第二層是 `tokenURI`——合約回傳的一個字串,指向某處。第三層是中繼資料:一份 JSON 文件,含有名稱、描述,以及最關鍵的 `image` 欄位。第四層才是媒體本身——那張 PNG、SVG 或 MP4。只有第一層保證在鏈上;第二到第四層可以放在任何地方,而正是這裡,藏著每一個儲存決策,也藏著每一個風險。

順著指標走:tokenURI 與那份 JSON

ERC-721 的中繼資料擴充只多了一個函式:`tokenURI(uint256 tokenId)` 回傳一個字串 URI。在鑄造時,合約會指派新的 tokenId;要找它的中繼資料,就呼叫這個函式。最常見的實作,只是把一段儲存的 base URI 接上 token id。

// ERC-721 metadata extension: tokenURI is just a string POINTER.
function tokenURI(uint256 tokenId) public view returns (string memory) {
    require(_ownerOf(tokenId) != address(0), "nonexistent token");
    // _baseURI might be "ipfs://bafybeigd.../"  ->  ".../7"
    return string.concat(_baseURI, Strings.toString(tokenId));
}
tokenURI 回傳一個字串——一個指標。合約並不儲存圖片,它儲存的是「去哪裡找」。

那個字串通常會解析成一份 JSON 文件,遵循事實上的中繼資料標準(ERC-721 的結構,加上 OpenSea 那套被廣泛採用的擴充欄位)。錢包與市集會抓取這份 JSON、渲染 `image`,並把 `attributes` 變成你瀏覽時看到的稀有度篩選條件。

{
  "name": "Glyph #7",
  "description": "An on-chain generative artwork.",
  "image": "ipfs://bafybeigdyrzt5.../7.png",
  "external_url": "https://example.art/7",
  "attributes": [
    { "trait_type": "Background", "value": "Cobalt" },
    { "trait_type": "Eyes", "value": "Laser" },
    { "display_type": "number", "trait_type": "Generation", "value": 2 }
  ]
}
ERC-721 的中繼資料 JSON。`image` 不過是另一個網址,由市集去抓取並快取。

ERC-1155 用更有效率的方式做同一件事。它的 `uri(uint256 id)` 回傳一個範本,裡頭含有字面字串 `{id}`,由用戶端把 `{id}` 換成補零、小寫、64 位元的十六進位 token id。於是一個像 `https://game.example/api/{id}.json` 的範本,就能服務整套道具目錄——完全不必逐一儲存。

內容定址:本身就是校驗碼的連結

你一再看到的那些 `ipfs://...` 連結,並不是位置——它們是指紋。一個普通的 HTTP 網址告訴你去哪裡找:哪台伺服器、哪條路徑;那台伺服器今天能回傳任何東西,明天又能換成別的。IPFS 把這套模型翻轉過來:位址本身就是內容的密碼雜湊。向網路要 `bafybei…`,任何持有相符位元組的節點都能提供;而你只要自己重新雜湊一次就能驗證答案。這就叫做內容定址

一個 CID(內容識別碼)包著一個 multihash:前面幾個標頭位元組標明所用的雜湊函式與摘要長度,後面接著摘要本身。以 SHA-256 來說,標頭位元組是 `0x12`(函式代碼)與 `0x20`(32 位元組長度),然後是 32 位元組的摘要;再加上 CID 版本與編碼器,把整串做 base 編碼,就得到 `Qm…`(CIDv0,base58)或 `bafy…`(CIDv1,base32)。

  1. 取出檔案的位元組。大檔會被切成約 256 KB 的區塊,以 Merkle DAG 串接(和 Merkle 樹同樣是「雜湊的雜湊」概念);根區塊的雜湊就成為 CID。
  2. 用 SHA-256 雜湊(根)區塊,得到 32 位元組摘要。
  3. 在前面加上 multihash 標頭位元組 `0x12 0x20`,再加上 CID 版本與編碼器。
  4. 把結果做 base 編碼 → `ipfs://bafybei…`。
  5. 任何人下載內容後,重跑步驟 1–4,核對 CID 是否相符。若不符,位元組就是錯的——拒收。

這就是為什麼一條 IPFS 連結在物理上無法說謊。改動一個像素,檔案的位元組就變了,SHA-256 摘要就變了,CID 也跟著變。原本的 CID 仍只指向原本的位元組——你沒有任何辦法讓 `bafybei…X` 解析成除了「產生它的那份內容」以外的東西。內容定址的媒體,繼承了與鏈本身相同的防竄改性:這是靠雜湊、而非靠承諾達成的不可竄改性

沒人提的陷阱:IPFS 不等於永遠

內容定址保證的是:如果你找得到位元組,它們就是真品。它完全不保證還有沒有人在儲存它們。IPFS 節點只保留自己選擇要留的資料,其餘的會被垃圾回收,所以你的檔案能活多久,完全取決於還有沒有節點釘選(pin)它——`pin` 就是「永遠別垃圾回收這個」的指令。一旦最後一個釘選它的節點離線、又沒有別人快取過這份內容,它就這麼消失了:CID 仍然「解析得出來」,卻指向虛無。

這道缺口,正是釘選服務與持久層要填補的。Pinata 與 web3.storage/nft.storage 會幫你把 CID 持續釘選;有些還會把資料推進 Filecoin,後者額外提供儲存證明的密碼學證據,證明某個供應商在付費合約下仍持有你那一模一樣的位元組。Arweave 走的是完全不同的路:付一次費,靠捐贈基金模型資助複製約兩百年——也就是用 `ar://` 定址的「永久網」。共同的重點是:永久性是一項有人付錢的服務,而不是你上傳到 IPFS 就免費獲得的性質。

更尖銳的危險,是那種樸素的 HTTPS 指標。如果 `tokenURI` 回傳 `https://api.coolproject.io/meta/7`——或者 JSON 的 `image` 欄位是這種網址——那麼決定你的代幣顯示什麼的,就是一台你無法掌控的伺服器。團隊可以換掉藝術品、網域可以到期、公司可以倒閉,而你那個「不可竄改」的 NFT 就靜悄悄地渲染出一個破圖示。這正是開頭 neitherconfirm 的示範,也是「真正的保證」與「行銷口號」之間的全部差別。

  1. 區塊瀏覽器上,呼叫 `tokenURI(tokenId)`(ERC-1155 則是 `uri(id)`)。
  2. 若它以 `https://` 開頭,藝術品就是中心化且可變的——問問誰在運行那台伺服器、他們收手時會怎樣。
  3. 若是 `ipfs://<cid>`,抓下 JSON,確認 `image` 欄位是 `ipfs://`,而不是藏在下一層的 `http` 網址。
  4. 確認真有可信的對象在釘選這份內容(專案自己的節點、nft.storage、Filecoin 合約、Arweave)。沒人釘選的 CID,就是隨時會消失的 CID。
  5. 問問合約擁有者是否還能更改 base URI。一次靠替換 base URI 的「揭曉」,就證明擁有者有能力再替換一次。

全鏈上:活在合約裡的藝術

要刪掉每一個外部依賴,唯一的辦法就是把藝術品放在鏈上。`tokenURI` 根本不必回傳一個連結——它可以回傳一個 data URI,把 JSON、以及裡頭的圖片,直接編碼進回傳的字串裡。什麼都不必去別處抓。外層格式是 `data:application/json;base64,<base64-json>`,而那份 JSON 裡的 `image` 值本身,就是 `data:image/svg+xml;base64,<base64-svg>`。

// Fully on-chain: the token IS its own metadata + image. No server, no IPFS.
function tokenURI(uint256 id) public pure returns (string memory) {
    string memory svg = string.concat(
        '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320">',
          '<rect width="320" height="320" fill="#', _bg(id), '"/>',
          '<text x="160" y="172" font-size="48" text-anchor="middle" fill="#fff">#',
            Strings.toString(id),
          '</text>',
        '</svg>'
    );
    string memory json = string.concat(
        '{"name":"Glyph #', Strings.toString(id), '",',
         '"description":"Fully on-chain. No IPFS, no server.",',
         '"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
    );
    return string.concat(
        "data:application/json;base64,",
        Base64.encode(bytes(json))
    );
}
全鏈上 NFT 在 view 函式裡組出自己的 SVG 與 JSON,並以 base64 data URI 回傳——任何錢包都讀得懂,且不依賴任何鏈下事物。(使用 OpenZeppelin 的 Base64 與 Strings。)

誠實的代價是成本。鏈上儲存貴得驚人:寫入一個全新的 32 位元組槽位約需 20,000 gas(`SSTORE`),所以一張幾 KB 的圖片就要動用數百個槽位、燒出一張令人咋舌的 gas 帳單。這就是為什麼鏈上專案,要嘛在讀取時用一顆極小的種子生成藝術品,要嘛以高度壓縮的形式儲存。Autoglyphs(Larva Labs,2019)是第一個全鏈上藝術:合約用一段簡短演算法畫出每個字形,每個代幣幾乎什麼都不必存。Nouns 把每個角色存成「以共用調色盤為基礎的行程長度編碼像素」,再於合約內組出 SVG。就連 CryptoPunks 起初也只是一半在鏈上——2017 年的合約存了合成圖的雜湊作為真實性證明,而圖片本身存放在鏈下,直到 2021 年才完全搬上鏈。

證明它從未改變——以及買家的兩分鐘檢查清單

即使媒體在鏈下,你仍能在任何人有機會動手腳之前,靠把一個雜湊承諾上鏈,來把不可竄改性釘死。Bored Ape Yacht Club 公布了一份出處紀錄(provenance record):它把 10,000 張圖片每一張的 SHA-256 雜湊,串接成一條長字串,再把那條字串雜湊一次,並在揭曉之前把結果存上鏈。再搭配一個隨機起始索引(由未來某個區塊推導而來),這就證明了團隊事後既無法重新排序整個系列、也無法替換任何一張圖——今天任何人都能重新雜湊那些圖片,核對承諾是否依然相符。這就是把一個「看似可變」的指標,變成一個「可驗證的承諾」的方法。

把這一切串起來,你就能直接從任何系列的合約,讀出它真正的耐久度。像 ERC-721 這樣的代幣標準只標準化了那個指標;耐久度則是團隊另外做了、或沒做的一個刻意選擇。大致由強到弱排序如下:

  1. 全鏈上(SVG/生成式): 最強——除非改寫整條鏈,藝術品不可能消失或改變。
  2. IPFS/Arweave 並附上已承諾的 CID 與真實釘選: 強——內容定址、可驗篡改,只要還有人讓位元組活著。
  3. IPFS 但未釘選,或只靠單一閘道網址才連得到: 脆弱——找得到時是真品,但隨時可能消失。
  4. 中心化 HTTPS 伺服器: 最弱——可變,且耐久度只等同於一間公司加一次網域註冊。