---
title: "数据准确性：Web3 中鲜为人知的普遍问题"
description: "探讨数据准确性在 Web3 基础设施中的重要性、现有解决方案的不足，以及 Alchemy 的应对方法。"
---

# 数据准确性：Web3 中鲜为人知的普遍问题

<ExternalVideo
  url="https://vimeo.com/728999085"
  provider="vimeo"
  providerUid="728999085"
/>

一个知名的 NFT 团队因为向用户展示了相互矛盾的信息而流失了数万用户\(_见上例_\)。

一个最大的 DeFi 协议因排查前端错误的 bug 而损失了 200 万美元。由于该协议无法定位根本原因，这个问题数周都没有得到解决。

#### 这两种情况都源于同一个问题：节点基础设施缺乏准确性，而这在 Web3 中是一个普遍存在的问题。

作为工程师、创始人、产品经理，甚至是用户，如果节点准确性还没有进入你的视野，你需要把它列为优先事项，这样你的应用才能取得成功。

## 一些背景信息

区块链由以点对点网络方式通信的节点运行。正是这种结构性特点实现了[去中心化](https://sgrlaw.com/blockchain-decentralized-ledgers/)，这是 web3 的基本价值主张之一：不依赖中心化的中介，而是由[数千个节点](https://etherscan.io/nodetracker)通过无需信任地相互传播信息来记录交易。

为了向用户提供正确且一致的结果，节点网络必须就区块链的最新状态达成一致。这在[点对点网络](https://medium.com/coinmonks/what-is-a-peer-to-peer-p2p-network-in-blockchain-3fe4417dfba2)中是极具挑战性的，因为信息不会同时到达每个节点。

<ExternalVideo
  url="https://vimeo.com/729065053"
  provider="vimeo"
  providerUid="729065053"
/>

这意味着当一个人与依赖节点提供数据的 dApp 交互时：

- 节点提供的数据并不总是一致的 ➡️
- 用户得到的结果并不总是他们期望的

<ImageBlock
  src="https://media.alchemy.com/1704793834-nfts-not-showing-up-in-wallets-on-time.png"
  alt="NFT 未能及时显示在钱包中"
  width={1068}
  height={557}
  caption="NFT 未能及时显示在钱包中"
/>

通常情况下，当用户收到无法解释的、意外或相互矛盾的信息时，是因为他们所使用的节点之间缺乏**准确性**。

在 Alchemy，我们将**准确性**定义为**用户与 dApp 交互并得到正确且一致的数据这一状态**。

在本文中，我们将解释：

- 为什么准确性是一个 web3 特有的问题
- 用单个节点解决准确性问题的局限性
- 用负载均衡器解决准确性问题的局限性
- 缺乏准确性会如何给应用带来广泛问题
- Alchemy 的 Cortex 引擎如何解决区块链准确性问题

## 当用户与 dApp 交互并得到正确且一致的数据时，准确性才成立

在 web2 中，[中心化系统让准确性成为一个容易解决的问题](https://ethereum.org/en/developers/docs/web2-vs-web3/#:~:text=Web2 refers to the version,that run on the blockchain)，因为信息只能来自一个来源。但 web3 的分布式系统带来了大量新的、复杂的问题，包括：

- 实现复杂
- 确保信息传递到网络中最远的参与者存在困难\(信息传递到网络中较远的节点确实需要更长时间\)
- 网络协调方面的困难

#### 所有这些挑战都可能导致一组节点之间缺乏准确性，进而导致：

- 提供有缺陷的体验
- 流失客户
- 花费时间和金钱去追溯修复问题

<CalloutBlock>

"[你能想到的所有关于 CryptoKitties 的状态问题](https://www.alchemy.com/case-studies/dapper)都发生过。如果节点不同步，用户体验和整个 dApp 都会乱套。"** - Eric Lin, Dapper**

</CalloutBlock>

## 运行单个节点的问题

如果数据准确性问题源于多个节点之间的通信，你可能会决定自己运行一个单独的节点。

假设你自己运行着一个单独的节点，一个用户向你的 dApp 发起了一次调用：

- 他们请求最新区块
- 你的节点告诉他们最新区块是 4

<ImageBlock
  src="https://media.alchemy.com/1704794016-the-problems-with-running-a-single-node.png"
  alt="用户向运行单一节点的 dApp 请求最新区块的示意图"
  width={1920}
  height={1081}
  caption="用户对你的 dApp 发起调用"
/>

✅ 这样是可行的！

但这里有个问题。

它只在一定程度上是可行的。

#### 让所有流量都经过单个节点会大大限制你的应用的可扩展性和可靠性：

- **可扩展性**：如果你的 dApp 今天有 100 个用户，但使用量需求增长了 10 倍，一个节点将无法有效处理增加的请求负载
- **可靠性**：如果你依赖一个节点，那么当这个节点宕机时，你的 dApp 也会随之宕机。而且由于节点出了名地不可靠，只用一个节点，你的正常运行时间将远低于所需水平\(我们测得的单节点正常运行时间在某些情况下低至 72%\)。

你的团队花在节点维护上的时间，[就是他们本可以用来为客户构建产品和体验的时间](https://www.alchemy.com/case-study/zerion)。

<CalloutBlock>

"与 Alchemy 合作帮助我们节省了相当于三名全职工程师的工作量，否则他们将不得不一直埋头处理基础设施维护工作。" - **Evgeny Yurtaev, [Zerion](https://www.alchemy.com/dapps/zerion) CEO 兼联合创始人**

</CalloutBlock>

好，既然你已经意识到单个节点不足以满足你的业务需求，那么接下来你可能会怎么做？‍

## 使用负载均衡器的问题

你可能想尝试使用一个通过负载均衡器管理多个节点流量的基础设施提供商；这是 web3 中非常常见的基础设施设计。

负载均衡是一种水平扩展机制，最初应用于 web2。可以把负载均衡想象成在超市里选择最短的队伍——负载均衡器会把你的 dApp 流量引导到排队人数最少的节点。‍

<ExternalVideo
  url="https://vimeo.com/729065065"
  provider="vimeo"
  providerUid="729065065"
/>

看看下面这个例子。

一个用户向你的 dApp 发起了一次调用：

- 他们请求最新区块
- 通过节点 A，你的 dApp 告诉他们最新区块是 2

然后，用户再次询问以确认：

- 这次查询被路由到了节点 B
- 通过节点 B，你的 dApp 告诉他们最新区块是 4

<ExternalVideo
  url="https://vimeo.com/729065080"
  provider="vimeo"
  providerUid="729065080"
/>

由于该系统仍然是将每个请求路由到单个节点，而在任何给定时刻，该节点都可能没有最新信息，所以结果往往是不正确的。你的用户将会看到相互矛盾、无法解释的结果。

**负载均衡比单个节点能提供更高的可扩展性和可靠性，但这是以牺牲准确性为代价的。**

#### 当流量通过负载均衡器路由时，节点之间可能对以下信息存在差异：

- 最新区块
- 最近的交易
- 待处理交易

这些问题可能导致：

- [开发者遇到不一致的数据](https://community.infura.io/t/how-does-the-load-balancer-work/1090)
- 请求失败
- 应用程序故障
- 同一个用户对同一请求看到相互矛盾的结果
- 多个用户对同一请求看到相互矛盾的结果

还不相信这是个大问题？接着往下看。

## 缺乏准确性如何造成重大问题

假设 Isaac 想加入一个 DAO，为此他需要购买该 DAO 的代币。

- Isaac 询问一个去中心化交易所\(DEX\)："DAO 代币还有吗？"
- 该 DEX 的基础设施提供商将 Isaac 的请求路由到了节点 A。
- 通过节点 A，该 DEX 告诉他："是的，代币还有。"

✅ 代币还有！

Isaac 前去购买。

- 该 DEX 的基础设施提供商将 Isaac 的请求路由到了节点 B。
- 通过节点 B，该 DEX 告诉他："交易失败。"

<ExternalVideo
  url="https://vimeo.com/729065088"
  provider="vimeo"
  providerUid="729065088"
/>

由于该 DEX 使用的基础设施提供商是在多个节点间做负载均衡\(一种非常常见的 web3 扩展机制\)，当 Isaac 确认代币还有时，节点 A 返回的信息在这种情况下是过时的。

**这凸显了一个关键点：在给定的时刻，来自单个节点的信息——而使用负载均衡器时信息始终来自单个节点——是不能被依赖的。**

那个节点可能没有最新信息，如果正是这个节点响应了你的请求，你的交易\(或后续交易\)很可能会失败或出现意外行为。

<EbookCta
  eyebrow="免费指南"
  title="自建 vs. 购买：区块链基础设施"
  description="在招聘基础设施团队之前，先看看这个：自建团队每年可能花费 85 万至 100 万美元以上，而且这还是在处理第一个生产请求之前。查看完整成本明细。"
  ctaLabel="下载指南"
  ctaHref="/resources/build-vs-buy"
  coverImage={{
    src: "https://media.alchemy.com/ebooks/cta-build-vs-buy.png",
    alt: "Build vs. Buy ebook cover",
  }}
/>

#### 准确性问题会导致各种各样的故障状态

Isaac 遇到的情况只是由节点不准确导致的众多故障状态之一。

我们再来看几个可能的故障状态：

1. 智能合约执行返回相互矛盾的答案，而本应只有一个确定的结果。例如：

- 用户问："这只 Bored Ape 的所有者是谁？"
- API 返回："Alice"
- 然后用户问："告诉我 Alice 拥有哪些 ape？"
- API 返回："Alice 一个都没有"‍

<ExternalVideo
  url="https://vimeo.com/728999085"
  provider="vimeo"
  providerUid="728999085"
/>

为什么会这样？在依赖负载均衡器的基础设施中，智能合约的状态，以及执行它们时发生的结果，会因处理请求的节点不同而不同。

2. dApp 返回了错误的"nonce"\(任意给定钱包地址每笔交易的唯一标识符\)，导致后续交易失败。例如：

- 用户用 nonce 100 完成了交易 1
- 用户问："下一个 nonce 是多少？"
- API 返回：100\(这本应是不可能的，因为 nonce 100 已经完成了\)
- 下一笔交易将被自动拒绝

<ExternalVideo
  url="https://vimeo.com/729065102"
  provider="vimeo"
  providerUid="729065102"
/>

_为什么会这样？在不准确的基础设施中，_[_待处理交易_](https://www.alchemy.com/docs/ethereum-transactions-pending-mined-dropped-replaced)_可能最终只存在于单个节点上，所以当你试图获取这些待处理交易或发送它们的钱包的状态信息时，返回的数据往往是缺失或误导性的。_

#### 这些故障状态隐蔽难察、无法解释，并会严重影响你应用的健康状况

可能发生的一些情况：

你可能会摄入不准确的数据，这会污染你的数据库，需要花费时间和精力去修复。

以[Origin](https://www.alchemy.com/case-study/origin)为例。他们最初的基础设施提供商经常出现 RPC 错误和调试问题，[调试这些错误](https://www.alchemy.com/build)耗费了他们团队大量时间。

他们发现，一个节点回答了一个请求，但这个节点比回答另一个请求的节点落后了几个区块。

这些状态问题几乎无法排查，一旦他们确实找到了根本原因，就发现这个问题已经污染了他们的整个系统。

**因为这些问题没有错误信息或失败代码，你 a\) 无法在代码中提前预判，b\) 在问题发生时需要花费数小时去调试；这对你的用户来说也同样令人困惑：**

<ImageBlock
  src="https://media.alchemy.com/1704794424-error-transferring-nft-from-bakery-swap.png"
  alt="从 Bakery swap 转移 NFT 时出错"
  width={942}
  height={359}
  caption="从 Bakery swap 转移 NFT 时出错"
/>

**用户可能会基于过时数据发送交易，而这些交易注定会失败；他们因此会损失 gas 费用：**‍

<ImageBlock
  src="https://media.alchemy.com/1704794521-nfts-missing-in-user-wallet.png"
  alt="用户反馈钱包中 NFT 丢失"
  width={1227}
  height={336}
  caption="NFT 丢失"
/>

**用户可能会在你的 dApp 上看到相互矛盾的界面，先后显示某笔交易成功或未成功。他们会把这个问题归咎于你的 dApp 的前端体验：**

<ImageBlock
  src="https://media.alchemy.com/1704794600-nfts-not-showing-up-in-user-wallet.png"
  alt="NFT 在 4 小时后仍未显示在用户钱包中"
  width={1224}
  height={552}
  caption="NFT 在 4 小时后仍未显示在用户钱包中"
/>

**用户可能会因为以为交易没有成功上链而多付费、再次尝试；而实际上原来的交易已经上链了，只是没有被准确反映出来：**

<ImageBlock
  src="https://media.alchemy.com/1704794692-user-paying-gas-fees-twice-for-failed-transaction.png"
  alt="用户因交易失败而支付两次 gas 费"
  width={1232}
  height={399}
  caption="用户因交易失败而支付两次 gas 费"
/>

## 为准确性而构建

如果现在还不够清楚的话：在区块链网络中，不准确的基础设施会对你的 dApp 和用户产生非常负面的影响。

你需要与这样的基础设施提供商合作：

- 把准确性视为一项基本优先事项
- 拥有一套不同于负载均衡、可持续的系统，能够在你的 dApp 扩展时维持准确性

#### 对节点提供商进行基准测试

我们整理了一些数据，对几家知名节点基础设施提供商的准确性进行了基准测试。你也可以用这个工具自行测试准确性。

准确性只是评估提供商的一个方面。你还可以在 Alchemy 的[RPC provider benchmarks](https://www.alchemy.com/benchmarks)中比较当前延迟、成功率和失败请求数。

在我们的测试中，我们调用 JSON RPC 方法 _eth_blockNumber_ 共约 1,072,000 次，以在 24 小时内从每个提供商获取最新区块号。这只是基准测试数据不准确性的一个简单例子。还有很多其他情况，往往失败得更频繁、更严重。

<ImageBlock
  src="https://media.alchemy.com/1704794854-benchmarking-node-providers.png"
  alt="各节点基础设施提供商 24 小时数据准确性对比图表"
  width={3250}
  height={1500}
  caption="节点提供商对比测试"
/>

_注_ 在我们的示例仪表盘截图中，我们对单日的数据准确性进行了查询和记录。

\*完整的方法论细节见附录。

#### 你可以做些什么来获得准确的数据？

<CalloutBlock>

"Alchemy 解决了之前一直存在的一致性问题，消除了 98% 的用户投诉，显著改善了 [Augur](https://www.alchemy.com/dapps/augur) 的用户体验和采用率。" - [**Augur CTO Alex Chapman**](https://www.alchemy.com/case-studies/augur)

</CalloutBlock>

为了确保你的[应用](https://www.alchemy.com/dapps/top/defi-dapps)和客户能获得准确性，Alchemy 投入了数十万个工程小时，开发了数百项独特的创新，打造出[Alchemy 的 Cortex 引擎](https://www.alchemy.com/rpc-api)，它在提供多节点基础设施的可扩展性和可靠性优势的同时，确保数据准确性的一致性。

Cortex 远不止是一堆连接在一起的节点——事实上，那只是 Cortex 引擎的一小部分。Cortex 结合了自研的、可扩展的分布式系统，本质上使我们的 API 能够表现得像单个节点一样运作，从而解决了我们之前讨论的所有准确性问题。

这是如何实现的？

<ExternalVideo
  url="https://vimeo.com/730080026"
  provider="vimeo"
  providerUid="730080026"
/>

我们构建了一个明确的一致性层，叫做 Vox Nodi\(节点之声\)。Vox Nodi 的职责是保证我们所服务的任何区块链请求都返回一致的结果。

其实现方式本质上是在我们的基础设施上运行一种共识算法，基础设施的每个组成部分都可以对区块链的正确状态进行投票。

通过正确地路由和调整查询，这个系统确保了尽管不同节点在任何给定时刻对交易数据有不同的视图，返回的结果始终保持一致准确。

简而言之，Vox Nodi 保证对我们 API 的任何请求都能快速、可靠地返回，且数据 100% 准确。

而 Cortex 仍然使开发者能够无限且可靠地扩展，因为响应每个请求的不是单个节点，而是一个完整的、分布式的区块链引擎。

简而言之，它**不是负载均衡器**。

## 影响与结论

在选择如何将你的 dApp 连接到区块链时，有许多不同的维度需要考虑：**提供商提供准确性的能力应该是你清单上的第一位。**

在 Alchemy，准确性是首要任务。立即[开始](https://dashboard.alchemy.com/signup/?a=f79ab530c4)免费使用 Alchemy。

-

## 附录

#### 基准测试方法论

在思考如何对准确性进行基准测试时，我们从第一性原理出发，测试了最基本的 Ethereum RPC 调用之一：eth_blockNumber。

简单回顾一下，eth_blockNumber 返回 Ethereum 网络的最新区块号，在去中心化网络中，区块号最大的区块包含了最新的交易。

因此，在正常的网络条件下，从单个节点/节点提供商调用 eth_blockNumber 应该只返回**递增**的区块号。任何不递增的不一致情况，都表明存在回滚，或者，如果发生得更频繁，则表明提供商可能存在数据不准确的问题。

请注意，这与"[reorg](https://www.alchemy.com/overviews/what-is-a-reorg)\(重组\)"不同，重组是指新的最长链替换了节点上已有的数据。"重组"后的链长度总是**至少等于**节点上已有的数据长度，所以如果我们看到区块号倒退，就可以明确地判定这是由基础设施故障造成的。

为了理解我们如何衡量数据不一致，假设我们在总共轮询 eth_blockNumber 13 次后得到了以下区块号序列。

<EmbeddedTable
  table={{
    columns: [
      { key: "1", width: 200, title: "eth_blockNumber", dataType: "object" },
      { key: "2", width: 200, title: "Response Sequence", dataType: "object" },
    ],
    data: [
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 0,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 1,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 2,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 3,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 4,
      },
      {
        "1": { title: "<p>2</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 5,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: false, tooltip: "", icon: "" },
        id: 6,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: false, tooltip: "", icon: "" },
        id: 7,
      },
      {
        "1": { title: "<p>3</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 8,
      },
      {
        "1": { title: "<p>2</p>", tooltip: "", icon: "" },
        "2": { title: false, tooltip: "", icon: "" },
        id: 9,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: false, tooltip: "", icon: "" },
        id: 10,
      },
      {
        "1": { title: "<p>1</p>", tooltip: "", icon: "" },
        "2": { title: false, tooltip: "", icon: "" },
        id: 11,
      },
      {
        "1": { title: "<p>3</p>", tooltip: "", icon: "" },
        "2": { title: true, tooltip: "", icon: "" },
        id: 12,
      },
    ],
  }}
/>

根据这个序列，我们发现在区块索引 6、7、9、10 和 11 处共有 5 个错误。这意味着在区块 6、7、9、10 和 11 处，返回的区块号没有遵循预期的递增顺序，证明存在某种潜在的数据准确性问题。

注：一旦我们记录到节点提供商已到达区块号 2，我们就假定之后任何返回值为 1 的情况都是错误的，相当于该提供商"时间倒流"，返回了滞后 1 个区块的_当前_区块值。同样地，一旦我们记录到节点提供商已到达区块号 3，此后任何出现的区块 1 或 2 都属于_不一致_。为了简化这项基准测试，我们明确只衡量各个提供商自身的数据准确性，而不对不同提供商之间的区块数据进行交叉比较。
