---
title: "Solana 歸檔資料：如何查詢完整區塊與交易歷史"
description: "說明 Solana 歸檔資料：節點為何會修剪歷史資料、哪些 RPC 方法需要歸檔存取權限，以及如何大規模查詢完整區塊與交易歷史。"
---

# Solana 歸檔資料：如何查詢完整區塊與交易歷史

<ImageBlock
  src="https://media.alchemy.com/blog/solana-archival-data-hero.png"
  alt="Solana 存檔資料：查詢完整區塊與交易紀錄"
  width={1920}
  height={900}
  priority
/>

每個索引 Solana 的團隊遲早都會撞上同一道牆。你向節點請求幾個月前的交易，得到的是「block cleaned up」錯誤，因為節點早在幾週前就刪掉了那段帳本。標準的 Solana RPC 節點只保留[大約最近幾天的鏈上資料](https://docs.anza.xyz/implemented-proposals/rpc-transaction-history)，其餘的都會被修剪掉以控制磁碟用量。考慮到 Solana [尖峰時每年產生超過四 PB 的資料](https://www.alchemy.com/blog/zero-downtime-zero-gaps-solana-grpc-streaming)，本來就不會有單一機器能存下全部內容。

archival data 就是用來取得節點已丟棄資料的方式。在談其他之前，先把一個定義弄清楚：Solana 的 archival data 指的是舊區塊與交易，而不是過去的帳戶狀態。如果你是從 Ethereum 過來的，在那裡 archive node 可以回報任何帳戶在任何歷史區塊的餘額,這個差異在你第一次照著一個這裡根本不存在的方法設計 pipeline 時,就會產生影響。剩下的都是實務問題：Solana 的完整歷史實際存放在哪裡、哪些 RPC 方法能取得它、如何查詢，以及如何從創世區塊回填索引而不留下缺口。

## 為什麼標準 Solana 節點無法提供完整歷史資料？

validator 會把帳本寫入本地資料庫，operator 通常搭配 [`--limit-ledger-size` 旗標](https://docs.anza.xyz/operations/guides/validator-start)執行,以避免磁碟被塞滿。設了這個旗標之後,節點會優先清除最舊的資料。它的預設值是兩億個 shred（Solana 為了在網路上傳播區塊而切分出的區塊碎片）,這能讓帳本控制在大約 500 GB 以內。若不設這個旗標,節點會保留所有收到的資料,直到磁碟用盡,以 Solana 的資料產出速度來說,那不會等太久。

Anza 是維護 [Agave validator client](https://docs.anza.xyz/implemented-proposals/rpc-transaction-history) 的團隊，他們對原因說得很直白：六個月的交易資料實際上無法儲存在 validator 的本地帳本裡,所以節點所帶的歷史資料「大概只有幾天的量級」。更早的資料都得存放在別處。

你可以呼叫 [`minimumLedgerSlot`](https://www.alchemy.com/docs/reference/solana-api-quickstart) 找出任何節點的下限,它會回傳該節點仍保有的最舊 slot。持續觀察一段時間會發現這個數字只會往上升,因為修剪永遠不會停止。查詢低於這個值的資料,你得到的不是資料,而是「block cleaned up」錯誤。更大的磁碟也改變不了這一切。提供鏈頂資料與提供深度歷史資料是不同的基礎設施問題,而 archival 系統之所以存在,正是因為後者已經超出單一節點能負荷的範圍。

## Solana 上的 archival 是什麼意思,和 Ethereum 有何不同？

[Ethereum archive node](https://www.alchemy.com/overviews/archive-nodes) 保留了 state trie 的每一個歷史版本。你可以查詢某個合約的儲存內容或某個錢包在過去任一區塊時的餘額,節點會用它專門為此保留的資料來回答。從 Ethereum 過來的團隊往往預設 Solana 也有對應的機制。事實上並沒有,而這個假設所造成的歷史資料規劃失誤,比其他任何原因都多。

Solana 是就地覆寫帳戶狀態。當一個帳戶發生變化時,新版本會取代舊版本,而 [AccountsDB 中的背景清理程序](https://www.anza.xyz/blog/a-deep-dive-into-solana-s-accountsdb)會在後續 slot 完成 finalize 之後,回收被取代掉的舊版本。沒有任何紀錄會留下一個帳戶三個月前持有的內容。這就是為什麼 Solana RPC API 沒有「slot N 時的餘額」這類方法,也是為什麼[查詢歷史帳戶狀態](https://github.com/solana-labs/solana/issues/18197)這項功能請求在 Solana repo 裡掛了好幾年都還沒解決。

archival 基礎設施保留的是帳本本身,也就是區塊以及其中的交易。archive 可以給你第 150,000,000 號區塊,或某地址曾經涉及的每一筆交易。但它無法告訴你某個錢包去年三月的 USDC 餘額。這個問題仍然可以回答,但答案來自透過 [indexer](https://www.alchemy.com/overviews/blockchain-indexer) 重播該錢包的交易歷史,而不是向節點詢問它從未保留過的狀態。

## 哪些 RPC 方法需要 archival data？

一旦某個方法所查詢的 slot 低於節點的本地下限,這個方法就變成一次 archival 讀取。以下是會觸及長期儲存的方法。

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "Method", dataType: "object" },
      { key: "2", width: 260, title: "What it returns", dataType: "object" },
      { key: "3", width: 300, title: "Limit to know", dataType: "object" },
    ],
    data: [
      {
        "1": {
          title: "<p><strong>getBlock</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>某個 slot 的完整區塊及其交易</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title: "<p>只接受 maxSupportedTransactionVersion: 0</p>",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        "1": {
          title: "<p><strong>getTransaction</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>依 signature 查詢一筆已確認的交易</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title:
            "<p>拒絕 processed commitment；查無結果則回傳 null</p>",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        "1": {
          title: "<p><strong>getSignaturesForAddress</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>與某地址相關的 signature,由新到舊排列</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title:
            "<p>每次呼叫限 1 至 1,000 筆；用 before 與 until 分頁</p>",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
      {
        "1": {
          title: "<p><strong>getBlocks</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>某個範圍內已確認的 slot</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title: "<p>範圍上限為 500,000 個 slot</p>",
          tooltip: "",
          icon: "",
        },
        id: 3,
      },
      {
        "1": {
          title: "<p><strong>getBlockTime</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>某區塊的預估產出時間</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title: "<p>若未記錄時間戳記則回傳 null</p>",
          tooltip: "",
          icon: "",
        },
        id: 4,
      },
      {
        "1": {
          title: "<p><strong>getFirstAvailableBlock</strong></p>",
          tooltip: "",
          icon: "",
        },
        "2": {
          title: "<p>儲存空間中可取得的最低 slot</p>",
          tooltip: "",
          icon: "",
        },
        "3": {
          title: "<p>這是 archive 的下限,而非節點本身的下限</p>",
          tooltip: "",
          icon: "",
        },
        id: 5,
      },
    ],
  }}
/>

大多數歷史資料 pipeline 都建立在其中兩個方法上。用 `getSignaturesForAddress` 向前翻頁取得某地址的歷史紀錄,再用 `getTransaction` 取得每筆交易的詳細內容。由於 signature 查詢每次呼叫最多只回傳 [1,000 筆結果](https://www.alchemy.com/docs/reference/solana-api-quickstart),要把一個活躍地址的歷史一路回溯到第一筆交易,需要一長串分頁呼叫,而其中幾乎每一筆低於節點最小帳本 slot 的呼叫,都是由 archive 提供服務。

這也是薄弱的 archive 會露出馬腳的地方。如果供應商的長期儲存有缺口,回填過程中深入某處的某次呼叫就會回傳「slot skipped」或「missing in long-term storage」之類的錯誤,如果你沒有檢查這種情況,你的索引就會在沒人察覺的情況下少了資料。每個供應商都會提供相同名稱的方法。真正有差異的是背後的儲存有沒有缺口,以及當你一次分頁翻查數千次呼叫時,回應速度有多快。

## 如何查詢完整的區塊與交易歷史？

查詢本身就是一般的 JSON-RPC。沒有獨立的 archival API,也沒有能解鎖歷史資料的特殊參數。你呼叫的是和查詢鏈頂時一樣的方法,只要 slot 低於節點的本地下限,供應商的 archive 就會負責回應。

從深度歷史中取出一個區塊看起來像這樣：

<CodeSnippet
  language="bash"
  theme="dark"
  code={`curl https://solana-mainnet.g.alchemy.com/v2/YOUR_API_KEY \\
  -X POST \\
  -H "Content-Type: application/json" \\
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getBlock",
    "params": [150000000, {
      "maxSupportedTransactionVersion": 0,
      "transactionDetails": "full",
      "rewards": false
    }]
  }'`}
/>

回應內容包含整個區塊、其中的每一筆交易,以及每筆交易的狀態與 metadata。務必在每次呼叫的 params 中保留 `maxSupportedTransactionVersion: 0`。少了它,任何含有 versioned transaction 的區塊都會報錯,而在 mainnet 上大多數區塊都是如此。

走訪一個地址的完整歷史,就是把上面表格中的兩個方法組成迴圈執行。用 `getSignaturesForAddress` 向前翻頁,直到回傳結果為空,再取得每筆交易的詳細內容：

<CodeSnippet
  language="typescript"
  theme="dark"
  code={`import { createSolanaRpc, address, type Signature } from "@solana/kit";
const rpc = createSolanaRpc(
  "https://solana-mainnet.g.alchemy.com/v2/YOUR_API_KEY"
);
const target = address("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4");
let before: Signature | undefined;
const signatures: Signature[] = [];
while (true) {
  const page = await rpc
    .getSignaturesForAddress(target, { before, limit: 1000 })
    .send();
  if (page.length === 0) break;
  signatures.push(...page.map((entry) => entry.signature));
  before = page[page.length - 1].signature;
}
// Resolve each signature to full transaction detail
const tx = await rpc
  .getTransaction(signatures[0], {
    maxSupportedTransactionVersion: 0,
    encoding: "jsonParsed",
  })
  .send();`}
/>

對單一錢包來說,這個迴圈就足夠了,而且在筆電上就能順利跑完。但當這個地址是一個有數百萬筆 signature 的活躍 program,或者你需要鏈上每個地址的歷史資料時,這個做法就不夠用了。這就是回填問題,以下段落會說明資料實際存放在哪裡,以及如何無缺口地載入它。

## Solana 的完整歷史資料實際上是怎麼儲存的？

沒有 validator 保有整條帳本,所以長期歷史資料完全存放在節點之外。實務上有兩套系統在處理這件事。

歷史較久的做法是倉儲模式。一個專用節點會持續把已 finalize 的區塊上傳到 [Google Bigtable](https://docs.anza.xyz/operations/setup-an-rpc-node),當 RPC 節點收到它已不再保有的 slot 查詢時,就會回退到那個儲存空間。近期的 slot 來自節點的本地資料庫,更早的則來自 Bigtable。多年來,大多數正式環境的 Solana RPC 都是用這種方式提供深度歷史資料。代價是集中化,因為整條鏈的過去最終都存放在單一家專屬雲端資料庫裡。

較新的做法是 [Old Faithful](https://old-faithful.net/),由 Triton 與 Yellowstone 專案主導的開源 archive。它把從創世區塊以來的每一個區塊,打包成 content-addressed 的 CAR 檔案,分散存放在 IPFS、Filecoin 與相容 S3 的儲存空間上,並透過標準的 Solana JSON-RPC 與 gRPC 提供服務。這個專案的存在,是為了讓完整的 Solana 歷史不必依賴任何單一公司的資料庫,而需要可驗證、從創世區塊起完整涵蓋的團隊,會把它當作參考用的 archive。

無論你的供應商背後用的是哪一套系統,該記住的重點是:archive 是與節點分開的獨立系統。深度與完整性是這套系統的屬性,值得直接詢問,而不是憑假設。

## 如何在規模化的情況下取得帳戶與代幣歷史資料？

想要「規模化 Solana 歷史資料」的團隊,很少是指原始區塊。他們想要的是某個錢包隨時間變化的餘額、某個地址發生過的每一筆轉帳,或某個代幣的完整持有者歷史,而且要快到足以支撐儀表板或報稅匯出。這些內容在帳本裡都不是以可查詢的形式存在,必須推導出來。

各專案的做法幾乎一樣。從 archival RPC 取得某地址完整的交易歷史,依序重播這些交易,並在過程中計算你關心的狀態:每筆交易後的餘額、所有權變化、轉帳流向。把結果寫進你自己的資料庫,讓昂貴的重播只需執行一次,而不是每次請求都重跑一遍。在任何鏈上,這都是 blockchain indexer 的工作。在 Solana 上,這是取得歷史狀態的唯一途徑,因為節點什麼都沒保留。

針對最尖銳的那個問題,也就是某個錢包在特定 slot 時持有什麼,我們現在直接提供答案。[`getTokenAccountsByOwnerAtSlot`](https://www.alchemy.com/docs/chains/solana/solana-api-endpoints/get-token-accounts-by-owner-at-slot) 保留了標準 `getTokenAccountsByOwner` 的語法,並新增一個 `slot` 參數,單次呼叫就能回傳該錢包在該歷史時間點的精確代幣餘額。它背後是持續維護的歷史索引,而非重播,所以對於某個時間點的持有量查詢,上面整套重建 pipeline 都不再需要。[historical Solana token balances 發表文章](https://www.alchemy.com/blog/historical-solana-token-balances)說明了背後的運作機制。

至於其他常見的推導型資料,例如隨時間變化的餘額、轉帳紀錄與代幣 metadata,[Data APIs](https://www.alchemy.com/docs/data) 會提供預先計算好的結果,你可以省下整套索引專案。當你需要自訂的推導資料或想完全掌控 pipeline 時,再自己建 indexer。當標準的資料形式已經夠用時,就用託管方法。唯一行不通的是向一般節點詢問過去的帳戶狀態,因為節點從來沒有保留過。所有有效的答案,都是建立在歷史資料之上的索引。

## 如何從創世區塊回填索引而不出問題？

歷史 pipeline 最難處理的部分是冷啟動。你的索引是空的,有數億個 slot 需要載入,而在載入的整個過程中,鏈上還在不斷產生新區塊。如果回填與即時資料流沒有乾淨地銜接,兩者交界處就會出現缺口。

不少團隊一開始會用輪詢 archival RPC 來完成整個回填,到創世區塊規模時就會變得很痛苦:rate limit、數億個 slot 累積下來的單次請求成本,以及沒有乾淨的方式能證明沒有遺漏任何資料。真正能在正式環境中撐下去的做法,是每個階段用不同的資料來源。大量的歷史資料直接來自 archive,像 [Jetstreamer](https://github.com/anza-xyz/jetstreamer) 這類工具能直接從 Old Faithful 串流出來。鏈頂資料則來自即時串流,而不是再多輪詢一次。

至於即時的那一半,相容 Yellowstone 的 [Solana gRPC stream](https://www.alchemy.com/solana-grpc) 會在新交易與帳戶更新發生時,依帳戶、program 或 signature 篩選後推送給你的 indexer。回填與串流之間的接縫,通常是 pipeline 出現漏洞的地方,而 replay 正是用來補起這道縫的機制。[我們的 gRPC](https://www.alchemy.com/blog/introducing-alchemy-solana-grpc) 讓 client 能帶著 `from_slot` 參數重新連線,重新接收它斷線期間錯過的 slot,這樣一次斷線就不會在你的資料中留下缺口。我們也把[串流層設計成能撐過 failover](https://www.alchemy.com/blog/zero-downtime-zero-gaps-solana-grpc-streaming) 而不遺漏訊息,否則這項工作就得另外建一套獨立於你 indexer 之外的缺口偵測服務。archive 涵蓋過去、串流涵蓋鏈頂之後,replay 就負責把兩者縫合在一起。

## 挑選 Solana archival 供應商時該注意什麼？

深度是第一件要確認清楚的事,值得用這樣的問法直接詢問任何供應商:你們是從創世區塊開始索引,還是從某個較近的高度開始往後索引?標榜「完整歷史資料」卻不明說下限的情況很常見,而你通常是在回填卡住的那一刻才發現這個下限。

其餘的檢查項目,都源自於回填實際運作的方式。完整性很重要,因為一個缺口就會讓你建立在它之上的整個索引出錯。速度很重要,因為完整走訪一次 signature 歷史需要成千上萬次循序的 archive 讀取,單次呼叫的延遲會被放大成數小時甚至數天的實際等待時間。標準 JSON-RPC 很重要,因為專屬的歷史查詢端點會把你的 pipeline 綁死在單一供應商身上,而一般 RPC 完全不需要改寫程式碼。價格也很重要,因為深度歷史資料本質上就是讀取密集型的。

我們就是依照這份檢查清單來打造 archival 存取能力的。它涵蓋[從創世區塊起完整的區塊與交易歷史](https://www.alchemy.com/docs/reference/solana-api-quickstart),並透過標準 JSON-RPC 提供服務,而[我們的 benchmark](https://www.alchemy.com/blog/solana-infrastructure) 顯示歷史 `getTransaction` 查詢速度比其他供應商快上最多 20 倍,`getBlock` 快上最多 3 倍,像 `getProgramAccounts` 這類負載較重的呼叫則快上最多 10 倍,而且完全不需要改寫程式碼或使用專屬方法。[我們如何打造 Solana 上最快 archival 方法的技術文章](https://www.alchemy.com/blog/how-alchemy-built-the-fastest-archival-methods-on-solana)說明了這些數字背後的架構。若想全面比較深度、可用性、價格與工具支援,[九大 Solana RPC 供應商決策指南](https://www.alchemy.com/overviews/solana-rpc)涵蓋了整個市場。無論你選哪一家,在開始回填之前,務必先取得書面確認的創世深度與完整性答案。

## 在 Alchemy 上建立你的 Solana 歷史資料 pipeline

一套能運作的歷史 pipeline 需要兩樣東西:一個深到能從創世區塊回填的 archive,以及一個快到能跟上鏈頂的串流。這兩者都是 [Alchemy 的 Solana 平台](https://www.alchemy.com/solana)的一部分。archival 讀取涵蓋透過標準 JSON-RPC 提供的完整區塊與交易歷史,所以把你現有的 Solana client 指向 Alchemy 端點,就是整個遷移過程的全部。[Solana gRPC streaming](https://www.alchemy.com/solana-grpc) 相容 Yellowstone,支援斷線重連後的 replay,採用隨用隨付計價,每 TB $75,沒有每月最低消費,也不需要任何方案門檻。

從免費方案開始,不需要合約,也不需要業務通話。正在建構 Solana 基礎設施的團隊,也可以透過我們的 [$20M Solana Fund](https://www.alchemy.com/solana-20m-fund) 申請最高 $25,000 的額度。等你的 pipeline 上線之後,節點丟棄的歷史資料,就不再是你的問題了。
