---
title: "Solana Account Archive:任意のスロットでアカウント状態をクエリする"
description: "任意のSolanaアカウントに対して、過去の任意のスロットでgetAccountInfoを実行可能。1年分のSolanaをリプレイして構築した、プルーニングされないアーカイブ。"
---

# Solana Account Archive:任意のスロットでアカウント状態をクエリする

<ImageBlock
  src="https://media.alchemy.com/solana-account-archive.png"
  alt="Solanaアカウントアーカイブ: 任意のスロットでアカウント状態をクエリ"
  width={1920}
  height={900}
  priority
/>

Account Archiveは、任意のSolanaアカウントについて、任意の過去のスロット時点での`getAccountInfo`に応答します（vote accountとスロットごとのsysvarを除く）。履歴カバレッジは常に拡大し続け、一切プルーニングされません。`slot`パラメータである時点の状態を読み取るか、`lastUpdateBeforeSlot` / `firstUpdateAfterSlot`でアカウントの更新履歴をたどることができます。読み取りはマイクロ秒単位の中央値レイテンシーで応答されます。

## Solanaが記憶しているのは「現在」だけ

経験豊富なビルダーでも意外に思う、Solanaに関する事実があります。バリデーターは各アカウントについて正確に1つのコピー、つまり最新のものだけを保持しています。書き込みのたびに以前の状態はその場で上書きされ、アカウントに変更が加わった瞬間、それ以前の状態は消えてしまいます。ノードがブロック全体を保持しているスロットであっても、そのスロット時点でのアカウント状態はマシン上のどこにも存在しません。

これはバグではなく仕様です。Solanaが高速である理由の一部でもあります。しかしこれにより、以下のような至って合理的な問いの多くに答えが出せなくなります。

- **デバッグ**: 「清算がスロット285,401,337で発動した。そのスロット時点でのoracle price accountはどうなっていたか？」
- **バックテスト**: 「過去6ヶ月間、このプールの準備金を各時点で再構築したい」
- **監査**: 「3月3日時点でこのtoken accountが保有していたものを証明したい」
- **復旧**: 「インデクサーが4時間ダウンしていた。見逃した状態遷移を正確に再構築したい」

`getBlock`がこれをカバーすると思うかもしれませんが、そうではありません。これは過去のスロットのトランザクションとその前後の残高を返すものであり、アカウントデータそのものではありません。ほとんどのアプリケーションにとって重要な状態は、データバイト、つまりオーダーブック、価格フィード、ポジション、設定などの中に存在します。

Solana Account Archiveは、この完全な履歴を取り戻します。2025年7月以降の任意のスロットにおける任意のアカウントについて、既に使っているのと同じリクエスト・レスポンスの形で`getAccountInfo`に答えます。これを際立たせる特徴は2つあります。アーカイブは一切プルーニングされないため、クエリ可能な範囲は時間とともに広がる一方です。また、チェーン自体のスロットごとの簿記処理（vote accountや、毎スロット書き換えられる一部のsysvar）はスキップされます。これらはアプリ開発者にとって有用な履歴ではなく、単なるノイズだからです。

## API: getAccountInfo、その拡張

私たちはあえて新しいメソッドを発明しませんでした。アーカイブは標準のJSON-RPCを話し、`getAccountInfo`を、既知のすべての設定オプション込みで実装しています。`encoding`（`base64`、`base58`、`base64+zstd`、`jsonParsed`）、`commitment`、`dataSlice`、そして`minContextSlot`に加え、相互排他的な3つの新パラメータがあります。

<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,
      },
    ],
  }}
/>

これら3つは`minContextSlot`とも相互排他的であり、いずれかと組み合わせるとリクエストは拒否されます。3つすべてを省略すれば通常の最新状態を返す`getAccountInfo`となり、アーカイブは既存機能をそのまま包含する上位互換です。以下は、あるtoken accountのスロット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`時点での状態」を意味します。アカウントが最後にスロット`S - 40,000`で書き込まれ、それ以降変更されていない場合、返されるのはその書き込みであり、これはスロット`S`で実行されるどのプログラムから見ても同じ結果になります。そして`jsonParsed`は過去の状態に対しても機能します。標準のRPCノードが使用するのと同じデコーダーが履歴バイト列に適用されるため、カバレッジ期間の初期のtoken accountであっても、不透明なバイト列ではなくデコードされた形で返ってきます。

### アカウント履歴を辿るイテレーター

`lastUpdateBeforeSlot`と`firstUpdateAfterSlot`は、アーカイブをアカウントのあらゆる状態遷移を辿るイテレーターに変えます。

`lastUpdateBeforeSlot`は見つかった書き込みの実際のスロットを`context.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`}
/>

この辿り方には停止地点が1つだけあります。エラー`-32020`、つまりカーソルがアーカイブのカバレッジの端を越えたことを意味します。途中で`null`という値が出てくる場合、それは`getAccountInfo`において通常持つ意味と同じです。そのスロット時点でアカウントが存在しなかった、ということです。ここでは、スロット`context.slot`での書き込みによってアカウントが削除されたためです。そのスロットをカーソルとして再度渡せば、アカウントのそれ以前の生存期間を辿り続けられます。

`firstUpdateAfterSlot`は逆方向の動作をします。時間を遡るのではなく前進し、与えられたスロットより厳密に後の次の書き込みを返します。既知のスロットから追いついていくインデクサーが、最新状態だけでなく中間状態すべてを必要とする場合に最適です。

この2つを組み合わせれば、アカウントのライフサイクル全体、つまりすべての残高変化、すべてのデータ変更、すべての所有権変更を、各書き込みが実際に起きたスロットとともに辿ることができます。

## 発生した新規書き込みの記録

今後発生する状態を記録するのは、比較的簡単な部分です。アーカイブはGeyserストリームを通じてネットワークを監視し、アカウントへの書き込みが発生した時点で記録します。記録の信頼性を保つための規則が2つあります。

アーカイブに入るのはfinalizeされたスロットのみです。Solanaは先端部分で常時フォークが発生しており、数秒間は実在したように見えたブロックが、単純に消えることがあります。アーカイブは各スロットの更新をネットワークがfinalizeするまで保留するため、恒久的な記録には実際に起きたことのみが含まれます。

サイレントに失われるものは何もありません。すべてのSolanaブロックは親ブロックを指定しているため、ストリームが見逃したブロック（ちょっとした不具合、再接続、サービスのデプロイなど）は即座に検知され、バックフィルされます。

その下にあるストレージ層についてはそれだけで一つの話です。ここで重要なのは結果です。時点指定の読み取りはホットパスに置けるほど高速で、中央値レイテンシーはマイクロ秒単位です（当社の内部サービス層で計測）。

## 過去1年分の再構築

今日以降のデータを取り込むだけなら、来年には使えるアーカイブが出来上がります。しかし私たちが求めたのは過去1年分でもありました。つまり、履歴を一切保持しないチェーンにおいて、すでに起きてしまった数千万スロット分のアカウント状態を再構築することです。しかもその履歴量は膨大です。Solanaは約400ミリ秒ごとに1ブロックを生成し、1日あたり約216,000スロットになります。1年でおよそ7,800万スロット、数千億件のアカウント更新、そして1ペタバイトを超える生のアカウントデータ、各スロットには完全なデータペイロード付きの数千件のアカウント書き込みが含まれます。

### なぜ「トランザクションをパースするだけ」ではうまくいかないのか

過去のブロックを取得し、各トランザクションが各アカウントに対して何を行ったかをデコードする、という魅力的な近道があります。これが失敗するのには根本的な理由があり、理解しておく価値があります。Solanaのトランザクションは状態変化の記述ではなく、プログラムの呼び出しなのです。

アカウントに実際に何が起きるかは、プログラムの実行内容、つまり他のプログラムへと連鎖するCPI、実行途中で読み取られるsysvar、compute計測、そのスロット時点でのランタイムの正確なセマンティクスに依存します。ブロックが記録しているのは、どのアカウントにトランザクションが触れたか、残高がどう動いたかであって、あなたが本当に欲しいデータバイトはコードを実際に実行しない限り決まりません。

トランザクションがアカウントに何をしたかを正確に知る方法は1つしかありません。実際のランタイムを使い、実際に実行された当時の状態に対して、それを実行することです。

### リプレイ：チェーンをもう一度走らせる

だからこそ、私たちはそれを行います。バックフィルパイプラインは本質的に、歴史を追体験するバリデーターです。

1. 信頼できるスナップショットから起動する。Solana Foundationは過去のスナップショット、つまりある時点でのすべてのアカウントの完全なキャプチャをパブリックアーカイブとして維持しています。スロット`A`時点のスナップショットを読み込むことで、その瞬間における数億個のアカウントの完全かつ正規な状態が得られます。
2. すべてのブロックを前方向にリプレイする。`A + 1`から、各過去のブロックが実際のバリデーターランタイムを通して実行されます。これはシミュレーションでもログのパースでもない、実際の実行です。すべてのトランザクションが実行され、ランタイムが生成するアカウント書き込みがスロットごとにキャプチャ・インデックス化されます。
3. 次のスナップショットで停止し、それを証明する。この工程が全体の信頼性を担保しています。リプレイが次の正規スナップショットが存在するスロット`B`に到達すると、リプレイ後の終端状態をそのスナップショットと比較します。これにはaccounts lattice hash、つまりアカウント集合全体に対する暗号学的コミットメント（ネットワーク自身が計算し合意しているもの）も含まれます。範囲内のどこか1つのアカウントの1バイトでも食い違っていれば、ハッシュは一致せず、その範囲は棄却されます。私たちはリプレイが正しいと仮定するのではなく、バックフィルされたすべての範囲をコンセンサスの真実と照合したうえで初めて信頼します。

### 古いブロックにはその時代のランタイムが必要

リプレイを本当に難しくしているのは、次の点です。Solanaの実行セマンティクスは時間軸上でバージョン管理されています。特定のエポックで有効化されるfeature gate、変化するcompute budgetのルール、追加されるsyscall、修正されるエッジケースの挙動。12ヶ月前のトランザクションは、そのスロット時点でクラスターが実際に振る舞っていたとおりに動作するランタイムでリプレイされなければなりません。今日のバリデーターでリプレイすれば、ステップ3が「実際には起きなかった歴史を作り出してしまった」と大声で教えてくれます。

実際には、バックフィルを行うクラスターは、それぞれが忠実に再現できる時代を担当する、固定された複数のバリデーター系統を稼働させており、作業はエポック境界に沿って分割されているため、各チャンクは検証可能なスナップショットで始まり終わります。アーカイブをさらに過去へ拡張することは、基本的には適切なランタイムの時代を立ち上げ、リプレイの計算コストを支払う問題に帰着します。そして各エポック整合チャンクは独立しているため、履歴処理は完全に並列化可能です。1年分のバックフィルは、基本的にはどれだけのリプレイワーカーを同時に走らせるかという問題になります。

## 継続的に検証される信頼性

ハッシュで検証されたバックフィルが過去をカバーする一方、別の懸念として、提供経路が本番環境で正しさを保ち続けているかという問題があります。そこで独立したウォッチドッグが、あなたが使うのと同じ公開APIを通じて、アーカイブの応答をライブのRPCノードと継続的に相互検証しています。正確性は私たちにとって最優先事項であり、このサービスが稼働している限り毎分再検証されます。バックフィルされた履歴に対するコンセンサスに基づくハッシュ、先端におけるfinalization gate付きの書き込み、そしてその上に重なる継続的なライブ相互検証という多層的なアプローチにより、時間とともに進んでいく真実の記録を信頼できるようになっています。

## これで何が構築できるか

この投稿の冒頭で挙げた問いは、すべて今やAPI呼び出しになります。

- 「そのスロット時点でのoracle accountは何だったか？」`getAccountInfo`に対して`slot`を設定した1回の呼び出しで済みます。失敗したトランザクションが読み取ったすべてのアカウントの、実行時点における正確な状態を取得できます。スクリーンショットや推測からoracleの入力値を再構築する必要はもうありません。
- 「このプールの準備金を6ヶ月間にわたって再構築したい。」固定のスロット間隔でプールアカウントをサンプリングし、トレードイベントから継ぎ合わせた近似値ではなく、実際にチェーン上にあったものに対してバックテストできます。
- 「3月3日時点でこのtoken accountが保有していたものを証明したい。」「スロットS時点で、アカウントXは正確にこのバイト列を保持していた」——これは答えられる問いであり、ネットワーク自身が合意したハッシュに裏付けられています。
- 「インデクサーが見逃した状態遷移を再構築したい。」既知の最後のスロットから`firstUpdateAfterSlot`でページングし、中間状態すべてを順番に受け取れます。

そして、冒頭では触れなかったものもあります。インデクサーを一切動かさずに残高やポジションの履歴を得ることです。`lastUpdateBeforeSlot`でtoken accountを後方に辿れば、その完全なタイムラインが手に入ります。過去のスロット時点でのウォレット全体のtoken保有状況については、[historical Solana token balances](https://www.alchemy.com/blog/historical-solana-token-balances)も参照してください。

## はじめに

Solanaは、バリデーターを軽量に保ち現在の状態に集中させることで高速性を維持しています。これまで過去のアカウント状態は単に別の場所に存在しており、エコシステムはそれを回避する方法を模索してきました。Solana Account Archiveはそれを変えます。finalization gate付きのライブ取り込みと、当時のランタイムを忠実に再現しつつコンセンサスハッシュに照らして結果を証明するバックフィルパイプラインにより、過去の`getAccountInfo`はただのRPC呼び出しになります。メソッドもレスポンス形式も同じで、パラメータが1つ増えるだけです。

続編の投稿では、ストレージ層について掘り下げます。1年分のSolanaのアカウント書き込みがどのようにディスク上で整理され、新規更新の取り込みと時点指定クエリへの応答の両方を高速に保っているか、という話です。アーカイブがインデックスするすべてのデータは圧縮され、冗長性のため少なくとも3つのレプリカで保存され、決してプルーニングされません。クエリ可能な範囲は広がり続けるだけです。

[Get started on Solana](https://www.alchemy.com/docs/reference/solana-api-quickstart)、そして、より深い履歴が必要な場合や、大規模な専用ソリューションと料金体系について相談したい場合は[reach out to us](https://www.alchemy.com/contact-sales)してください。

## FAQ

### 特定のスロット時点でのSolanaアカウントの状態はどうやって取得しますか？

Account Archiveで`getAccountInfo`を呼び出し、アカウントのpubkeyとconfigオブジェクト内の`slot`パラメータを指定します: `getAccountInfo(pubkey, { "slot": S })`。レスポンスは標準の`getAccountInfo`形式で、スロット`S`時点、つまり`S`以前の最新の書き込み時点でのアカウントの状態を返します。

### Solanaアカウントの完全な更新履歴はどうやって取得しますか？

カーソルパラメータでページングします。`getAccountInfo(pubkey, { "lastUpdateBeforeSlot": S })`は`S`より厳密に前の最も新しい書き込みを、その書き込みの実際のスロットを`context.slot`に含めて返します。そのスロットを次のカーソルとして渡せば、すべての状態遷移を過去方向に辿れます。`firstUpdateAfterSlot`は同様に前方向に辿ります。

### slot、lastUpdateBeforeSlot、firstUpdateAfterSlotの違いは何ですか？

`slot`は時点指定の読み取りで、スロット`S`時点（inclusive）での状態を返します。`lastUpdateBeforeSlot`と`firstUpdateAfterSlot`は排他的な履歴カーソルで、`S`より厳密に前、または厳密に後の最も近い書き込みを返します。これら3つは相互排他的であり、すべて省略すれば通常の最新状態読み取りになります。

### jsonParsedエンコーディングは過去のアカウント状態でも機能しますか？

はい。標準のSolana RPCノードが使用するのと同じアカウントデコーダーが履歴バイト列に適用されるため、token account、mint、その他既知のプログラムアカウントはカバレッジ内の任意のスロットでパースされた形で返ってきます。標準のエンコーディング（`base64`、`base58`、`base64+zstd`、`jsonParsed`）はすべて機能し、`dataSlice`も同様です。

### どこまで遡ってクエリできますか？

現在のカバレッジは2025年7月まで遡り、バックフィルによって時間とともにさらに拡大していきます。履歴は一切プルーニングされないため、一度インデックスされたものはすべてクエリ可能なまま残り、範囲は広がり続けるのみです。

### 履歴データの正しさはどうやって保証されていますか？

バックフィルされた範囲は、該当する時代のバリデーターランタイムを通じてすべてのトランザクションを再実行することで生成され、結果として得られる状態は、正規スナップショットから得られるaccounts lattice hashを含む、ネットワーク自身のコンセンサス成果物と照合して検証されます。ライブ取り込みはfinalizeされたスロットのみをコミットし、独立したウォッチドッグが提供結果をライブのRPCノードと継続的に相互チェックしています。

### すべてのアカウントがカバーされていますか？

純粋なスロットごとのチェーン簿記処理を除くすべてのアカウントです。vote accountと、毎スロット書き換えられる3つのsysvar（`SlotHashes`、`SlotHistory`、`RecentBlockhashes`）はインデックスから除外されています。プログラムアカウント、token account、mint、PDA、ウォレット、および残りのsysvar（Clock、Rentなど）はすべてカバーされています。

### 新しいSDKやクライアントは必要ですか？

いいえ。既存のSolana JSON-RPCクライアントがそのまま使えます。アーカイブは標準の`getAccountInfo`を実装しており、履歴用パラメータは既存のconfigオブジェクトに加わる追加フィールドに過ぎません。
