---
title: "Solana Account Archive：在任何 slot 查詢帳戶狀態"
description: "透過 getAccountInfo 查詢任何 Solana 帳戶在任何歷史 slot 的狀態，此永不清除的檔案庫是透過重播一整年的 Solana 資料建構而成。"
---

# Solana Account Archive：在任何 slot 查詢帳戶狀態

<ImageBlock
  src="https://media.alchemy.com/solana-account-archive.png"
  alt="Solana 帳戶封存：查詢任意 slot 的帳戶狀態"
  width={1920}
  height={900}
  priority
/>

Account Archive 可以回答 `getAccountInfo`，查詢任何 Solana 帳戶在任何歷史 slot 的狀態（vote 帳戶與 per-slot sysvars 除外），涵蓋範圍會不斷擴展且永不清除。使用 `slot` 參數讀取某個時間點的狀態，或用 `lastUpdateBeforeSlot` / `firstUpdateAfterSlot` 走訪帳戶的更新歷史。讀取延遲中位數落在微秒等級。

## Solana 只記得現在

關於 Solana 有個細節，連經驗豐富的開發者都會感到意外：validator 只保存每個帳戶的一份副本，也就是最新的那份。每次寫入都會就地覆蓋先前的狀態，所以帳戶一旦被寫入，先前的樣子就消失了。即便節點仍保有某個 slot 的完整區塊，該 slot 當時的帳戶狀態也早已不存在於機器上任何地方。

這是設計上的特性，不是缺陷，也是 Solana 快速的原因之一。但這代表一整類合理的問題都無法回答，例如：

- **除錯**：「我們的清算在 slot 285,401,337 觸發。那個確切的 slot 上，oracle price 帳戶長什麼樣子？」
- **回測**：「重建這個 pool 在過去六個月每個時間點的儲備量。」
- **稽核**：「證明這個 token 帳戶在 3 月 3 日持有的內容。」
- **復原**：「我們的索引器停機了四小時，重建我們錯過的確切狀態變化。」

你可能會以為 `getBlock` 可以處理這些問題，但它辦不到。它回傳的是過去某個 slot 的交易以及交易前後的餘額，而不是帳戶資料。對大多數應用程式而言，真正重要的狀態存在於資料位元組裡：訂單簿、價格資訊、部位，或設定。

Solana Account Archive 把完整的歷史找回來了。它可以回答 `getAccountInfo`，查詢自 2025 年 7 月以來任何帳戶在任何 slot 的狀態，使用你已經熟悉的請求與回應格式。有兩點與眾不同：第一，這個 archive 永不清除資料，所以可查詢的範圍只會隨時間增加；第二，它會略過鏈上自身的 per-slot 記帳資料（vote 帳戶，以及少數每個 slot 都會被重寫的 sysvars），因為對應用程式開發者而言，那些只是雜訊，不是有用的歷史資料。

## API：getAccountInfo 的延伸

我們刻意沒有發明新的方法。這個 archive 使用標準的 JSON-RPC，並實作 `getAccountInfo`，支援你早已熟悉的所有設定選項：`encoding`（`base64`、`base58`、`base64+zstd`、`jsonParsed`）、`commitment`、`dataSlice`、`minContextSlot`，外加三個彼此互斥的新參數：

<EmbeddedTable
  table={{
    columns: [
      {
        key: "parameter",
        width: 200,
        title: "Parameter",
        dataType: "object",
      },
      {
        key: "semantics",
        width: 320,
        title: "Semantics",
        dataType: "object",
      },
      {
        key: "useItFor",
        width: 220,
        title: "Use it for",
        dataType: "object",
      },
    ],
    data: [
      {
        parameter: { title: "<code>slot</code>", tooltip: "", icon: "" },
        semantics: {
          title:
            "State as of slot <code>S</code> (inclusive: the latest write with <code>slot &lt;= S</code>)",
          tooltip: "",
          icon: "",
        },
        useItFor: { title: "Point-in-time snapshots", tooltip: "", icon: "" },
        id: 0,
      },
      {
        parameter: {
          title: "<code>lastUpdateBeforeSlot</code>",
          tooltip: "",
          icon: "",
        },
        semantics: {
          title: "The most recent write strictly before <code>S</code>",
          tooltip: "",
          icon: "",
        },
        useItFor: {
          title: "Walking an account's history backward",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        parameter: {
          title: "<code>firstUpdateAfterSlot</code>",
          tooltip: "",
          icon: "",
        },
        semantics: {
          title: "The first write strictly after <code>S</code>",
          tooltip: "",
          icon: "",
        },
        useItFor: {
          title: "Walking an account's history forward",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
    ],
  }}
/>

這三個參數也和 `minContextSlot` 互斥；若與其中任一參數搭配使用會被拒絕。三者都省略的話，就會得到一般的最新狀態 `getAccountInfo`；這個 archive 是既有功能的超集，可直接替換使用。以下是一個完整的定點時間讀取範例：查詢某個 token 帳戶在 slot 400,000,000 時的狀態：

<CodeSnippet
  language="bash"
  code={`curl $ARCHIVE_URL -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getAccountInfo",
  "params": [
    "3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa",
    { "encoding": "jsonParsed", "slot": 400000000 }
  ]
}'`}
/>

回應採用標準的 `getAccountInfo` 格式：

<CodeSnippet
  language="json"
  code={`{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "context": { "apiVersion": "4.1.0", "slot": 400000000 },
    "value": {
      "lamports": 2040821,
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "data": {
        "program": "spl-token",
        "parsed": {
          "info": {
            "isNative": false,
            "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
            "owner": "7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE",
            "state": "initialized",
            "tokenAmount": {
              "amount": "1056153754430274",
              "decimals": 6,
              "uiAmount": 1056153754.430274,
              "uiAmountString": "1056153754.430274"
            }
          },
          "type": "account"
        },
        "space": 165
      },
      "executable": false,
      "rentEpoch": 18446744073709551615,
      "space": 165
    }
  }
}`}
/>

注意這裡的語意：`slot: S` 代表「截至 `S` 時的狀態」。如果該帳戶最後一次被寫入是在 slot `S - 40,000`，之後就沒有異動，你拿到的就是那次寫入的結果——正是任何在 slot `S` 執行的程式會看到的狀態。而且 `jsonParsed` 同樣適用於歷史狀態：標準 RPC 節點所使用的同一套解碼器，會套用在歷史資料位元組上，所以即使是涵蓋範圍最早期的 token 帳戶，回傳的也是已解碼的資料，而非不透明的原始位元組。

### 走訪帳戶歷史的迭代器

`lastUpdateBeforeSlot` 與 `firstUpdateAfterSlot` 讓這個 archive 變成一個可以走訪帳戶每次狀態變化的迭代器。

`lastUpdateBeforeSlot` 會在 `context.slot` 中回傳它找到的那次寫入的實際 slot，所以每個回應都能作為下一次請求的游標：

<CodeSnippet
  language="python"
  code={`# Walk every state transition of an account, newest to oldest.
cursor = current_slot + 1
while True:
    try:
        result = rpc("getAccountInfo", [pubkey, {"lastUpdateBeforeSlot": cursor}])
    except RpcError as e:
        if e.code == -32020:           # walked past the edge of coverage; done
            break
        raise
    slot = result["context"]["slot"]   # the slot this write landed at
    handle(slot, result["value"])      # value is null if this write closed the account
    cursor = slot                      # strictly-before: no overlap, no gaps`}
/>

這個走訪過程只有一個終止點：錯誤 `-32020`，代表游標已經走出這個 archive 涵蓋範圍的邊界。過程中出現 `null` 的值，其意義和在 `getAccountInfo` 中一樣：該帳戶在那個 slot 時不存在——在這裡，是因為 `context.slot` 那次寫入把它刪除了。把那個 slot 當作游標再送回去，走訪就會繼續往該帳戶更早的歷史前進。

`firstUpdateAfterSlot` 則反過來：不是往回走，而是往前走，回傳在指定 slot 之後嚴格意義上的下一次寫入。這很適合用在索引器要從一個已知 slot 追趕進度、且需要取得每一個中間狀態、而不只是最新狀態的情境。

搭配這兩者，你可以走訪一個帳戶的整個生命週期：每一次餘額變化、每一次資料異動、每一次擁有權變更，以及各自發生的確切 slot。

## 即時記錄新的寫入

往未來記錄狀態是比較容易的部分。這個 archive 透過 Geyser 串流監聽網路，即時記錄帳戶寫入，並遵循兩條規則以維持紀錄的可信度：

只有 finalized 的 slot 才會進入 archive。Solana 在鏈尖經常分岔，一個看起來真實的區塊可能幾秒後就消失了。archive 會保留每個 slot 的更新，直到網路將其 finalize 後才寫入，所以永久紀錄裡只會包含真正發生過的事。

不會有資料被悄悄遺漏。每個 Solana 區塊都會指名它的 parent，所以任何串流漏掉的區塊（無論是小故障、重新連線，還是服務部署）都會立即被偵測到並回補。

底層的儲存層本身就是另一個值得說的故事。這裡重要的是結果：定點時間讀取快到可以放在熱路徑上，延遲中位數落在微秒等級（在我們內部服務層測得）。

## 重建過去一年的歷史

從今天開始擷取資料，可以讓你明年擁有一份有用的 archive。但我們也想要過去一年的資料，這代表要在一條不保留歷史的鏈上，重建數千萬個已經發生過的 slot 的帳戶狀態。而且資料量非常龐大：Solana 大約每 400 毫秒產生一個區塊，一天約 216,000 個 slot。一年下來大約是 7,800 萬個 slot、數千億次的帳戶更新，以及超過一 petabyte 的原始帳戶資料，每個 slot 都帶有數千筆附帶完整資料內容的帳戶寫入。

### 為什麼「單純解析交易」行不通

有個誘人的捷徑：抓取歷史區塊，解讀每筆交易對每個帳戶做了什麼。但這種做法在根本上行不通，這一點值得牢記：Solana 的交易並不是狀態變化的描述，而是一次程式呼叫。

帳戶實際發生了什麼，取決於該程式的執行過程：CPI 展開呼叫到其他程式、執行途中讀取的 sysvars、計算量計費、以及當下 runtime 的精確語意。區塊只記錄了某筆交易碰到了哪些帳戶、餘額如何變動，但真正重要的資料位元組，只能透過實際執行程式碼才能確定。

要忠實地知道一筆交易對某個帳戶做了什麼，只有一種方法：用真實的 runtime，對照它當時實際執行的真實狀態，把它執行一遍。

### Replay：重新執行一次鏈上歷史

所以這正是我們做的事。這個回補（backfill）管線本質上就是一個重新經歷歷史的 validator：

1. 從可信任的快照啟動。Solana Foundation 維護著公開的歷史快照存檔：某個 slot 上每個帳戶的完整擷取。載入 slot `A` 的快照，就能得到那一刻數億個帳戶完整且具權威性的狀態。
2. 往前重放每一個區塊。從 `A + 1` 開始，每個歷史區塊都會通過真正的 validator runtime 執行：是真實執行，不是模擬，也不是解析日誌。每筆交易都會被執行；runtime 產生的帳戶寫入會被擷取並逐 slot 建立索引。
3. 在下一個快照處停下並進行驗證。這一步讓整件事真正值得信任。當重放進行到存在下一個標準快照的 slot `B` 時，我們會把重放得到的最終狀態與該快照比對，包括 accounts lattice hash：這是網路本身計算並取得共識的、對整個帳戶集合的加密承諾。只要範圍內任何一個帳戶有任何一個位元組出現偏差，雜湊值就不會相符，該範圍就會被拒絕。我們不假設重放一定正確；每一段回補的範圍在被信任之前，都會對照共識層的事實進行檢驗。

### 舊的區塊需要對應時代的 runtime

這是讓 replay 真正困難的地方：Solana 的執行語意會隨時間變化版本。Feature gates 會在特定 epoch 啟用、計算量預算規則會改變、syscalls 會新增、邊界情況的行為也會被修正。十二個月前的交易，必須用當時該叢集實際運行的那個 runtime 來重放。如果用今天的 validator 來重放，第三步就會明確告訴你：你製造出了一段從未真實發生過的歷史。

實務上，回補機群會運行好幾個固定版本的 validator 世系，各自負責它能忠實重現的時代，並依 epoch 邊界切分工作，確保每個區塊的開頭和結尾都位在可驗證的快照上。要把 archive 的涵蓋範圍再往前延伸，主要的工作就是建立正確時代的 runtime，並負擔對應的 replay 運算成本。而且由於每個對齊 epoch 的區塊都是獨立的，歷史的回補完全可以平行進行；回補一年的資料，主要取決於同時運行多少個 replay worker。

## 持續驗證的信任

雜湊驗證的回補涵蓋了過去的資料；另一個獨立的關注點，則是服務路徑在正式環境中是否始終誠實可信。因此有一套獨立的監控機制，會透過你會使用的同一套公開 API，持續交叉驗證 archive 回應的內容是否與即時的 RPC 節點一致。正確性對我們來說至關重要，所以服務運行的每一分鐘都會重新驗證一次。這套多層次的做法——共識為根據的雜湊用於回補歷史、finalization 閘門控管鏈尖寫入、再加上持續的即時交叉驗證——確保你能夠信任這份隨時間推進的真實紀錄。

## 你可以用它來建構什麼

開頭提到的每個問題，現在都只是一次 API 呼叫：

- 「那個確切 slot 上的 oracle 帳戶長什麼樣子？」一次設定 `slot` 的 `getAccountInfo` 呼叫即可。取得某筆失敗交易在執行當下所讀取的每個帳戶的確切狀態，不必再靠截圖和猜測來重建 oracle 的輸入資料。
- 「重建這個 pool 六個月來的儲備量。」以固定的 slot 間隔取樣該 pool 帳戶，並用鏈上實際發生的資料進行回測，而不是用交易事件拼湊出來的近似值。
- 「證明這個 token 帳戶在 3 月 3 日持有的內容。」「截至 slot S，帳戶 X 確切包含了這些位元組。」這個問題現在可以回答，而且是以網路本身達成共識的雜湊為依據。
- 「重建我們索引器錯過的狀態變化。」從你最後已知的 slot 開始，用 `firstUpdateAfterSlot` 往前分頁，即可依序取得每一個中間狀態。

還有一個開頭沒問到的：完全不需要運行索引器，就能取得餘額與部位歷史。用 `lastUpdateBeforeSlot` 往回走訪一個 token 帳戶，就能得到它完整的時間軸。若想查詢某個錢包在過去某個 slot 的整體 token 持有狀況，可另外參考 [historical Solana token balances](https://www.alchemy.com/blog/historical-solana-token-balances)。

## 開始使用

Solana 之所以能保持快速，靠的是讓 validator 保持精簡、只專注於目前的狀態。歷史帳戶狀態本來就存在別處，而在此之前，整個生態系都只能想辦法繞過這個限制。Solana Account Archive 改變了這一點。透過以 finalization 為閘門的即時擷取，以及會用時代忠實的 runtime 重新執行歷史、並對照共識雜湊驗證結果的回補管線，歷史 `getAccountInfo` 就只是多一個參數的普通 RPC 呼叫：相同的方法、相同的回應格式。

在後續文章中，我們會深入介紹儲存層：一年份 Solana 帳戶寫入資料是如何組織在磁碟上，讓擷取新的更新和回答定點時間查詢都能保持快速。這個 archive 索引的所有資料都經過壓縮，至少保存 3 份副本以確保冗餘，且永不清除，所以可查詢的範圍只會持續增加。

[Get started on Solana](https://www.alchemy.com/docs/reference/solana-api-quickstart)，如果你的使用情境需要更深的歷史資料，或想在大規模場景下探索特製方案與計價，歡迎[聯絡我們](https://www.alchemy.com/contact-sales)。

## 常見問答

### 如何取得 Solana 帳戶在特定 slot 的狀態？

在設定物件中帶入該帳戶的 pubkey 以及 `slot` 參數，對 Account Archive 呼叫 `getAccountInfo`：`getAccountInfo(pubkey, { "slot": S })`。回應採用標準的 `getAccountInfo` 格式，回傳的是該帳戶截至 slot `S` 時的狀態，也就是在 `S` 或之前的最新一次寫入。

### 如何取得 Solana 帳戶的完整更新歷史？

使用游標參數進行分頁。`getAccountInfo(pubkey, { "lastUpdateBeforeSlot": S })` 會回傳在 `S` 之前嚴格意義上最近一次的寫入，並在 `context.slot` 中給出該次寫入的實際 slot；把那個 slot 當作下一個游標送回去，就能往回走訪每一次狀態變化。`firstUpdateAfterSlot` 則以相同方式往前走訪。

### slot、lastUpdateBeforeSlot 與 firstUpdateAfterSlot 有什麼差異？

`slot` 是一次定點時間讀取：截至 slot `S`（含）為止的狀態。`lastUpdateBeforeSlot` 與 `firstUpdateAfterSlot` 則是排他性的歷史游標：分別是嚴格早於或嚴格晚於 `S` 的最近一次寫入。這三者彼此互斥；若三者都省略，就會得到一般的最新狀態讀取。

### jsonParsed 編碼適用於歷史帳戶狀態嗎？

適用。標準 Solana RPC 節點所使用的同一套帳戶解碼器，會套用在歷史資料位元組上，所以 token 帳戶、mint，以及其他已知程式的帳戶，在涵蓋範圍內的任何 slot 都能被正確解析回傳。所有標準編碼都支援：`base64`、`base58`、`base64+zstd`、`jsonParsed`，以及 `dataSlice`。

### 可以查詢多久之前的資料？

目前涵蓋範圍可回溯至 2025 年 7 月，回補作業會持續將範圍往更早的時間延伸。歷史資料永不清除，所以所有已索引的內容都能持續查詢，範圍只會增加。

### 歷史資料的正確性如何獲得保證？

回補的資料範圍是透過用對應時代的 validator runtime 重新執行每一筆交易而得出，所得結果會與網路本身的共識產物進行驗證，包括來自標準快照的 accounts lattice hash。即時擷取只會提交 finalized 的 slot，並有獨立的監控機制持續交叉檢查服務回傳結果與即時 RPC 節點是否一致。

### 是否涵蓋所有帳戶？

除了純粹的 per-slot 鏈上記帳資料之外，其餘所有帳戶都涵蓋在內：vote 帳戶，以及每個 slot 都會被重寫的三個 sysvars（`SlotHashes`、`SlotHistory`、`RecentBlockhashes`）不在索引範圍內。程式帳戶、token 帳戶、mint、PDA、錢包，以及其餘的 sysvars（Clock、Rent 等）都涵蓋在內。

### 需要新的 SDK 或用戶端嗎？

不需要。任何 Solana JSON-RPC 用戶端都適用。這個 archive 實作了標準的 `getAccountInfo`，而歷史相關的參數只是既有設定物件中額外的欄位。
