---
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 在峰值速度下[每年产生超过 4 PB 的数据](https://www.alchemy.com/blog/zero-downtime-zero-gaps-solana-grpc-streaming),没有任何一台机器能扛得住全部数据。

归档数据就是用来访问那些被节点丢弃的数据的途径。在讨论其他内容之前,先明确一个定义。Solana 的归档数据指的是旧区块和交易,而不是过去的账户状态。如果你是从 Ethereum 转过来的——在那里,一个 archive node 可以查到任意账户在任意历史区块的余额——这个差异很关键,因为一旦你围绕一个 Solana 上根本不存在的方法去设计 pipeline,第一次就会踩坑。剩下的都是实操问题:Solana 的完整历史实际存放在哪里、哪些 RPC 方法能访问到它、如何查询,以及如何从创世区块开始无遗漏地回填索引。

## 为什么标准 Solana 节点无法提供完整历史数据?

Validator 会把账本写入本地数据库,运营者通常配上 [`--limit-ledger-size` 标志](https://docs.anza.xyz/operations/guides/validator-start)以防磁盘被占满。设置该标志后,节点会优先清除最旧的数据。它的默认值是 2 亿个 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"错误。更大的磁盘无法改变这一切。提供链上最新数据和提供深度历史数据是两个不同的基础设施问题,归档系统的存在,正是因为后者已经超出了单个节点的处理能力。

## Solana 上的"归档"是什么意思,它与 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 时的余额"这样的方法,也是为什么[查询历史账户状态](https://github.com/solana-labs/solana/issues/18197)在 Solana 代码仓库中作为一个 feature request 挂了这么多年一直未解决。

归档基础设施所保存的是账本本身,也就是区块以及区块内的交易。归档系统可以给你区块 150,000,000,或某个地址曾经涉及的每一笔交易。但它无法告诉你某个钱包去年三月的 USDC 余额。这个问题依然可以回答,但你需要通过 [indexer](https://www.alchemy.com/overviews/blockchain-indexer) 重放该钱包的交易历史来得出答案,而不是向节点索要它从未保留过的状态。

## 哪些 RPC 方法需要用到归档数据?

一旦某个方法所针对的 slot 低于节点的本地下限,这个方法就变成了一次归档读取。以下是需要访问长期存储的方法。

<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>根据签名查询已确认的交易</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>与某个地址相关的签名,按时间倒序排列</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>这是归档系统的下限,而非节点的下限</p>",
          tooltip: "",
          icon: "",
        },
        id: 5,
      },
    ],
  }}
/>

大多数历史数据 pipeline 都建立在这两个方法之上。用 `getSignaturesForAddress` 向前翻页某个地址的历史记录,再用 `getTransaction` 获取每笔交易的详情。由于签名查询的调用[每次最多返回 1,000 条结果](https://www.alchemy.com/docs/reference/solana-api-quickstart),对一个活跃地址的历史一路回溯到它的第一笔交易,意味着一长串分页调用,其中几乎每一次只要超出节点的最小账本 slot,就是由归档系统提供服务的。

这也是薄弱的归档系统会露馅的地方。如果提供商的长期存储存在缺口,你在深度回填过程中的某次调用就会返回"slot skipped"或"missing in long-term storage"错误,如果你没有做检查,你的索引就会在无人察觉的情况下出现缺失。所有提供商暴露的方法名都是一样的。真正有区别的是这些方法背后的存储是否存在缺口,以及在你以每次成千上万次调用进行分页查询时,它的响应速度有多快。

## 如何查询完整的区块和交易历史?

查询本身就是普通的 JSON-RPC。没有单独的归档 API,也没有什么特殊参数能解锁历史数据。你调用的是与查询链上最新数据完全相同的方法,当所查 slot 低于节点的本地下限时,提供商的归档系统就会接管应答。

获取一个深度历史区块的调用如下:

<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
    }]
  }'`}
/>

响应中包含整个区块、区块内的每一笔交易,以及每笔交易的状态和元数据。每次调用都要在参数中保留 `maxSupportedTransactionVersion: 0`。如果不加它,凡是包含 versioned transaction 的区块调用都会报错,而在主网上大多数区块都包含这类交易。

遍历一个地址的完整历史,用的就是上面表格中那套两方法模式,循环调用即可。用 `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();`}
/>

对单个钱包而言,这个循环就够用了,在笔记本电脑上就能顺利跑起来。但当地址是一个拥有数百万签名的活跃 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 项目主导的开源归档方案。它将创世以来的每一个区块打包成 content-addressed 的 CAR 文件,分布存储在 IPFS、Filecoin 和兼容 S3 的存储上,并通过标准的 Solana JSON-RPC 和 gRPC 提供服务。这个项目的存在,是为了让 Solana 的完整历史不依赖于任何单一公司的数据库,而需要可验证的、从创世开始完整覆盖的团队会把它当作参考归档系统。

无论你的提供商背后用的是哪种后端,需要牢记的是:归档系统是与节点相互独立的系统。深度和完整性是这个系统本身的属性,值得直接去询问,而不是想当然地假设。

## 如何大规模获取账户和代币历史数据?

想要"Solana 大规模历史数据"的团队,很少是真的想要原始区块。他们想要的是一个钱包随时间变化的余额、某个地址发生过的每一笔转账,或者某个代币的完整持有者历史,而且要足够快,能支撑一个仪表盘或税务导出功能。这些数据在账本中都不是以可查询的形式存在的,必须经过推导计算得出。

各个项目的做法几乎大同小异。从归档 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)详细介绍了其中的原理。

对于另外几种常见的衍生数据形态——随时间变化的余额、转账记录和代币元数据——[Data APIs](https://www.alchemy.com/docs/data) 会以预计算好的形式提供这些数据,你可以跳过整个索引项目。当你需要自定义的衍生数据或需要对 pipeline 拥有完全控制权时,再去自建 indexer。当标准形态已经能满足需求时,直接使用托管方法即可。唯一行不通的,是直接向一个普通节点查询过去的账户状态,因为节点从来没有保留过这些数据。每一个能用的答案,都建立在一个基于历史数据构建的索引之上。

## 如何从创世区块开始回填索引而不出问题?

历史数据 pipeline 中最难的部分是冷启动。你的索引是空的,需要加载数亿个 slot,而在你加载的整个过程中,链还在持续产生新区块。如果回填过程和实时数据流衔接得不干净,一端结束和另一端开始之间就会出现缺口。

不少团队一开始会用轮询归档 RPC 的方式完成整个回填,但在创世级别的规模下这会变得很痛苦:速率限制、数亿个 slot 带来的按请求计费成本,以及没有干净的方法来证明没有遗漏任何数据。经受住生产环境考验的模式,是在不同阶段使用不同的数据来源。批量历史数据直接来自归档系统,像 [Jetstreamer](https://github.com/anza-xyz/jetstreamer) 这样的工具可以直接从 Old Faithful 中流式拉取数据。链上最新数据则来自一个实时数据流,而不是继续轮询。

对于实时的那一半,兼容 Yellowstone 的 [Solana gRPC 流](https://www.alchemy.com/solana-grpc)会在新交易和账户更新发生时,按账户、program 或签名过滤后推送给你的 indexer。回填和数据流之间的接缝,正是 pipeline 通常会出现泄漏的地方,而 replay 正是用来堵住这个缺口的机制。[我们的 gRPC](https://www.alchemy.com/blog/introducing-alchemy-solana-grpc) 允许客户端在重新连接时带上 `from_slot` 参数,重新接收它掉线期间错过的那些 slot,这样一次断线就不会在你的数据中留下缺口。我们还构建了[能够在 failover 时保持数据流不中断](https://www.alchemy.com/blog/zero-downtime-zero-gaps-solana-grpc-streaming)的流式处理层,这原本会变成你需要在 indexer 旁额外维护的一套独立缺口检测服务。一旦归档系统覆盖了历史数据、数据流覆盖了最新数据,replay 就能把两者牢牢缝合在一起。

## 挑选 Solana 归档提供商时应该关注什么?

深度是首先要确认清楚的问题,值得用这样的原话去问任何一个提供商:你们是从创世区块开始索引,还是从某个更靠后的高度开始?"完整历史数据"却不说明起始下限的情况很常见,而你往往是在自己的回填过程卡死在那个下限上时才发现它。

其余的检查项都源自回填实际运行的方式。完整性很重要,因为一个缺口就会毁掉建立在它之上的整个索引。速度很重要,因为一次完整的签名历史遍历意味着成千上万次的连续归档读取,每次调用的延迟都会被放大成数小时甚至数天的实际耗时。标准 JSON-RPC 很重要,因为一个专有的历史查询接口会把你的 pipeline 锁死在单一供应商身上,而普通 RPC 完全不需要重写代码。价格也很重要,因为深度历史数据本质上是重度读取型的负载。

我们就是按照这份检查清单构建了自己的归档访问能力。它覆盖了[从创世区块开始的完整区块和交易历史](https://www.alchemy.com/docs/reference/solana-api-quickstart),全部通过标准 JSON-RPC 提供;[我们的基准测试](https://www.alchemy.com/blog/solana-infrastructure)显示,历史 `getTransaction` 的运行速度比其他提供商快达 20 倍,`getBlock` 快达 3 倍,而像 `getProgramAccounts` 这样的重量级调用快达 10 倍,且不需要修改任何代码,也不涉及任何专有方法。[我们如何构建 Solana 上最快归档方法的工程实录](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 需要两样东西:一个深度足以支持从创世区块回填的归档系统,以及一个快到能跟上链上最新进度的数据流。这两者我们都作为 [Alchemy 的 Solana 平台](https://www.alchemy.com/solana)的一部分提供。归档读取覆盖了通过标准 JSON-RPC 提供的完整区块和交易历史,因此只需将你现有的 Solana 客户端指向 Alchemy 的端点,整个迁移工作就完成了。[Solana gRPC streaming](https://www.alchemy.com/solana-grpc) 兼容 Yellowstone,支持重连时的 replay,按用量计费,每 TB 75 美元,无月度最低消费,也无需任何套餐前提条件。

从免费套餐开始,不需要签合同,也不需要销售电话。正在构建 Solana 基础设施的团队还可以通过我们的[2000 万美元 Solana Fund](https://www.alchemy.com/solana-20m-fund)申请最高 25,000 美元的额度。一旦你的 pipeline 上线运行,节点丢弃的那些历史数据,就不再是你需要操心的问题了。
