Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查阅读约 14 分钟

Solana getSignaturesForAddress 分页深度解析

正确处理游标、commitment 和保留期,无遗漏、无重复、无静默截断地遍历 Solana 地址的签名历史。

TL;DR

getSignaturesForAddress 返回的是按最新优先排序的签名级状态条目,而非完整交易,因此完整的历史遍历分为两个阶段:先用 before/until 游标枚举签名,再逐个获取交易。每次调用的最大页大小和保留窗口是提供商和集群的属性,而非固定的协议常量,因此正确的遍历器必须将短页、空页和缺失交易视为预期的边界情况,而不是错误。本指南解释游标机制、无缝算法、commitment 交互、WebSocket 传输行为,以及一个可运行的 Node.js 示例,可在你自己的端点上产生可复现的证据。

getSignaturesForAddress 返回什么、省略什么

Solana JSON-RPC 方法 getSignaturesForAddress 接收一个地址以及可选的 before、until、limit、commitment 和 minContextSlot 参数,返回按最新优先排序的签名状态条目数组。每个条目包含 signature、slot、err、memo、blockTime 和 confirmationStatus。这是官方 Solana RPC 方法参考(https://solana.com/docs/rpc/http/getsignaturesforaddress)中记录的行为。

关键在于,该方法有意省略了交易本身。你得到的是签名及其状态元数据,但不包含指令、日志或账户密钥。因此完整的历史遍历分为两个阶段:先枚举签名,再用 getTransaction 或类似方法获取每笔交易。第二阶段必须容忍某个签名的交易已无法检索,因为即使签名索引仍列出该签名,节点也可能已修剪较旧的交易数据。

这种两阶段设计对索引器很重要,因为两个阶段的失败模式不同。签名枚举开销小且由游标驱动;交易获取更重,可能超时,并且对于节点不再提供的签名可能返回 null。将 null 交易视为致命错误会中断原本健康的遍历。

  • 返回:signature、slot、err、memo、blockTime、confirmationStatus。
  • 省略:交易主体、日志和指令数据。
  • 影响:需要规划独立的 getTransaction 阶段,并制定自己的重试和跳过策略。

before 和 until 游标的实际工作方式

before 和 until 都接收签名,而不是 slot 或索引。实用模式是将 before 设为上一页的最后一个签名,这告诉节点返回严格早于该签名的条目。你只将 until 作为有界停止条件来推进,例如你不想越过的已知检查点签名。

最常见的静默截断 bug 是在多页之间复用陈旧的 until。如果你在开始时设置一次 until 且从不更新,后续每一页都会针对同一个边界进行过滤,遍历会在没有报错的情况下提前停止。正确做法是在完整遍历中不设置 until,或者仅当你打算在特定点停止时才刻意推进它。

由于 before 是签名而非 slot,你必须记录收到的最后一个签名,而不是最后一个 slot。slot 对每个地址并不唯一;同一地址的多个签名可以共享同一个 slot。签名是唯一的,这使其成为唯一安全的游标。

  • before:返回严格早于该签名的条目。
  • until:到达该签名时停止;用作有界停止,而非固定过滤器。
  • 游标类型:签名,绝不用 slot。

朴素分页循环失败的三种方式

第一,将 limit 用作页大小,同时假设它总是返回完整的一页。limit 参数是最大值,而非保证。短页可能意味着该地址剩余的签名本来就较少,也可能意味着请求的窗口落在保留期之外。如果你的循环只在空页时停止,那么短页后跟空页可能会掩盖保留期边界。

第二,从保存的 slot 而非保存的签名重新开始遍历。由于 slot 对每个地址并不唯一,从 slot 恢复可能会跳过或重复共享该 slot 的签名。始终持久化你成功处理的最后一个签名。

第三,将空页视为历史结束的证明。空页可能仅意味着请求的窗口落在节点的保留期之外。正确的解释是:只有当遍历从 null 开始时,空页才表示已耗尽,即你从最新签名开始并向回遍历到保留历史的真正末尾。

  • 不要假设 limit 会返回完整的一页。
  • 不要从 slot 恢复;要从签名恢复。
  • 不要将每个空页都视为历史结束。

无缝遍历算法

健壮的遍历器将游标记录为收到的最后一个签名,检测页大小不足,验证合并结果内部的 slot 单调排序,并且仅当遍历从 null 开始时才将空页标记为已耗尽。这为你提供了一个确定性的状态机,而不是一个充满希望的循环。

开始时 before 未设置、until 未设置。请求一页。如果页面为空且你从 null 开始,标记为已耗尽并停止。如果页面为空且你不是从 null 开始,标记为保留期边界并停止。如果页面非空,追加条目,将 before 设为最后一个签名,然后继续。

合并页面后,断言从最新到最旧移动时 slot 单调非递增。违反此规则表明存在游标 bug 或提供商侧不一致,应停止遍历,而不是静默损坏你的索引。

  • 状态:cursor(最后一个签名)、startedFromNull(布尔值)、exhausted(布尔值)。
  • 短页时:记录 pageSizeReturned 并继续,除非为空。
  • 空页时:仅当 startedFromNull 时才为已耗尽,否则为保留期边界。
  • 合并后检查:slot 必须单调非递增。

处理交易保留期边界

提供商节点保留近期交易数据,但可能不提供非常旧的交易。跨越此边界的遍历必须区分“此签名比节点能提供的更旧”和“此签名不存在”。签名索引和交易存储可能有不同的保留窗口,因此列出的签名并不保证交易可检索。

正确的设计是持久化最后成功归档的签名,以便未来每次遍历都从已知良好的点恢复。当 getTransaction 对你已枚举的签名返回 null 时,将其记录为 archived-unavailable,而不是无限重试。这能保持遍历继续,并保留干净的恢复点。

对于长期历史,仅靠分页无法重建集群不再保留的数据。你需要归档策略:在遍历时持久化交易,并将签名遍历视为发现机制,而非检索保证。保留期背景请参见 Solana 通过 RPC 获取历史数据

  • 签名索引保留期和交易保留期可能不同。
  • 持久化最后成功归档的签名作为恢复点。
  • null 交易:标记为 archived-unavailable,不要无限重试。

可运行的 Node.js 遍历器与可复现输出

以下示例使用显式页大小,针对作为参数传入的端点 URL 遍历一个地址。它打印 pageIndex、pageSizeReturned、firstSignature、lastSignature、firstSlot、lastSlot 和 elapsedMs,并断言连续页面从不重叠且从不跳过 slot。在你自己的端点和地址上运行它,以构建可复现的证据。

将 RPC 端点作为第一个参数,地址作为第二个参数传入。脚本使用 fetch,现代 Node.js 中可用。将 PAGE_SIZE 调整为远低于提供商最大值的值。

断言有意严格:如果某页与上一页重叠,或者 slot 不单调,它们会抛出异常。这正是重点。你希望遍历器大声失败,而不是静默产生损坏的索引。

// walk.mjs
// Usage: node walk.mjs <RPC_URL> <ADDRESS> [PAGE_SIZE]
const RPC_URL = process.argv[2];
const ADDRESS = process.argv[3];
const PAGE_SIZE = Number(process.argv[4] || 100);

if (!RPC_URL || !ADDRESS) {
  console.error('Usage: node walk.mjs <RPC_URL> <ADDRESS> [PAGE_SIZE]');
  process.exit(1);
}

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function walk() {
  let before = null;
  let pageIndex = 0;
  let startedFromNull = true;
  let previousLastSignature = null;
  let previousFirstSlot = null;
  const seen = new Set();

  while (true) {
    const t0 = Date.now();
    const params = [ADDRESS, { limit: PAGE_SIZE }];
    if (before) params[1].before = before;
    const page = await rpc('getSignaturesForAddress', params);
    const elapsedMs = Date.now() - t0;

    const pageSizeReturned = page.length;
    const firstSignature = page[0]?.signature ?? null;
    const lastSignature = page[page.length - 1]?.signature ?? null;
    const firstSlot = page[0]?.slot ?? null;
    const lastSlot = page[page.length - 1]?.slot ?? null;

    console.log(JSON.stringify({
      pageIndex,
      pageSizeReturned,
      firstSignature,
      lastSignature,
      firstSlot,
      lastSlot,
      elapsedMs,
    }));

    if (pageSizeReturned === 0) {
      if (startedFromNull) console.log('exhausted: true');
      else console.log('retention boundary reached');
      break;
    }

    for (const entry of page) {
      if (seen.has(entry.signature)) {
        throw new Error('overlap detected: ' + entry.signature);
      }
      seen.add(entry.signature);
    }

    if (previousFirstSlot !== null && firstSlot > previousFirstSlot) {
      throw new Error('slot ordering violation: ' + firstSlot + ' > ' + previousFirstSlot);
    }

    previousLastSignature = lastSignature;
    previousFirstSlot = firstSlot;
    before = lastSignature;
    startedFromNull = false;
    pageIndex += 1;
  }
}

walk().catch((err) => {
  console.error('walk failed:', err.message);
  process.exit(1);
});

测量你自己的端点:结果表指南

由于最大页大小、保留期和速率限制是提供商和集群的属性,你应该针对自己的端点进行测量,而不是假设数值。用较小的页大小运行上面的遍历器并记录输出。然后用较大的页大小重复并比较。

用你自己的观察填写下表。目标是确定开始出现短页或超时的页大小,以及空页表示保留期而非耗尽的临界点。

不要将任何单次运行视为定论。提供商行为可能在没有通知的情况下改变,同一端点在负载下可能表现不同。在不同时间重复测量并记录差异。

  • 列:pageSizeRequested、pageSizeReturned、elapsedMs、shortPage?、emptyPage?、notes。
  • 至少运行三种页大小:小、中、接近疑似最大值。
  • 记录最终空页之前是完整页还是短页。
| pageSizeRequested | pageSizeReturned | elapsedMs | shortPage? | emptyPage? | notes |
| --- | --- | --- | --- | --- | --- |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |
| ____ | ____ | ____ | ____ | ____ | ____ |

Commitment 级别与临时页面

commitment 参数与遍历相互作用,因为 confirmed 和 finalized 历史在链尖附近会出现分歧。如果分叉被解决,confirmed 遍历可能包含后来从 finalized 历史中消失的签名。索引器应针对它实际需要的 commitment 级别进行遍历,并将最近的页面视为临时的。

如果你需要 finalized 数据,请使用 commitment finalized 遍历,并接受最新签名可能尚不可用。如果你需要低延迟数据,请使用 confirmed 遍历,并准备好稍后对链尖进行对账。在单次遍历中混合 commitment 级别会产生不一致的结果。

关于 commitment 如何影响确认的更深入讨论,请参见 Solana commitment 级别与交易确认

  • confirmed 和 finalized 历史在链尖附近出现分歧。
  • 针对你实际需要的 commitment 级别进行遍历。
  • 将最近的页面视为临时的,稍后对账。

WebSocket 传输与游标安全

getSignaturesForAddress 是普通的请求/响应调用,不是订阅。在 WebSocket 传输上,它的行为与 HTTP 上相同;WebSocket 路径关乎连接复用,而非推送语义。此方法没有服务器发起的签名流。

朴素的重新订阅循环可能从错误的游标重新开始遍历。如果你的 WebSocket 连接断开,并且你在没有保留最后一个签名的情况下重新连接,你可能会从最新签名重新开始并重复工作,或者更糟,从不再匹配服务器状态的陈旧内存游标恢复。

在连接生命周期之外持久化游标。将 WebSocket 视为传输细节,而不是遍历状态的真相来源。关于端点选择和传输指导,请参见 RPC 端点指南(RPC Assistant)

  • 不是订阅:此方法没有推送语义。
  • 在连接生命周期之外持久化游标。
  • 重新连接时,从最后持久化的签名恢复。

选择低于提供商最大值的页大小

limit 参数应选择远低于提供商最大值的值。较小的页面限制响应大小和故障影响范围,重试也变得廉价。满页限制会最大化超时风险,而超时会损失整页。

如果某页超时,你会丢失该页的工作,并且必须从同一游标重试。使用较小的页面,重试更快,重复超时的风险更低。这是在请求数量和每请求可靠性之间的权衡。

关于与此指导互补的超时和重试模式,请参见 Solana RPC 超时与重试

  • 较小页面:更低的影响范围,更廉价的重试。
  • 满页限制:每请求更高的超时风险。
  • 根据你自己的端点测量调整页大小。

限制、权衡与故障排查

保留期、最大页大小和速率限制是提供商和集群的属性,会有所不同。对它们的未记录更改会破坏长时间遍历。签名遍历无法重建集群不再保留的历史,因此长期历史需要归档策略,而非分页策略。

常见失败模式包括:遍历因 until 在多页之间被复用而提前停止;遍历因从 slot 恢复而重复签名;遍历在实际上触及保留期时将空页视为耗尽;以及遍历因 getTransaction 对旧签名返回 null 而停滞。

要进行故障排查,请为每一页记录 pageIndex、pageSizeReturned、firstSignature、lastSignature、firstSlot、lastSlot 和 elapsedMs。比较连续页面是否存在重叠和 slot 单调性。如果遍历意外停止,请检查是否设置了 until,以及最后一页是空页还是短页。

关于旧遍历器中可能出现的已弃用方法的迁移,请参见 迁移已弃用的 Solana RPC 方法。关于特定网络的端点详情,请参见 Solana 网络

  • 保留期、页大小和速率限制因提供商和集群而异。
  • 分页无法恢复集群不再保留的数据。
  • 记录每页指标以诊断提前停止和重叠。
  • 区分保留期边界和真正的耗尽。

生产索引器的后续步骤

通过持久化最后成功归档的签名、安排定期遍历,并针对 finalized commitment 级别对临时链尖进行对账,从一次性遍历转向持久索引器。将签名遍历视为发现机制,将交易获取视为归档步骤。

选择符合你保留期和速率要求的端点。查看 RPC 定价API 服务 以了解提供商侧约束,并使用 OnFinality Learn 中心 获取关于历史数据、超时和 commitment 的相邻指南。

最后,使用上面的结果表指南,针对你自己的地址和端点验证遍历器。来自你自己测量的可复现证据是调整页大小和重试策略的唯一可靠基础。

  • 持久化最后归档的签名作为恢复点。
  • 安排定期遍历并对链尖进行对账。
  • 在生产前针对你自己的端点进行验证。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始