

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# Amazon QLDB 中的数据验证
<a name="verification"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

借助 Amazon QLDB，您可以相信对应用程序数据的更改历史是准确无误的。*QLDB 使用不可变的事务日记账（称为日记账）进行数据存储。*日记账会跟踪已提交数据的每一次更改，并保存完整、可验证的更改历史记录。

QLDB 使用 SHA-256 哈希函数和基于 Merkle 树的模型来生成日记账的加密表示，即*摘要*。摘要是数据在某个时间点上整个更改历史的唯一签名。您可以使用摘要来验证您的文档修订相对于该签名的完整性。

**Topics**
+ [您可以在 QLDB 中验证哪种数据？](#verification.structure)
+ [数据完整性意味着什么？](#verification.integrity)
+ [验证是如何运行的？](#verification.how-it-works)
+ [验证示例](#verification.example)
+ [数据编辑对验证有何影响？](#verification.redaction)
+ [开始验证](#verification.getting-started)
+ [步骤 1：在 QLDB 中请求摘要](verification.digest.md)
+ [步骤 2：在 QLDB 中验证您的数据](verification.verify.md)
+ [验证结果](verification.results.md)
+ [教程：使用 AWS 软件开发工具包验证数据](verification.tutorial-block-hash.md)
+ [常见的验证错误](verification.errors.md)

## 您可以在 QLDB 中验证哪种数据？
<a name="verification.structure"></a>

在 QLDB 中，每个分类账只有一个日记账。一个日记账可以有多个*链*，即日记账的分区。

**注意**  
QLDB 目前仅支持单链日记账。

*区块*是在事务过程中提交到日记账链的对象。此区块包含*条目*对象，代表事务产生的文档修订。您可以在 QLDB 中验证单个修订或整个日记账区块。

下面的示意图阐明了此日记账结构。

![\[Amazon QLDB 日记账结构图显示了构成一条链的一组哈希链区块，以及每个区块的序列号和哈希值。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/verification/journal-structure.png)


该图显示事务作为包含文档修订条目的数据块提交到日记账中。它还显示，每个区块都与后续区块进行了散列，并有一个序列号来指定其在链中的地址。

有关块中数据内容的更多信息，请参阅 [Amazon QLDB 中的日记账内容](journal-contents.md)。

## 数据完整性意味着什么？
<a name="verification.integrity"></a>

QLDB 中的数据完整性意味着您的分类账日记账实际上是不可变的。换句话说，您的数据（特别是每个文档修订）处于以下情况的状态：

1. 它存在于日记账中最初写下它的位置。

1. 自撰写以来，它从未被以任何方式修改过。

## 验证是如何运行的？
<a name="verification.how-it-works"></a>

要了解验证在 Amazon QLDB 中的工作原理，您可以将该概念分解为四个基本组成部分。
+ [哈希](#verification.how-it-works.hashing)
+ [摘要](#verification.how-it-works.digest)
+ [Merkle 树](#verification.how-it-works.merkle-tree)
+ [证明](#verification.how-it-works.proof)

### 哈希
<a name="verification.how-it-works.hashing"></a>

QLDB 使用 SHA-256 加密哈希函数创建 256 位的哈希值。哈希值是任意数量输入数据的唯一固定长度签名。如果更改输入的任何部分，哪怕是一个字符或一个比特，那么输出哈希值就会完全改变。

下图显示，SHA-256 哈希函数为两个 QLDB 文档创建完全唯一的哈希值，而这些哈希值仅相差一个数字。

![\[该图显示，SHA-256 加密哈希函数为两个 QLDB 文档创建了完全唯一的哈希值，而这些哈希值仅相差一个数字。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/sha256.png)


SHA-256 散列函数是单向的，这意味着在给定输出的情况下计算输入在数学上是不可行的。下图显示，在给定输出哈希值时，计算输入 QLDB 文档是不可行的。

![\[图中显示，在给定输出哈希值的情况下，计算输入 QLDB 文档是不可行的。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/sha256-one-way.png)


出于验证目的，以下数据输入在 QLDB 中经过哈希处理：
+ 文档修订
+ PartiQL 语句
+ 修订条目
+ 日记账区块

### 摘要
<a name="verification.how-it-works.digest"></a>

*摘要*是您分类帐在某一时刻的整个日记账的加密表示。日记账是只追加的，日记账区块按序号排列并像区块链一样进行哈希链接。

您可以随时请求分类账摘要。QLDB 生成摘要并将其作为安全输出文件返回给您。然后，您使用该摘要来验证在先前时刻提交的文档修订的完整性。如果您通过从一个修订开始重新计算哈希直到摘要，您可以证明在此期间数据未被更改。

### Merkle 树
<a name="verification.how-it-works.merkle-tree"></a>

随着您的分类帐规模的增长，重新计算整个日记账的哈希链进行验证变得越来越低效。QLDB 使用 Merkle 树模型来解决这种效率低下的问题。

*Merkle 树*是一种树数据结构，其中每个叶子节点代表一个数据块的哈希值。每个非叶子节点都是其子节点的哈希值。Merkle 树通常用于区块链，它通过一个审计证明机制，帮助您高效地验证大型数据集。有关 Merkle 树的更多信息，请参阅[ Merkle 树维基百科页面](https://en.wikipedia.org/wiki/Merkle_tree)。要了解有关 Merkle 审计证明的更多信息以及示例用例，请参阅证书透明度网站上的[日记账证明的工作原理](https://www.certificate-transparency.org/log-proofs-work)部分。

Merkle 树的 QLDB 实现是由日记的完整哈希链构造的。在此模型中，叶节点是所有单个文档修订哈希的集合。根节点表示截至某个时间点的整个日记账的摘要。

使用 Merkle 审计证明，您可以通过仅检查分类账修订历史记录的一小部分来验证修订。通过从给定的叶节点（修订）遍历树到其根节点（摘要），您可以实现这一点。沿着这条遍历路径，您递归地对节点的兄弟对进行哈希运算，直到最终得到摘要。这种遍历具有树中`log(n)`节点的时间复杂度。

### 证明
<a name="verification.how-it-works.proof"></a>

*证明*是 QLDB 针对给定摘要和文档修订返回的节点哈希的有序列表。它由 Merkle 树模型所需的哈希，用于将给定的叶节点哈希（一个修订）链接到根哈希（摘要）。

在修订和摘要之间更改任何已提交的数据会破坏日记账的哈希链，并导致无法生成证明。

## 验证示例
<a name="verification.example"></a>

下图阐明了 Amazon QLDB 哈希树模型。它显示了一组向上滚动到顶部根节点的区块哈希，它代表了日记账链的摘要。在具有单链日记账的分类账中，这个根节点也是整个分类账的摘要。

![\[日记账链中一组区块哈希的 Amazon QLDB 哈希树图。\]](http://docs.aws.amazon.com/zh_cn/qldb/latest/developerguide/images/verification/hash-tree.png)


假设节点 **A** 是包含您要验证哈希的文档修订的块。****以下节点代表 QLDB 在您的证明中提供的有序哈希列表**：**B、E、G。**** **这些哈希值是为了从哈希 A 重新计算摘要而必需的。**

要重新计算摘要，请执行以下操作：

1. **从哈希 **A 开始，**然后将其与哈希 B 连接起来。** 然后，对结果进行哈希处理以计算 **D**。

1. 使用 **D** 和 **E** 来计算 **F**。

1. 使用 **F** 和 **G** 来计算摘要。

如果您重新计算的摘要与预期值匹配，那么验证就是成功的。鉴于一个修订哈希和一个摘要，不可行对证明中的哈希进行逆向工程。因此，这个过程证明了相对于摘要，确实将您的修订写入了这个日记账位置。

## 数据编辑对验证有何影响？
<a name="verification.redaction"></a>

在 Amazon QLDB，`DELETE` 语句只能通过创建将文档标记为已删除的新修订版，从逻辑上删除文档。QLDB 还支持*数据编校*操作，允许您永久删除表历史记录中的非活动文档修订版本。

密文操作仅删除指定修订中的用户数据，而日记账序列和文档元数据则保持不变。修订已被编辑后，修订中的用户数据（由 `data` 结构表示）将替换为一个新 `dataHash` 字段。该字段的值是已移除 `data` 结构的 [Amazon Ion](ion.md) 哈希。有关修订操作示例的更多信息，请参阅 [对文档修订版执行编校](working.redaction.md)。

因此，分类账保持了其整体数据的完整性，并通过现有验证 API 操作保持加密可验证性。您仍然可以按预期使用这些 API 操作来请求摘要 ([GetDigest](https://docs.aws.amazon.com/qldb/latest/developerguide/API_GetDigest.html))、请求证明（[GetBlock](https://docs.aws.amazon.com/qldb/latest/developerguide/API_GetBlock.html)或 [GetRevision](https://docs.aws.amazon.com/qldb/latest/developerguide/API_GetRevision.html)），然后使用返回的对象运行验证算法。

### 重新计算修订哈希值
<a name="verification.redaction.recalc-hash"></a>

如果您计划通过重新计算其哈希来验证单个文档修订，您必须有条件地检查修订是否已被撤销。如果修订已被编辑，则可以使用`dataHash`字段中提供的哈希值。如果哈希值未被编辑，则可以使用该字段重新计算哈希。`data`

通过执行此条件检查，您可以识别已编辑的修订并采取适当的措施。例如，您可以记录数据操作事件以进行监视目的。

## 开始验证
<a name="verification.getting-started"></a>

在进行数据验证之前，您必须从您的分类帐请求一个摘要并保存它以备将来使用。在摘要覆盖的最新区块之前提交的任何文档修订都可以对该摘要进行验证。

然后，您向 Amazon QLDB 请求一个证明，以验证您想要验证的符合条件的修订。利用这个证明，您调用客户端 API 重新计算摘要，以您的修订哈希为起点。只要之前*保存的摘要在 QLDB *之外是已知和受信任的，如果您重新计算的摘要哈希与保存的摘要哈希匹配，那么您的文档的完整性就得到了证明。

**重要**  
您具体证明的是在您保存这个摘要和运行验证之间，文档修订没有被更改。您可以在稍后要验证的修订提交到日记账后立即请求并保存一个摘要。
作为最佳实践，我们建议您定期请求摘要并将其存储在离分类帐远离的地方。确定请求摘要的频率应基于您在分类帐中提交修订的频率。  
有关在现实用例背景下讨论加密验证价值的详细 AWS 博客文章，请参阅使用 [Amazon QLDB 进行真实世界的加密验证](https://aws.amazon.com/blogs/database/real-world-cryptographic-verification-with-amazon-qldb/)。

有关如何从账本中请求摘要然后验证数据的 step-by-step指南，请参阅以下内容：
+ [步骤 1：在 QLDB 中请求摘要](verification.digest.md)
+ [步骤 2：在 QLDB 中验证您的数据](verification.verify.md)

# 步骤 1：在 QLDB 中请求摘要
<a name="verification.digest"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

Amazon QLDB 提供了一个 API，用于请求涵盖分类账中日记账当前*提示* 的摘要。日记账提示指的是 QLDB 收到您的请求时的最近提交的数据块。您可以使用 AWS 管理控制台、S AWS DK 或 AWS Command Line Interface (AWS CLI) 来获取摘要。

**Topics**
+ [AWS 管理控制台](#verification.digest.con)
+ [QLDB API](#verification.digest.api)

## AWS 管理控制台
<a name="verification.digest.con"></a>

使用 QLDB 控制台，按照以下步骤来还原资源。

**请求摘要（控制台）**

1. [登录并打开亚马逊 QLDB 控制台，网址为 /qldb。 AWS 管理控制台 https://console.aws.amazon.com](https://console.aws.amazon.com/qldb)

1. 在导航窗格中，选择**分类账**。

1. 在分类账列表中，选择要申请摘要的分类账名称。

1. 选择“**获取摘要**”。**获取摘要**对话框显示以下摘要详细信息：
   + **摘要** - 您请求的摘要的 SHA-256 哈希值。
   + **摘要提示地址** - 您请求的摘要所涵盖的日记中的最新区块位置。地址包含以下两个字段：
     + `strandId` — 包含数据块的日记账链的唯一 ID。
     + `sequenceNo` — 一个索引号，用于指定数据块在链中的位置。
   + **分类账** - 您请求摘要的分类账名称。
   + **日期** - 您请求摘要时的时间戳。

1. 检查摘要信息。然后选择 **Save**（保存）。您可以保留默认文件名，或输入新名称。
**注意**  
您可能会注意到，即使您不修改分类账中的任何数据，您的摘要散列值和提示地址值也会发生变化。这是因为每次在 *PartiQL 编辑器*中运行查询时，控制台都会检索分类账的系统目录。这是向日记账提交的读事务，会导致最新区块地址发生变化。

   此步骤将保存一个内容为 [Amazon Ion](ion.md) 格式的纯文本文件。该文件的文件扩展名为 `.ion.txt`，包含前面对话框中列出的所有摘要信息。以下是摘要内容的示例。字段的顺序可能因您的浏览器而异。

   ```
   {
     "digest": "42zaJOfV8iGutVGNaIuzQWhD5Xb/5B9lScHnvxPXm9E=",
     "digestTipAddress": "{strandId:\"BlFTjlSXze9BIh1KOszcE3\",sequenceNo:73}",
     "ledger": "my-ledger",
     "date": "2019-04-17T16:57:26.749Z"
   }
   ```

1. 将这个文件保存在您将来可以访问的地方。随后，您可以使用这个文件来验证文档修订。
**重要**  
您稍后验证的文档修订必须包含在您保存的摘要中。也就是说，文档地址的序列号必须小于或等于**摘要提示地址**的序列号。

## QLDB API
<a name="verification.digest.api"></a>

您还可以使用 Amazon QLDB API 与相关的 AWS SDK 或者使用 AWS CLI，通过向您的分类帐请求摘要。QLDB API 提供以下操作以供应用程序使用：
+ [GetDigest](https://docs.aws.amazon.com/qldb/latest/developerguide/API_GetDigest.html) — 返回日记账中最新提交区块的分类账摘要。响应包括一个 256 位的哈希值和一个块地址。

有关使用请求摘要的信息 AWS CLI，请参阅《命令参考》中的 [get-diges](https://docs.aws.amazon.com/cli/latest/reference/qldb/get-digest.html) t *AWS CLI 命令*。

### 示例应用程序
<a name="verification.digest.api.sample"></a>

有关 Java 代码示例，请参阅 GitHub 存储库 a [ws-samples/-amazon-qldb-dmv-sample](https://github.com/aws-samples/amazon-qldb-dmv-sample-java) java。有关如何下载和安装此示例应用程序的说明，请参阅 [安装 Amazon QLDB Java 示例应用程序](sample-app.java.md)。在请求摘要之前，请确保按照 [Java 教程](getting-started.java.tutorial.md) 中的步骤 1-3 创建一个示例分类帐并用示例数据加载它。

课堂中的教程代码[GetDigest](https://github.com/aws-samples/amazon-qldb-dmv-sample-java/blob/master/src/main/java/software/amazon/qldb/tutorial/GetDigest.java)提供了从`vehicle-registration`示例账本中请求摘要的示例。

要使用您保存的摘要验证文档修订，请继续执行 [步骤 2：在 QLDB 中验证您的数据](verification.verify.md)。

# 步骤 2：在 QLDB 中验证您的数据
<a name="verification.verify"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

Amazon QLDB 提供了一个 API，用于请求指定文档ID及其关联块的证明。您还必须提供之前保存的摘要的提示地址，如[步骤 1：在 QLDB 中请求摘要](verification.digest.md)中所述。您可以使用 AWS 管理控制台、S AWS DK 或 AWS CLI 来获取证据。

然后，您可以使用 QLDB 返回的证明通过客户端 API 验证文档修订与保存的摘要是否匹配。这使您能够控制用于验证数据的算法。

**Topics**
+ [AWS 管理控制台](#verification.verify.con)
+ [QLDB API](#verification.verify.api)

## AWS 管理控制台
<a name="verification.verify.con"></a>

本节描述了使用 Amazon QLDB 控制台验证文档修订与先前保存的摘要相匹配的步骤。

开始之前，请确保您已完成 [步骤 1：在 QLDB 中请求摘要](verification.digest.md) 中的步骤。验证需要先前保存的摘要，其中包含您要验证的修订。

**验证文档修订（控制台）**

1. [在 /qldb 上打开亚马逊 QLDB 控制台。https://console.aws.amazon.com](https://console.aws.amazon.com/qldb)

1. 首先，在分类账中查询要验证的文档修订的 `id`和 `blockAddress`。这些字段包含在文档的元数据中，您可以在*提交视图*中查询它们。

   文档 `id` 是系统分配的唯一 ID 字符串。`blockAddress` 是一种 Ion 结构，用于指定提交修订版本的区块位置。

   在导航窗格中，选择 **‬PartiQL 编辑器**。

1. 选择要验证修订的分类帐名称。

1. 在查询编辑器中输入类似以下内容的语句，然后选择 `SELECT`Run Query**（运行查询），或者按 **。

   ```
   SELECT metadata.id, blockAddress FROM _ql_committed_table_name
   WHERE criteria
   ```

   例如，以下查询从在 [Amazon QLDB 控制台入门](getting-started.md) 中创建的示例分类帐中的 `VehicleRegistration` 表返回一个文档。

   ```
   SELECT r.metadata.id, r.blockAddress FROM _ql_committed_VehicleRegistration AS r 
   WHERE r.data.VIN = 'KM8SRDHF6EU074761'
   ```

1. 复制并保存查询返回的 `id` 和 `blockAddress` 值。请务必省略 `id` 字段的双引号。在 [Amazon Ion](ion.md) 中，字符串数据类型用双引号分隔。例如，您必须仅复制以下代码片段中的字母数字文本。

   `"LtMNJYNjSwzBLgf7sLifrG"`

1. 您已经选择了文档修订版，现在可以开始对其进行验证。

   在导航窗格中选择**验证**。

1. 在“**验证文档**”表单中，在**“指定要验证的文档”**下，输入以下输入参数：
   + **分类账**-您要在其中验证修订的分类账。
   + **区块地址** - 您在步骤 4 中查询返回的`blockAddress`值。
   + **文档 ID** - 您在步骤 4 中查询返回的 `id` 值。

1. 在“**指定要验证的摘要**”下，通过选择“**选择摘要**”，选择之前保存的摘要。如果文件有效，则会自动填充控制台上的所有摘要字段。或者，您可以直接从摘要文件中手动复制和粘贴以下值：
   + **摘要** - 摘要文件中的 `digest` 值。
   + **摘要提示地址** - 摘要文件中的 `digestTipAddress` 值。

1. 查看您的文档和摘要输入参数，然后选择**Verify（验证）**。

   控制台为您自动执行两个步骤：

   1. 向 QLDB 请求指定文档的证明。

   1. 使用 QLDB 返回的证明来调用客户端 API，该API会根据提供的摘要验证您的文档修订版本。要查看此验证算法，请参阅下一节[QLDB API](#verification.verify.api)以下载代码示例。

   控制台在**验证结果**卡中显示您的请求结果。有关更多信息，请参阅[验证结果](verification.results.md)。

## QLDB API
<a name="verification.verify.api"></a>

您还可以使用 Amazon QLDB API 与相关 AWS SDK 或者使用 AWS CLI，通过向 Amazon QLDB 请求验证文档修订。QLDB API 提供以下操作以供应用程序使用：
+ `GetDigest` — 返回日记账中最新提交区块的分类账摘要。响应包括一个 256 位的哈希值和一个块地址。
+ `GetBlock` — 返回日记账中指定地址的数据块对象。如果 `DigestTipAddress` 已提供，还会返回指定数据块的证明以供验证。
+ `GetRevision` — 返回指定文档 ID 和块地址的修订数据对象。如果 `DigestTipAddress` 已提供，还将返回指定修订的用于验证的证明。

有关这些 API 操作的完整介绍，请参阅 [Amazon QLDB API 参考](api-reference.md)。

有关使用验证数据的信息 AWS CLI，请参阅《[AWS CLI 命令参考](https://docs.aws.amazon.com/cli/latest/reference/qldb/index.html)》。

### 示例应用程序
<a name="verification.verify.api.sample"></a>

有关 Java 代码示例，请参阅 GitHub 存储库 a [ws-samples/-amazon-qldb-dmv-sample](https://github.com/aws-samples/amazon-qldb-dmv-sample-java) java。有关如何下载和安装此示例应用程序的说明，请参阅 [安装 Amazon QLDB Java 示例应用程序](sample-app.java.md)。在进行验证之前，请确保按照 [Java 教程](getting-started.java.tutorial.md) 中的步骤 1-3 创建一个示例分类帐并用示例数据加载它。

课堂中的教程代码[GetRevision](https://github.com/aws-samples/amazon-qldb-dmv-sample-java/blob/master/src/main/java/software/amazon/qldb/tutorial/GetRevision.java)提供了一个示例，说明如何请求文档修订版的证据，然后验证该修订版。此类运行以下步骤：

1. 从示例分类账中请求新的摘要`vehicle-registration`。

1. 请求从 `vehicle-registration` 分类帐中的 `VehicleRegistration` 表中获取示例文档修订的证明。

1. 使用返回的摘要和证明验证示例修订。

# 验证结果
<a name="verification.results"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节介绍上的 Amazon QLDB 数据验证请求返回的结果。 AWS 管理控制台有关如何提交验证请求的详细步骤，请参阅[步骤 2：在 QLDB 中验证您的数据](verification.verify.md)。

在 QLDB 控制台的**验证**页面上，您的请求结果显示在**验证结果卡**中。**证明**选项卡显示了 QLDB 为您指定的文档修订和摘要返回的证明的内容。其中包括以下内容：
+ **修订哈希** — SHA-256 值，唯一表示您正在验证的文档修订。
+ **证明哈希** — QLDB 提供的用于重新计算指定摘要的有序哈希列表。控制台从**修订哈希**开始，并依次与每个证明哈希组合，直到最终得到重新计算的摘要。

  默认情况下，列表处于折叠状态，因此您可以将其展开以显示哈希值。可选择按照 [使用证明重新计算您的摘要](#verification.results.recalc) 中描述的步骤自行尝试进行哈希计算。
+ **计算的摘要** - 在**修订哈希**上进行的一系列**哈希计算**产生的哈希值。如果此值与您之前保存的**摘要**匹配，验证就成功了。

“**区块**”选项卡显示包含您正在验证的修订的块的内容。其中包括以下内容：
+ **事务 ID** — 提交数据块的事务唯一 ID。
+ **事务时间** — 数据块提交到链的时间戳。
+ **区块哈希** — 唯一表示此区块及其所有内容的 SHA-256 值。
+ **区块地址** — 分类账日记账中提交区块的位置。地址包含以下两个字段：
  + **Strand** ID — 包含数据块的日记账链的唯一 ID。
  + **序列号** — 索引号，用于指定数据块在链中的位置。
+ **语句** — 执行的 PartiQL 语句，用于提交该块中的条目。
**注意**  
如果以编程方式运行参数化语句，它们将以绑定参数的形式记录在您的日记账区块中，而不是文字数据。例如，您可能会在日记块中看到以下语句，其中问号（`?`）是文档内容的变量占位符。  

  ```
  INSERT INTO Vehicle ?
  ```
+ **文档条目** - 在该块中提交的文档修订。

如果您的请求未能验证文档修订，请参阅 [常见的验证错误](verification.errors.md) 以获取可能原因的信息。

## 使用证明重新计算您的摘要
<a name="verification.results.recalc"></a>

在 QLDB 返回您的文档验证请求的证明后，您可以尝试自行进行哈希计算。本节概述了使用提供的证明重新计算摘要的高层步骤。

首先，将您的**修订哈希**与**证明哈希**列表中的第一个哈希配对。然后执行以下操作。

1. 对两个哈希进行排序。按照小端字节顺序比较这两个哈希的*签名*字节值。

1. 按照排序顺序连接这两个哈希。

1. 使用 SHA-256 哈希生成器对连接的对进行哈希处理。

1. 将您的新哈希与证明中的下一个哈希配对，并重复步骤 1-3。在您处理最后一个证明哈希之后，您的新哈希就是您重新计算的摘要。

如果您重新计算的摘要与之前保存的摘要相匹配，则说明您的文档已成功验证。

有关演示这些验证步骤的代码示例的 step-by-step教程，请继续[教程：使用 AWS 软件开发工具包验证数据](verification.tutorial-block-hash.md)。

# 教程：使用 AWS 软件开发工具包验证数据
<a name="verification.tutorial-block-hash"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

在本教程中，您将通过软件开发工具包使用 QLDB API 来验证 Amazon QLDB 账本中的文档修订哈希和日志区块哈希。 AWS 您还可以使用 QLDB 驱动程序来查询文档修订。

举一个例子，您有一个文档修订，其中包含车辆识别码（VIN）为 `KM8SRDHF6EU074761` 的车辆的数据。文档修订位于一个名为 `vehicle-registration` 的分类帐中的一个名为 `VehicleRegistration` 的表中。假设您想要验证这辆车的文档修订以及包含修订的日记账区块的完整性。

**注意**  
有关在现实用例背景下讨论加密验证价值的详细 AWS 博客文章，请参阅使用 [Amazon QLDB 进行真实世界的加密验证](https://aws.amazon.com/blogs/database/real-world-cryptographic-verification-with-amazon-qldb/)。

**Topics**
+ [先决条件](#verification.tutorial.prereqs)
+ [步骤 1：请求摘要](#verification.tutorial.step-1)
+ [步骤 2：查询文档修订](#verification.tutorial.step-2)
+ [第 3 步：请求修订证明](#verification.tutorial.step-3)
+ [步骤 4：重新计算修订中的摘要](#verification.tutorial.step-4)
+ [第 5 步：申请日记账区块的证明](#verification.tutorial.step-5)
+ [步骤 6：重新计算区块中的摘要](#verification.tutorial.step-6)
+ [运行完整的代码示例](#verification.tutorial.full)

## 先决条件
<a name="verification.tutorial.prereqs"></a>

在开始之前，请务必执行以下操作：

1. 通过完成 [Amazon QLDB 驱动程序入门](getting-started-driver.md) 下的相应先决条件，为您选择的语言设置 QLDB 驱动程序。这包括注册 AWS、授予开发所需的编程访问权限以及配置您的开发环境。

1. 按照 [Amazon QLDB 控制台入门](getting-started.md) 中的步骤 1-2，创建一个名为 `vehicle-registration` 的分类帐，并加载预定义的示例数据。

接下来，查看以下步骤以了解验证的工作原理，然后运行从头到尾的完整代码示例。

## 步骤 1：请求摘要
<a name="verification.tutorial.step-1"></a>

在您可以验证数据之前，您首先需要从您的分类帐`vehicle-registration`中请求一个摘要以供以后使用。

------
#### [ Java ]

```
// Get a digest
GetDigestRequest digestRequest = new GetDigestRequest().withName(ledgerName);
GetDigestResult digestResult = client.getDigest(digestRequest);

java.nio.ByteBuffer digest = digestResult.getDigest();

// expectedDigest is the buffer we will use later to compare against our calculated digest
byte[] expectedDigest = new byte[digest.remaining()];
digest.get(expectedDigest);
```

------
#### [ .NET ]

```
// Get a digest
GetDigestRequest getDigestRequest = new GetDigestRequest
{
    Name = ledgerName
};
GetDigestResponse getDigestResponse = client.GetDigestAsync(getDigestRequest).Result;

// expectedDigest is the buffer we will use later to compare against our calculated digest
MemoryStream digest = getDigestResponse.Digest;
byte[] expectedDigest = digest.ToArray();
```

------
#### [ Go ]

```
// Get a digest
currentLedgerName := ledgerName
input := qldb.GetDigestInput{Name: &currentLedgerName}
digestOutput, err := client.GetDigest(&input)
if err != nil {
    panic(err)
}

// expectedDigest is the buffer we will later use to compare against our calculated digest
expectedDigest := digestOutput.Digest
```

------
#### [ Node.js ]

```
// Get a digest
const getDigestRequest: GetDigestRequest = {
    Name: ledgerName
};
const getDigestResponse: GetDigestResponse = await qldbClient.getDigest(getDigestRequest).promise();

// expectedDigest is the buffer we will later use to compare against our calculated digest
const expectedDigest: Uint8Array = <Uint8Array>getDigestResponse.Digest;
```

------
#### [ Python ]

```
# Get a digest
get_digest_response = qldb_client.get_digest(Name=ledger_name)

# expected_digest is the buffer we will later use to compare against our calculated digest
expected_digest = get_digest_response.get('Digest')
digest_tip_address = get_digest_response.get('DigestTipAddress')
```

------

## 步骤 2：查询文档修订
<a name="verification.tutorial.step-2"></a>

使用 QLDB 驱动程序查询与 VIN 关联的区块地址、哈希值和 IDs 文档。`KM8SRDHF6EU074761`

------
#### [ Java ]

```
// Retrieve info for the given vin's document revisions
Result result = driver.execute(txn -> {
    final String query = String.format("SELECT blockAddress, hash, metadata.id FROM _ql_committed_%s WHERE data.VIN = '%s'", tableName, vin);
    return txn.execute(query);
});
```

------
#### [ .NET ]

```
// Retrieve info for the given vin's document revisions
var result = driver.Execute(txn => {
    string query = $"SELECT blockAddress, hash, metadata.id FROM _ql_committed_{tableName} WHERE data.VIN = '{vin}'";
    return txn.Execute(query);
});
```

------
#### [ Go ]

```
// Retrieve info for the given vin's document revisions
result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
    statement := fmt.Sprintf(
            "SELECT blockAddress, hash, metadata.id FROM _ql_committed_%s WHERE data.VIN = '%s'",
            tableName,
            vin)
    result, err := txn.Execute(statement)
    if err != nil {
        return nil, err
    }

    results := make([]map[string]interface{}, 0)

    // Convert the result set into a map
    for result.Next(txn) {
        var doc map[string]interface{}
        err := ion.Unmarshal(result.GetCurrentData(), &doc)
        if err != nil {
            return nil, err
        }
        results = append(results, doc)
    }
    return results, nil
})
if err != nil {
    panic(err)
}
resultSlice := result.([]map[string]interface{})
```

------
#### [ Node.js ]

```
const result: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor): Promise<dom.Value[]> => {
    const query: string = `SELECT blockAddress, hash, metadata.id FROM _ql_committed_${tableName} WHERE data.VIN = '${vin}'`;
    const queryResult: Result = await txn.execute(query);
    return queryResult.getResultList();
});
```

------
#### [ Python ]

```
def query_doc_revision(txn):
    query = "SELECT blockAddress, hash, metadata.id FROM _ql_committed_{} WHERE data.VIN = '{}'".format(table_name, vin)
    return txn.execute_statement(query)

# Retrieve info for the given vin's document revisions
result = qldb_driver.execute_lambda(query_doc_revision)
```

------

## 第 3 步：请求修订证明
<a name="verification.tutorial.step-3"></a>

遍历查询结果，对于每个块地址和文档 ID，以及分类帐名称，使用 QLDB 驱动程序提交一个 `GetRevision`请求。*要获得修订的证明，您还必须提供先前保存的摘要中的提示地址。*此 API 操作返回一个对象，其中包含文档修订和修订证明。

有关修订结构及其内容的信息，请参见[查询文档元数据](working.metadata.md)。

------
#### [ Java ]

```
for (IonValue ionValue : result) {
    IonStruct ionStruct = (IonStruct)ionValue;

    // Get the requested fields
    IonValue blockAddress = ionStruct.get("blockAddress");
    IonBlob hash = (IonBlob)ionStruct.get("hash");
    String metadataId = ((IonString)ionStruct.get("id")).stringValue();

    System.out.printf("Verifying document revision for id '%s'%n", metadataId);

    String blockAddressText = blockAddress.toString();

    // Submit a request for the revision
    GetRevisionRequest revisionRequest = new GetRevisionRequest()
            .withName(ledgerName)
            .withBlockAddress(new ValueHolder().withIonText(blockAddressText))
            .withDocumentId(metadataId)
            .withDigestTipAddress(digestResult.getDigestTipAddress());

    // Get a result back
    GetRevisionResult revisionResult = client.getRevision(revisionRequest);

    ...
}
```

------
#### [ .NET ]

```
foreach (IIonValue ionValue in result)
{
    IIonStruct ionStruct = ionValue;

    // Get the requested fields
    IIonValue blockAddress = ionStruct.GetField("blockAddress");
    IIonBlob hash = ionStruct.GetField("hash");
    String metadataId = ionStruct.GetField("id").StringValue;

    Console.WriteLine($"Verifying document revision for id '{metadataId}'");

    // Use an Ion Reader to convert block address to text
    IIonReader reader = IonReaderBuilder.Build(blockAddress);
    StringWriter sw = new StringWriter();
    IIonWriter textWriter = IonTextWriterBuilder.Build(sw);
    textWriter.WriteValues(reader);
    string blockAddressText = sw.ToString();

    // Submit a request for the revision
    GetRevisionRequest revisionRequest = new GetRevisionRequest
    {
        Name = ledgerName,
        BlockAddress = new ValueHolder
        {
            IonText = blockAddressText
        },
        DocumentId = metadataId,
        DigestTipAddress = getDigestResponse.DigestTipAddress
    };
    
    // Get a response back
    GetRevisionResponse revisionResponse = client.GetRevisionAsync(revisionRequest).Result;

    ...
}
```

------
#### [ Go ]

```
for _, value := range resultSlice {
    // Get the requested fields
    ionBlockAddress, err := ion.MarshalText(value["blockAddress"])
    if err != nil {
        panic(err)
    }
    blockAddress := string(ionBlockAddress)
    metadataId := value["id"].(string)
    documentHash := value["hash"].([]byte)

    fmt.Printf("Verifying document revision for id '%s'\n", metadataId)

    // Submit a request for the revision
    revisionInput := qldb.GetRevisionInput{
        BlockAddress:     &qldb.ValueHolder{IonText: &blockAddress},
        DigestTipAddress: digestOutput.DigestTipAddress,
        DocumentId:       &metadataId,
        Name:             &currentLedgerName,
    }

    // Get a result back
    revisionOutput, err := client.GetRevision(&revisionInput)
    if err != nil {
        panic(err)
    }

    ...
}
```

------
#### [ Node.js ]

```
for (let value of result) {
    // Get the requested fields
    const blockAddress: dom.Value = value.get("blockAddress");
    const hash: dom.Value = value.get("hash");
    const metadataId: string = value.get("id").stringValue();

    console.log(`Verifying document revision for id '${metadataId}'`);

    // Submit a request for the revision
    const revisionRequest: GetRevisionRequest = {
        Name: ledgerName,
        BlockAddress: {
            IonText: dumpText(blockAddress)
        },
        DocumentId: metadataId,
        DigestTipAddress: getDigestResponse.DigestTipAddress
    };

    // Get a response back
    const revisionResponse: GetRevisionResponse = await qldbClient.getRevision(revisionRequest).promise();

    ...
}
```

------
#### [ Python ]

```
for value in result:
    # Get the requested fields
    block_address = value['blockAddress']
    document_hash = value['hash']
    metadata_id = value['id']

    print("Verifying document revision for id '{}'".format(metadata_id))

    # Submit a request for the revision and get a result back
    proof_response = qldb_client.get_revision(Name=ledger_name,
                                              BlockAddress=block_address_to_dictionary(block_address),
                                              DocumentId=metadata_id,
                                              DigestTipAddress=digest_tip_address)
```

------

然后，检索所请求修订的证明。

QLDB API 以节点哈希的有序列表的字符串表示形式返回证明。要将此字符串转换为节点哈希的二进制表示形式的列表，您可以使用 Amazon Ion 库中的 Ion 读取器。有关使用 Ion 库的更多信息，请参阅 [Amazon Ion 说明书](http://amzn.github.io/ion-docs/guides/cookbook.html#reading-and-writing-ion-data)。

------
#### [ Java ]

在此示例中，您使用`IonReader`进行二进制转换。

```
String proofText = revisionResult.getProof().getIonText();

// Take the proof and convert it to a list of byte arrays
List<byte[]> internalHashes = new ArrayList<>();
IonReader reader = SYSTEM.newReader(proofText);
reader.next();
reader.stepIn();
while (reader.next() != null) {
    internalHashes.add(reader.newBytes());
}
```

------
#### [ .NET ]

在这个例子中，您使用 `IonLoader` 加载证明到一个 Ion 数据包中。

```
string proofText = revisionResponse.Proof.IonText;
IIonDatagram proofValue = IonLoader.Default.Load(proofText);
```

------
#### [ Go ]

在此示例中，您使用 Ion 读取器将证明转换为二进制，并遍历证明的节点哈希列表。

```
proofText := revisionOutput.Proof.IonText

// Use ion.Reader to iterate over the proof's node hashes
reader := ion.NewReaderString(*proofText)
// Enter the struct containing node hashes
reader.Next()
if err := reader.StepIn(); err != nil {
    panic(err)
}
```

------
#### [ Node.js ]

在此示例中，您使用`load`函数进行二进制转换。

```
let proofValue: dom.Value = load(revisionResponse.Proof.IonText);
```

------
#### [ Python ]

在此示例中，您使用`loads`函数进行二进制转换。

```
proof_text = proof_response.get('Proof').get('IonText')
proof_hashes = loads(proof_text)
```

------

## 步骤 4：重新计算修订中的摘要
<a name="verification.tutorial.step-4"></a>

使用证明的哈希列表重新计算摘要，从修订哈希开始。只要之前保存的摘要在 QLDB 之外是已知和受信任的，如果重新计算的摘要哈希与保存的摘要哈希匹配，就证明了文档修订的完整性。

------
#### [ Java ]

```
// Calculate digest
byte[] calculatedDigest = internalHashes.stream().reduce(hash.getBytes(), BlockHashVerification::dot);

boolean verified = Arrays.equals(expectedDigest, calculatedDigest);

if (verified) {
    System.out.printf("Successfully verified document revision for id '%s'!%n", metadataId);
} else {
    System.out.printf("Document revision for id '%s' verification failed!%n", metadataId);
    return;
}
```

------
#### [ .NET ]

```
byte[] documentHash = hash.Bytes().ToArray();
foreach (IIonValue proofHash in proofValue.GetElementAt(0))
{
    // Calculate the digest
    documentHash = Dot(documentHash, proofHash.Bytes().ToArray());
}

bool verified = expectedDigest.SequenceEqual(documentHash);

if (verified)
{
    Console.WriteLine($"Successfully verified document revision for id '{metadataId}'!");
}
else
{
    Console.WriteLine($"Document revision for id '{metadataId}' verification failed!");
    return;
}
```

------
#### [ Go ]

```
// Going through nodes and calculate digest
for reader.Next() {
    val, _ := reader.ByteValue()
    documentHash, err = dot(documentHash, val)
}

// Compare documentHash with the expected digest
verified := reflect.DeepEqual(documentHash, expectedDigest)

if verified {
    fmt.Printf("Successfully verified document revision for id '%s'!\n", metadataId)
} else {
    fmt.Printf("Document revision for id '%s' verification failed!\n", metadataId)
    return
}
```

------
#### [ Node.js ]

```
let documentHash: Uint8Array = hash.uInt8ArrayValue();
proofValue.elements().forEach((proofHash: dom.Value) => {
    // Calculate the digest
    documentHash = dot(documentHash, proofHash.uInt8ArrayValue());
});

let verified: boolean = isEqual(expectedDigest, documentHash);

if (verified) {
    console.log(`Successfully verified document revision for id '${metadataId}'!`);
} else {
    console.log(`Document revision for id '${metadataId}' verification failed!`);
    return;
}
```

------
#### [ Python ]

```
# Calculate digest
calculated_digest = reduce(dot, proof_hashes, document_hash)

verified = calculated_digest == expected_digest
if verified:
    print("Successfully verified document revision for id '{}'!".format(metadata_id))
else:
    print("Document revision for id '{}' verification failed!".format(metadata_id))
```

------

## 第 5 步：申请日记账区块的证明
<a name="verification.tutorial.step-5"></a>

接下来，验证包含文档修订的日记账区块。

使用您在[步骤 1](#verification.tutorial.step-1) 中保存的摘要中的屏蔽地址和提示地址提交`GetBlock`请求。与[步骤 2](#verification.tutorial.step-2) 中的`GetRevision`请求类似，*您必须再次提供已保存摘要中的提示地址才能获得区块的证明。*此 API 操作返回一个包含区块和区块证明的对象。

有关日记账区块结构及其内容的信息，请参见[Amazon QLDB 中的日记账内容](journal-contents.md)。

------
#### [ Java ]

```
// Submit a request for the block
GetBlockRequest getBlockRequest = new GetBlockRequest()
        .withName(ledgerName)
        .withBlockAddress(new ValueHolder().withIonText(blockAddressText))
        .withDigestTipAddress(digestResult.getDigestTipAddress());

// Get a result back
GetBlockResult getBlockResult = client.getBlock(getBlockRequest);
```

------
#### [ .NET ]

```
// Submit a request for the block
GetBlockRequest getBlockRequest = new GetBlockRequest
{
    Name = ledgerName,
    BlockAddress = new ValueHolder
    {
        IonText = blockAddressText
    },
    DigestTipAddress = getDigestResponse.DigestTipAddress
};

// Get a response back
GetBlockResponse getBlockResponse = client.GetBlockAsync(getBlockRequest).Result;
```

------
#### [ Go ]

```
// Submit a request for the block
blockInput := qldb.GetBlockInput{
    Name:             &currentLedgerName,
    BlockAddress:     &qldb.ValueHolder{IonText: &blockAddress},
    DigestTipAddress: digestOutput.DigestTipAddress,
}

// Get a result back
blockOutput, err := client.GetBlock(&blockInput)
if err != nil {
    panic(err)
}
```

------
#### [ Node.js ]

```
// Submit a request for the block
const getBlockRequest: GetBlockRequest = {
    Name: ledgerName,
    BlockAddress: {
        IonText: dumpText(blockAddress)
    },
    DigestTipAddress: getDigestResponse.DigestTipAddress
};

// Get a response back
const getBlockResponse: GetBlockResponse = await qldbClient.getBlock(getBlockRequest).promise();
```

------
#### [ Python ]

```
def block_address_to_dictionary(ion_dict):
"""
Convert a block address from IonPyDict into a dictionary.
Shape of the dictionary must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}

:type ion_dict: :py:class:`amazon.ion.simple_types.IonPyDict`/str
:param ion_dict: The block address value to convert.

:rtype: dict
:return: The converted dict.
"""
block_address = {'IonText': {}}
if not isinstance(ion_dict, str):
    py_dict = '{{strandId: "{}", sequenceNo:{}}}'.format(ion_dict['strandId'], ion_dict['sequenceNo'])
    ion_dict = py_dict
block_address['IonText'] = ion_dict
return block_address

# Submit a request for the block and get a result back
block_response = qldb_client.get_block(Name=ledger_name, BlockAddress=block_address_to_dictionary(block_address),
                                       DigestTipAddress=digest_tip_address)
```

------

然后，从结果中检索区块哈希值和证明。

------
#### [ Java ]

在此示例中，您使用 `IonLoader` 将区块对象加载到 `IonDatagram` 容器中。

```
String blockText = getBlockResult.getBlock().getIonText();

IonDatagram datagram = SYSTEM.getLoader().load(blockText);
ionStruct = (IonStruct)datagram.get(0);

final byte[] blockHash = ((IonBlob)ionStruct.get("blockHash")).getBytes();
```

您还可以使用 `IonLoader` 将证明加载到 `IonDatagram`。

```
proofText = getBlockResult.getProof().getIonText();

// Take the proof and create a list of hash binary data
datagram = SYSTEM.getLoader().load(proofText);
ListIterator<IonValue> listIter = ((IonList)datagram.iterator().next()).listIterator();

internalHashes.clear();
while (listIter.hasNext()) {
    internalHashes.add(((IonBlob)listIter.next()).getBytes());
}
```

------
#### [ .NET ]

在此示例中，您使用 `IonLoader` 将区块和证明分别加载到离子数据报中。

```
string blockText = getBlockResponse.Block.IonText;
IIonDatagram blockValue = IonLoader.Default.Load(blockText);

// blockValue is a IonDatagram, and the first value is an IonStruct containing the blockHash
byte[] blockHash = blockValue.GetElementAt(0).GetField("blockHash").Bytes().ToArray();

proofText = getBlockResponse.Proof.IonText;
proofValue = IonLoader.Default.Load(proofText);
```

------
#### [ Go ]

在此示例中，您使用 Ion 读取器将证明转换为二进制，并遍历证明的节点哈希列表。

```
proofText = blockOutput.Proof.IonText

block := new(map[string]interface{})
err = ion.UnmarshalString(*blockOutput.Block.IonText, block)
if err != nil {
    panic(err)
}

blockHash := (*block)["blockHash"].([]byte)

// Use ion.Reader to iterate over the proof's node hashes
reader = ion.NewReaderString(*proofText)
// Enter the struct containing node hashes
reader.Next()
if err := reader.StepIn(); err != nil {
    panic(err)
}
```

------
#### [ Node.js ]

在此示例中，您使用`load`函数将区块和证明转换为二进制。

```
const blockValue: dom.Value = load(getBlockResponse.Block.IonText)
let blockHash: Uint8Array = blockValue.get("blockHash").uInt8ArrayValue();

proofValue = load(getBlockResponse.Proof.IonText);
```

------
#### [ Python ]

在此示例中，您使用`loads`函数将区块和证明转换为二进制。

```
block_text = block_response.get('Block').get('IonText')
block = loads(block_text)

block_hash = block.get('blockHash')

proof_text = block_response.get('Proof').get('IonText')
proof_hashes = loads(proof_text)
```

------

## 步骤 6：重新计算区块中的摘要
<a name="verification.tutorial.step-6"></a>

使用证明的哈希列表重新计算摘要，从区块哈希开始。只要之前保存的摘要在 QLDB 之外是已知和受信任的，如果重新计算的摘要哈希与保存的摘要哈希匹配，就证明了区块的完整性。

------
#### [ Java ]

```
// Calculate digest
calculatedDigest = internalHashes.stream().reduce(blockHash, BlockHashVerification::dot);

verified = Arrays.equals(expectedDigest, calculatedDigest);

if (verified) {
    System.out.printf("Block address '%s' successfully verified!%n", blockAddressText);
} else {
    System.out.printf("Block address '%s' verification failed!%n", blockAddressText);
}
```

------
#### [ .NET ]

```
foreach (IIonValue proofHash in proofValue.GetElementAt(0))
{
    // Calculate the digest
    blockHash = Dot(blockHash, proofHash.Bytes().ToArray());
}

verified = expectedDigest.SequenceEqual(blockHash);

if (verified)
{
    Console.WriteLine($"Block address '{blockAddressText}' successfully verified!");
}
else
{
    Console.WriteLine($"Block address '{blockAddressText}' verification failed!");
}
```

------
#### [ Go ]

```
// Going through nodes and calculate digest
for reader.Next() {
    val, err := reader.ByteValue()
    if err != nil {
        panic(err)
    }
    blockHash, err = dot(blockHash, val)
}

// Compare blockHash with the expected digest
verified = reflect.DeepEqual(blockHash, expectedDigest)

if verified {
    fmt.Printf("Block address '%s' successfully verified!\n", blockAddress)
} else {
    fmt.Printf("Block address '%s' verification failed!\n", blockAddress)
    return
}
```

------
#### [ Node.js ]

```
proofValue.elements().forEach((proofHash: dom.Value) => {
    // Calculate the digest
    blockHash = dot(blockHash, proofHash.uInt8ArrayValue());
});

verified = isEqual(expectedDigest, blockHash);

if (verified) {
    console.log(`Block address '${dumpText(blockAddress)}' successfully verified!`);
} else {
    console.log(`Block address '${dumpText(blockAddress)}' verification failed!`);
}
```

------
#### [ Python ]

```
# Calculate digest
calculated_digest = reduce(dot, proof_hashes, block_hash)

verified = calculated_digest == expected_digest
if verified:
    print("Block address '{}' successfully verified!".format(dumps(block_address,
                                                                   binary=False,
                                                                   omit_version_marker=True)))
else:
    print("Block address '{}' verification failed!".format(block_address))
```

------

前面的代码示例在重新计算摘要时使用以下`dot`函数。这个函数接受两个哈希作为输入，对它们进行排序，连接它们，然后返回连接数组的哈希值。

------
#### [ Java ]

```
/**
 * Takes two hashes, sorts them, concatenates them, and then returns the
 * hash of the concatenated array.
 *
 * @param h1
 *              Byte array containing one of the hashes to compare.
 * @param h2
 *              Byte array containing one of the hashes to compare.
 * @return the concatenated array of hashes.
 */
public static byte[] dot(final byte[] h1, final byte[] h2) {
    if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
        throw new IllegalArgumentException("Invalid hash.");
    }

    int byteEqual = 0;
    for (int i = h1.length - 1; i >= 0; i--) {
        byteEqual = Byte.compare(h1[i], h2[i]);
        if (byteEqual != 0) {
            break;
        }
    }

    byte[] concatenated = new byte[h1.length + h2.length];
    if (byteEqual < 0) {
        System.arraycopy(h1, 0, concatenated, 0, h1.length);
        System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
    } else {
        System.arraycopy(h2, 0, concatenated, 0, h2.length);
        System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
    }

    MessageDigest messageDigest;
    try {
        messageDigest = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
        throw new IllegalStateException("SHA-256 message digest is unavailable", e);
    }

    messageDigest.update(concatenated);
    return messageDigest.digest();
}
```

------
#### [ .NET ]

```
/// <summary>
/// Takes two hashes, sorts them, concatenates them, and then returns the
/// hash of the concatenated array.
/// </summary>
/// <param name="h1">Byte array containing one of the hashes to compare.</param>
/// <param name="h2">Byte array containing one of the hashes to compare.</param>
/// <returns>The concatenated array of hashes.</returns>
private static byte[] Dot(byte[] h1, byte[] h2)
{
    if (h1.Length == 0)
    {
        return h2;
    }

    if (h2.Length == 0)
    {
        return h1;
    }

    HashAlgorithm hashAlgorithm = HashAlgorithm.Create("SHA256");
    HashComparer comparer = new HashComparer();
    if (comparer.Compare(h1, h2) < 0)
    {
        return hashAlgorithm.ComputeHash(h1.Concat(h2).ToArray());
    }
    else
    {
        return hashAlgorithm.ComputeHash(h2.Concat(h1).ToArray());
    }
}

private class HashComparer : IComparer<byte[]>
{
    private static readonly int HASH_LENGTH = 32;

    public int Compare(byte[] h1, byte[] h2)
    {
        if (h1.Length != HASH_LENGTH || h2.Length != HASH_LENGTH)
        {
            throw new ArgumentException("Invalid hash");
        }

        for (var i = h1.Length - 1; i >= 0; i--)
        {
            var byteEqual = (sbyte)h1[i] - (sbyte)h2[i];
            if (byteEqual != 0)
            {
                return byteEqual;
            }
        }

        return 0;
    }
}
```

------
#### [ Go ]

```
// Takes two hashes, sorts them, concatenates them, and then returns the hash of the concatenated array.
func dot(h1, h2 []byte) ([]byte, error) {
    compare, err := hashComparator(h1, h2)
    if err != nil {
        return nil, err
    }

    var concatenated []byte
    if compare < 0 {
        concatenated = append(h1, h2...)
    } else {
        concatenated = append(h2, h1...)
    }

    newHash := sha256.Sum256(concatenated)
    return newHash[:], nil
}

func hashComparator(h1 []byte, h2 []byte) (int16, error) {
    if len(h1) != hashLength || len(h2) != hashLength {
        return 0, errors.New("invalid hash")
    }
    for i := range h1 {
        // Reverse index for little endianness
        index := hashLength - 1 - i

        // Handle byte being unsigned and overflow
        h1Int := int16(h1[index])
        h2Int := int16(h2[index])
        if h1Int > 127 {
            h1Int = 0 - (256 - h1Int)
        }
        if h2Int > 127 {
            h2Int = 0 - (256 - h2Int)
        }

        difference := h1Int - h2Int
        if difference != 0 {
            return difference, nil
        }
    }
    return 0, nil
}
```

------
#### [ Node.js ]

```
/**
 * Takes two hashes, sorts them, concatenates them, and calculates a digest based on the concatenated hash.
 * @param h1 Byte array containing one of the hashes to compare.
 * @param h2 Byte array containing one of the hashes to compare.
 * @returns The digest calculated from the concatenated hash values.
 */
function dot(h1: Uint8Array, h2: Uint8Array): Uint8Array {
    if (h1.length === 0) {
        return h2;
    }
    if (h2.length === 0) {
        return h1;
    }

    const newHashLib = createHash("sha256");

    let concatenated: Uint8Array;
    if (hashComparator(h1, h2) < 0) {
        concatenated = concatenate(h1, h2);
    } else {
        concatenated = concatenate(h2, h1);
    }
    newHashLib.update(concatenated);
    return newHashLib.digest();
}

/**
 * Compares two hashes by their **signed** byte values in little-endian order.
 * @param hash1 The hash value to compare.
 * @param hash2 The hash value to compare.
 * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching
 *          bytes.
 * @throws RangeError When the hash is not the correct hash size.
 */
function hashComparator(hash1: Uint8Array, hash2: Uint8Array): number {
    if (hash1.length !== HASH_SIZE || hash2.length !== HASH_SIZE) {
        throw new RangeError("Invalid hash.");
    }
    for (let i = hash1.length-1; i >= 0; i--) {
        const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24);
        if (difference !== 0) {
            return difference;
        }
    }
    return 0;
}

/**
 * Helper method that concatenates two Uint8Array.
 * @param arrays List of arrays to concatenate, in the order provided.
 * @returns The concatenated array.
 */
function concatenate(...arrays: Uint8Array[]): Uint8Array {
    let totalLength = 0;
    for (const arr of arrays) {
        totalLength += arr.length;
    }
    const result = new Uint8Array(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

/**
 * Helper method that checks for equality between two Uint8Array.
 * @param expected Byte array containing one of the hashes to compare.
 * @param actual Byte array containing one of the hashes to compare.
 * @returns Boolean indicating equality between the two Uint8Array.
 */
function isEqual(expected: Uint8Array, actual: Uint8Array): boolean {
    if (expected === actual) return true;
    if (expected == null || actual == null) return false;
    if (expected.length !== actual.length) return false;

    for (let i = 0; i < expected.length; i++) {
        if (expected[i] !== actual[i]) {
            return false;
        }
    }
    return true;
}
```

------
#### [ Python ]

```
def dot(hash1, hash2):
    """
    Takes two hashes, sorts them, concatenates them, and then returns the
    hash of the concatenated array.

    :type hash1: bytes
    :param hash1: The hash value to compare.

    :type hash2: bytes
    :param hash2: The hash value to compare.

    :rtype: bytes
    :return: The new hash value generated from concatenated hash values.
    """
    if len(hash1) != hash_length or len(hash2) != hash_length:
        raise ValueError('Illegal hash.')

    hash_array1 = array('b', hash1)
    hash_array2 = array('b', hash2)

    difference = 0
    for i in range(len(hash_array1) - 1, -1, -1):
        difference = hash_array1[i] - hash_array2[i]
        if difference != 0:
            break

    if difference < 0:
        concatenated = hash1 + hash2
    else:
        concatenated = hash2 + hash1

    new_hash_lib = sha256()
    new_hash_lib.update(concatenated)
    new_digest = new_hash_lib.digest()
    return new_digest
```

------

## 运行完整的代码示例
<a name="verification.tutorial.full"></a>

运行以下完整的代码示例，以执行从头到尾的所有先前的步骤。

------
#### [ Java ]

```
import com.amazon.ion.IonBlob;
import com.amazon.ion.IonDatagram;
import com.amazon.ion.IonList;
import com.amazon.ion.IonReader;
import com.amazon.ion.IonString;
import com.amazon.ion.IonStruct;
import com.amazon.ion.IonSystem;
import com.amazon.ion.IonValue;
import com.amazon.ion.system.IonSystemBuilder;
import com.amazonaws.services.qldb.AmazonQLDB;
import com.amazonaws.services.qldb.AmazonQLDBClientBuilder;
import com.amazonaws.services.qldb.model.GetBlockRequest;
import com.amazonaws.services.qldb.model.GetBlockResult;
import com.amazonaws.services.qldb.model.GetDigestRequest;
import com.amazonaws.services.qldb.model.GetDigestResult;
import com.amazonaws.services.qldb.model.GetRevisionRequest;
import com.amazonaws.services.qldb.model.GetRevisionResult;
import com.amazonaws.services.qldb.model.ValueHolder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.qldbsession.QldbSessionClient;
import software.amazon.awssdk.services.qldbsession.QldbSessionClientBuilder;
import software.amazon.qldb.QldbDriver;
import software.amazon.qldb.Result;

public class BlockHashVerification {
    private static final IonSystem SYSTEM = IonSystemBuilder.standard().build();
    private static final QldbDriver driver = createQldbDriver();
    private static final AmazonQLDB client = AmazonQLDBClientBuilder.standard().build();
    private static final String region = "us-east-1";
    private static final String ledgerName = "vehicle-registration";
    private static final String tableName = "VehicleRegistration";
    private static final String vin = "KM8SRDHF6EU074761";
    private static final int HASH_LENGTH = 32;

    /**
     * Create a pooled driver for creating sessions.
     *
     * @return The pooled driver for creating sessions.
     */
    public static QldbDriver createQldbDriver() {
        QldbSessionClientBuilder sessionClientBuilder = QldbSessionClient.builder();
        sessionClientBuilder.region(Region.of(region));

        return QldbDriver.builder()
                .ledger(ledgerName)
                .sessionClientBuilder(sessionClientBuilder)
                .build();
    }

    /**
     * Takes two hashes, sorts them, concatenates them, and then returns the
     * hash of the concatenated array.
     *
     * @param h1
     *              Byte array containing one of the hashes to compare.
     * @param h2
     *              Byte array containing one of the hashes to compare.
     * @return the concatenated array of hashes.
     */
    public static byte[] dot(final byte[] h1, final byte[] h2) {
        if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
            throw new IllegalArgumentException("Invalid hash.");
        }

        int byteEqual = 0;
        for (int i = h1.length - 1; i >= 0; i--) {
            byteEqual = Byte.compare(h1[i], h2[i]);
            if (byteEqual != 0) {
                break;
            }
        }

        byte[] concatenated = new byte[h1.length + h2.length];
        if (byteEqual < 0) {
            System.arraycopy(h1, 0, concatenated, 0, h1.length);
            System.arraycopy(h2, 0, concatenated, h1.length, h2.length);
        } else {
            System.arraycopy(h2, 0, concatenated, 0, h2.length);
            System.arraycopy(h1, 0, concatenated, h2.length, h1.length);
        }

        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 message digest is unavailable", e);
        }

        messageDigest.update(concatenated);
        return messageDigest.digest();
    }

    public static void main(String[] args) {
        // Get a digest
        GetDigestRequest digestRequest = new GetDigestRequest().withName(ledgerName);
        GetDigestResult digestResult = client.getDigest(digestRequest);

        java.nio.ByteBuffer digest = digestResult.getDigest();

        // expectedDigest is the buffer we will use later to compare against our calculated digest
        byte[] expectedDigest = new byte[digest.remaining()];
        digest.get(expectedDigest);

        // Retrieve info for the given vin's document revisions
        Result result = driver.execute(txn -> {
            final String query = String.format("SELECT blockAddress, hash, metadata.id FROM _ql_committed_%s WHERE data.VIN = '%s'", tableName, vin);
            return txn.execute(query);
        });

        System.out.printf("Verifying document revisions for vin '%s' in table '%s' in ledger '%s'%n", vin, tableName, ledgerName);

        for (IonValue ionValue : result) {
            IonStruct ionStruct = (IonStruct)ionValue;

            // Get the requested fields
            IonValue blockAddress = ionStruct.get("blockAddress");
            IonBlob hash = (IonBlob)ionStruct.get("hash");
            String metadataId = ((IonString)ionStruct.get("id")).stringValue();

            System.out.printf("Verifying document revision for id '%s'%n", metadataId);

            String blockAddressText = blockAddress.toString();

            // Submit a request for the revision
            GetRevisionRequest revisionRequest = new GetRevisionRequest()
                    .withName(ledgerName)
                    .withBlockAddress(new ValueHolder().withIonText(blockAddressText))
                    .withDocumentId(metadataId)
                    .withDigestTipAddress(digestResult.getDigestTipAddress());

            // Get a result back
            GetRevisionResult revisionResult = client.getRevision(revisionRequest);

            String proofText = revisionResult.getProof().getIonText();

            // Take the proof and convert it to a list of byte arrays
            List<byte[]> internalHashes = new ArrayList<>();
            IonReader reader = SYSTEM.newReader(proofText);
            reader.next();
            reader.stepIn();
            while (reader.next() != null) {
                internalHashes.add(reader.newBytes());
            }

            // Calculate digest
            byte[] calculatedDigest = internalHashes.stream().reduce(hash.getBytes(), BlockHashVerification::dot);

            boolean verified = Arrays.equals(expectedDigest, calculatedDigest);

            if (verified) {
                System.out.printf("Successfully verified document revision for id '%s'!%n", metadataId);
            } else {
                System.out.printf("Document revision for id '%s' verification failed!%n", metadataId);
                return;
            }

            // Submit a request for the block
            GetBlockRequest getBlockRequest = new GetBlockRequest()
                    .withName(ledgerName)
                    .withBlockAddress(new ValueHolder().withIonText(blockAddressText))
                    .withDigestTipAddress(digestResult.getDigestTipAddress());

            // Get a result back
            GetBlockResult getBlockResult = client.getBlock(getBlockRequest);

            String blockText = getBlockResult.getBlock().getIonText();

            IonDatagram datagram = SYSTEM.getLoader().load(blockText);
            ionStruct = (IonStruct)datagram.get(0);

            final byte[] blockHash = ((IonBlob)ionStruct.get("blockHash")).getBytes();

            proofText = getBlockResult.getProof().getIonText();

            // Take the proof and create a list of hash binary data
            datagram = SYSTEM.getLoader().load(proofText);
            ListIterator<IonValue> listIter = ((IonList)datagram.iterator().next()).listIterator();

            internalHashes.clear();
            while (listIter.hasNext()) {
                internalHashes.add(((IonBlob)listIter.next()).getBytes());
            }

            // Calculate digest
            calculatedDigest = internalHashes.stream().reduce(blockHash, BlockHashVerification::dot);

            verified = Arrays.equals(expectedDigest, calculatedDigest);

            if (verified) {
                System.out.printf("Block address '%s' successfully verified!%n", blockAddressText);
            } else {
                System.out.printf("Block address '%s' verification failed!%n", blockAddressText);
            }
        }
    }
}
```

------
#### [ .NET ]

```
using Amazon.IonDotnet;
using Amazon.IonDotnet.Builders;
using Amazon.IonDotnet.Tree;
using Amazon.QLDB;
using Amazon.QLDB.Driver;
using Amazon.QLDB.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;

namespace BlockHashVerification
{
    class BlockHashVerification
    {
        private static readonly string ledgerName = "vehicle-registration";
        private static readonly string tableName = "VehicleRegistration";
        private static readonly string vin = "KM8SRDHF6EU074761";
        private static readonly IQldbDriver driver = QldbDriver.Builder().WithLedger(ledgerName).Build();
        private static readonly IAmazonQLDB client = new AmazonQLDBClient();

        /// <summary>
        /// Takes two hashes, sorts them, concatenates them, and then returns the
        /// hash of the concatenated array.
        /// </summary>
        /// <param name="h1">Byte array containing one of the hashes to compare.</param>
        /// <param name="h2">Byte array containing one of the hashes to compare.</param>
        /// <returns>The concatenated array of hashes.</returns>
        private static byte[] Dot(byte[] h1, byte[] h2)
        {
            if (h1.Length == 0)
            {
                return h2;
            }

            if (h2.Length == 0)
            {
                return h1;
            }

            HashAlgorithm hashAlgorithm = HashAlgorithm.Create("SHA256");
            HashComparer comparer = new HashComparer();
            if (comparer.Compare(h1, h2) < 0)
            {
                return hashAlgorithm.ComputeHash(h1.Concat(h2).ToArray());
            }
            else
            {
                return hashAlgorithm.ComputeHash(h2.Concat(h1).ToArray());
            }
        }

        private class HashComparer : IComparer<byte[]>
        {
            private static readonly int HASH_LENGTH = 32;

            public int Compare(byte[] h1, byte[] h2)
            {
                if (h1.Length != HASH_LENGTH || h2.Length != HASH_LENGTH)
                {
                    throw new ArgumentException("Invalid hash");
                }

                for (var i = h1.Length - 1; i >= 0; i--)
                {
                    var byteEqual = (sbyte)h1[i] - (sbyte)h2[i];
                    if (byteEqual != 0)
                    {
                        return byteEqual;
                    }
                }

                return 0;
            }
        }

        static void Main()
        {
            // Get a digest
            GetDigestRequest getDigestRequest = new GetDigestRequest
            {
                Name = ledgerName
            };
            GetDigestResponse getDigestResponse = client.GetDigestAsync(getDigestRequest).Result;

            // expectedDigest is the buffer we will use later to compare against our calculated digest
            MemoryStream digest = getDigestResponse.Digest;
            byte[] expectedDigest = digest.ToArray();

            // Retrieve info for the given vin's document revisions
            var result = driver.Execute(txn => {
                string query = $"SELECT blockAddress, hash, metadata.id FROM _ql_committed_{tableName} WHERE data.VIN = '{vin}'";
                return txn.Execute(query);
            });

            Console.WriteLine($"Verifying document revisions for vin '{vin}' in table '{tableName}' in ledger '{ledgerName}'");

            foreach (IIonValue ionValue in result)
            {
                IIonStruct ionStruct = ionValue;

                // Get the requested fields
                IIonValue blockAddress = ionStruct.GetField("blockAddress");
                IIonBlob hash = ionStruct.GetField("hash");
                String metadataId = ionStruct.GetField("id").StringValue;

                Console.WriteLine($"Verifying document revision for id '{metadataId}'");

                // Use an Ion Reader to convert block address to text
                IIonReader reader = IonReaderBuilder.Build(blockAddress);
                StringWriter sw = new StringWriter();
                IIonWriter textWriter = IonTextWriterBuilder.Build(sw);
                textWriter.WriteValues(reader);
                string blockAddressText = sw.ToString();

                // Submit a request for the revision
                GetRevisionRequest revisionRequest = new GetRevisionRequest
                {
                    Name = ledgerName,
                    BlockAddress = new ValueHolder
                    {
                        IonText = blockAddressText
                    },
                    DocumentId = metadataId,
                    DigestTipAddress = getDigestResponse.DigestTipAddress
                };
                
                // Get a response back
                GetRevisionResponse revisionResponse = client.GetRevisionAsync(revisionRequest).Result;

                string proofText = revisionResponse.Proof.IonText;
                IIonDatagram proofValue = IonLoader.Default.Load(proofText);

                byte[] documentHash = hash.Bytes().ToArray();
                foreach (IIonValue proofHash in proofValue.GetElementAt(0))
                {
                    // Calculate the digest
                    documentHash = Dot(documentHash, proofHash.Bytes().ToArray());
                }

                bool verified = expectedDigest.SequenceEqual(documentHash);

                if (verified)
                {
                    Console.WriteLine($"Successfully verified document revision for id '{metadataId}'!");
                }
                else
                {
                    Console.WriteLine($"Document revision for id '{metadataId}' verification failed!");
                    return;
                }

                // Submit a request for the block
                GetBlockRequest getBlockRequest = new GetBlockRequest
                {
                    Name = ledgerName,
                    BlockAddress = new ValueHolder
                    {
                        IonText = blockAddressText
                    },
                    DigestTipAddress = getDigestResponse.DigestTipAddress
                };

                // Get a response back
                GetBlockResponse getBlockResponse = client.GetBlockAsync(getBlockRequest).Result;

                string blockText = getBlockResponse.Block.IonText;
                IIonDatagram blockValue = IonLoader.Default.Load(blockText);

                // blockValue is a IonDatagram, and the first value is an IonStruct containing the blockHash
                byte[] blockHash = blockValue.GetElementAt(0).GetField("blockHash").Bytes().ToArray();

                proofText = getBlockResponse.Proof.IonText;
                proofValue = IonLoader.Default.Load(proofText);

                foreach (IIonValue proofHash in proofValue.GetElementAt(0))
                {
                    // Calculate the digest
                    blockHash = Dot(blockHash, proofHash.Bytes().ToArray());
                }

                verified = expectedDigest.SequenceEqual(blockHash);

                if (verified)
                {
                    Console.WriteLine($"Block address '{blockAddressText}' successfully verified!");
                }
                else
                {
                    Console.WriteLine($"Block address '{blockAddressText}' verification failed!");
                }
            }
        }
    }
}
```

------
#### [ Go ]

```
package main

import (
    "context"
    "crypto/sha256"
    "errors"
    "fmt"
    "reflect"

    "github.com/amzn/ion-go/ion"
    "github.com/aws/aws-sdk-go/aws"
    AWSSession "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/qldb"
    "github.com/aws/aws-sdk-go/service/qldbsession"
    "github.com/awslabs/amazon-qldb-driver-go/qldbdriver"
)

const (
    hashLength = 32
    ledgerName = "vehicle-registration"
    tableName  = "VehicleRegistration"
    vin        = "KM8SRDHF6EU074761"
)

// Takes two hashes, sorts them, concatenates them, and then returns the hash of the concatenated array.
func dot(h1, h2 []byte) ([]byte, error) {
    compare, err := hashComparator(h1, h2)
    if err != nil {
        return nil, err
    }

    var concatenated []byte
    if compare < 0 {
        concatenated = append(h1, h2...)
    } else {
        concatenated = append(h2, h1...)
    }

    newHash := sha256.Sum256(concatenated)
    return newHash[:], nil
}

func hashComparator(h1 []byte, h2 []byte) (int16, error) {
    if len(h1) != hashLength || len(h2) != hashLength {
        return 0, errors.New("invalid hash")
    }
    for i := range h1 {
        // Reverse index for little endianness
        index := hashLength - 1 - i

        // Handle byte being unsigned and overflow
        h1Int := int16(h1[index])
        h2Int := int16(h2[index])
        if h1Int > 127 {
            h1Int = 0 - (256 - h1Int)
        }
        if h2Int > 127 {
            h2Int = 0 - (256 - h2Int)
        }

        difference := h1Int - h2Int
        if difference != 0 {
            return difference, nil
        }
    }
    return 0, nil
}

func main() {
    driverSession := AWSSession.Must(AWSSession.NewSession(aws.NewConfig()))
    qldbSession := qldbsession.New(driverSession)
    driver, err := qldbdriver.New(ledgerName, qldbSession, func(options *qldbdriver.DriverOptions) {})
    if err != nil {
        panic(err)
    }
    client := qldb.New(driverSession)

    // Get a digest
    currentLedgerName := ledgerName
    input := qldb.GetDigestInput{Name: &currentLedgerName}
    digestOutput, err := client.GetDigest(&input)
    if err != nil {
        panic(err)
    }

    // expectedDigest is the buffer we will later use to compare against our calculated digest
    expectedDigest := digestOutput.Digest

    // Retrieve info for the given vin's document revisions
    result, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {
        statement := fmt.Sprintf(
                "SELECT blockAddress, hash, metadata.id FROM _ql_committed_%s WHERE data.VIN = '%s'",
                tableName,
                vin)
        result, err := txn.Execute(statement)
        if err != nil {
            return nil, err
        }

        results := make([]map[string]interface{}, 0)

        // Convert the result set into a map
        for result.Next(txn) {
            var doc map[string]interface{}
            err := ion.Unmarshal(result.GetCurrentData(), &doc)
            if err != nil {
                return nil, err
            }
            results = append(results, doc)
        }
        return results, nil
    })
    if err != nil {
        panic(err)
    }
    resultSlice := result.([]map[string]interface{})

    fmt.Printf("Verifying document revisions for vin '%s' in table '%s' in ledger '%s'\n", vin, tableName, ledgerName)

    for _, value := range resultSlice {
        // Get the requested fields
        ionBlockAddress, err := ion.MarshalText(value["blockAddress"])
        if err != nil {
            panic(err)
        }
        blockAddress := string(ionBlockAddress)
        metadataId := value["id"].(string)
        documentHash := value["hash"].([]byte)

        fmt.Printf("Verifying document revision for id '%s'\n", metadataId)

        // Submit a request for the revision
        revisionInput := qldb.GetRevisionInput{
            BlockAddress:     &qldb.ValueHolder{IonText: &blockAddress},
            DigestTipAddress: digestOutput.DigestTipAddress,
            DocumentId:       &metadataId,
            Name:             &currentLedgerName,
        }

        // Get a result back
        revisionOutput, err := client.GetRevision(&revisionInput)
        if err != nil {
            panic(err)
        }

        proofText := revisionOutput.Proof.IonText

        // Use ion.Reader to iterate over the proof's node hashes
        reader := ion.NewReaderString(*proofText)
        // Enter the struct containing node hashes
        reader.Next()
        if err := reader.StepIn(); err != nil {
            panic(err)
        }

        // Going through nodes and calculate digest
        for reader.Next() {
            val, _ := reader.ByteValue()
            documentHash, err = dot(documentHash, val)
        }

        // Compare documentHash with the expected digest
        verified := reflect.DeepEqual(documentHash, expectedDigest)

        if verified {
            fmt.Printf("Successfully verified document revision for id '%s'!\n", metadataId)
        } else {
            fmt.Printf("Document revision for id '%s' verification failed!\n", metadataId)
            return
        }

        // Submit a request for the block
        blockInput := qldb.GetBlockInput{
            Name:             &currentLedgerName,
            BlockAddress:     &qldb.ValueHolder{IonText: &blockAddress},
            DigestTipAddress: digestOutput.DigestTipAddress,
        }

        // Get a result back
        blockOutput, err := client.GetBlock(&blockInput)
        if err != nil {
            panic(err)
        }

        proofText = blockOutput.Proof.IonText

        block := new(map[string]interface{})
        err = ion.UnmarshalString(*blockOutput.Block.IonText, block)
        if err != nil {
            panic(err)
        }

        blockHash := (*block)["blockHash"].([]byte)

        // Use ion.Reader to iterate over the proof's node hashes
        reader = ion.NewReaderString(*proofText)
        // Enter the struct containing node hashes
        reader.Next()
        if err := reader.StepIn(); err != nil {
            panic(err)
        }

        // Going through nodes and calculate digest
        for reader.Next() {
            val, err := reader.ByteValue()
            if err != nil {
                panic(err)
            }
            blockHash, err = dot(blockHash, val)
        }

        // Compare blockHash with the expected digest
        verified = reflect.DeepEqual(blockHash, expectedDigest)

        if verified {
            fmt.Printf("Block address '%s' successfully verified!\n", blockAddress)
        } else {
            fmt.Printf("Block address '%s' verification failed!\n", blockAddress)
            return
        }
    }
}
```

------
#### [ Node.js ]

```
import { QldbDriver, Result, TransactionExecutor} from "amazon-qldb-driver-nodejs";
import { QLDB } from "aws-sdk"
import { GetBlockRequest, GetBlockResponse, GetDigestRequest, GetDigestResponse, GetRevisionRequest, GetRevisionResponse } from "aws-sdk/clients/qldb";
import { createHash } from "crypto";
import { dom, dumpText, load } from "ion-js"

const ledgerName: string = "vehicle-registration";
const tableName: string = "VehicleRegistration";
const vin: string = "KM8SRDHF6EU074761";
const driver: QldbDriver = new QldbDriver(ledgerName);
const qldbClient: QLDB = new QLDB();
const HASH_SIZE = 32;

/**
 * Takes two hashes, sorts them, concatenates them, and calculates a digest based on the concatenated hash.
 * @param h1 Byte array containing one of the hashes to compare.
 * @param h2 Byte array containing one of the hashes to compare.
 * @returns The digest calculated from the concatenated hash values.
 */
function dot(h1: Uint8Array, h2: Uint8Array): Uint8Array {
    if (h1.length === 0) {
        return h2;
    }
    if (h2.length === 0) {
        return h1;
    }

    const newHashLib = createHash("sha256");

    let concatenated: Uint8Array;
    if (hashComparator(h1, h2) < 0) {
        concatenated = concatenate(h1, h2);
    } else {
        concatenated = concatenate(h2, h1);
    }
    newHashLib.update(concatenated);
    return newHashLib.digest();
}

/**
 * Compares two hashes by their **signed** byte values in little-endian order.
 * @param hash1 The hash value to compare.
 * @param hash2 The hash value to compare.
 * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching
 *          bytes.
 * @throws RangeError When the hash is not the correct hash size.
 */
function hashComparator(hash1: Uint8Array, hash2: Uint8Array): number {
    if (hash1.length !== HASH_SIZE || hash2.length !== HASH_SIZE) {
        throw new RangeError("Invalid hash.");
    }
    for (let i = hash1.length-1; i >= 0; i--) {
        const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24);
        if (difference !== 0) {
            return difference;
        }
    }
    return 0;
}

/**
 * Helper method that concatenates two Uint8Array.
 * @param arrays List of arrays to concatenate, in the order provided.
 * @returns The concatenated array.
 */
function concatenate(...arrays: Uint8Array[]): Uint8Array {
    let totalLength = 0;
    for (const arr of arrays) {
        totalLength += arr.length;
    }
    const result = new Uint8Array(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}

/**
 * Helper method that checks for equality between two Uint8Array.
 * @param expected Byte array containing one of the hashes to compare.
 * @param actual Byte array containing one of the hashes to compare.
 * @returns Boolean indicating equality between the two Uint8Array.
 */
function isEqual(expected: Uint8Array, actual: Uint8Array): boolean {
    if (expected === actual) return true;
    if (expected == null || actual == null) return false;
    if (expected.length !== actual.length) return false;

    for (let i = 0; i < expected.length; i++) {
        if (expected[i] !== actual[i]) {
            return false;
        }
    }
    return true;
}

const main = async function (): Promise<void> {
    // Get a digest
    const getDigestRequest: GetDigestRequest = {
        Name: ledgerName
    };
    const getDigestResponse: GetDigestResponse = await qldbClient.getDigest(getDigestRequest).promise();

    // expectedDigest is the buffer we will later use to compare against our calculated digest
    const expectedDigest: Uint8Array = <Uint8Array>getDigestResponse.Digest;

    const result: dom.Value[] = await driver.executeLambda(async (txn: TransactionExecutor): Promise<dom.Value[]> => {
        const query: string = `SELECT blockAddress, hash, metadata.id FROM _ql_committed_${tableName} WHERE data.VIN = '${vin}'`;
        const queryResult: Result = await txn.execute(query);
        return queryResult.getResultList();
    });

    console.log(`Verifying document revisions for vin '${vin}' in table '${tableName}' in ledger '${ledgerName}'`);

    for (let value of result) {
        // Get the requested fields
        const blockAddress: dom.Value = value.get("blockAddress");
        const hash: dom.Value = value.get("hash");
        const metadataId: string = value.get("id").stringValue();

        console.log(`Verifying document revision for id '${metadataId}'`);

        // Submit a request for the revision
        const revisionRequest: GetRevisionRequest = {
            Name: ledgerName,
            BlockAddress: {
                IonText: dumpText(blockAddress)
            },
            DocumentId: metadataId,
            DigestTipAddress: getDigestResponse.DigestTipAddress
        };

        // Get a response back
        const revisionResponse: GetRevisionResponse = await qldbClient.getRevision(revisionRequest).promise();

        let proofValue: dom.Value = load(revisionResponse.Proof.IonText);

        let documentHash: Uint8Array = hash.uInt8ArrayValue();
        proofValue.elements().forEach((proofHash: dom.Value) => {
            // Calculate the digest
            documentHash = dot(documentHash, proofHash.uInt8ArrayValue());
        });

        let verified: boolean = isEqual(expectedDigest, documentHash);

        if (verified) {
            console.log(`Successfully verified document revision for id '${metadataId}'!`);
        } else {
            console.log(`Document revision for id '${metadataId}' verification failed!`);
            return;
        }

        // Submit a request for the block
        const getBlockRequest: GetBlockRequest = {
            Name: ledgerName,
            BlockAddress: {
                IonText: dumpText(blockAddress)
            },
            DigestTipAddress: getDigestResponse.DigestTipAddress
        };

        // Get a response back
        const getBlockResponse: GetBlockResponse = await qldbClient.getBlock(getBlockRequest).promise();

        const blockValue: dom.Value = load(getBlockResponse.Block.IonText)
        let blockHash: Uint8Array = blockValue.get("blockHash").uInt8ArrayValue();

        proofValue = load(getBlockResponse.Proof.IonText);

        proofValue.elements().forEach((proofHash: dom.Value) => {
            // Calculate the digest
            blockHash = dot(blockHash, proofHash.uInt8ArrayValue());
        });

        verified = isEqual(expectedDigest, blockHash);

        if (verified) {
            console.log(`Block address '${dumpText(blockAddress)}' successfully verified!`);
        } else {
            console.log(`Block address '${dumpText(blockAddress)}' verification failed!`);
        }
    }
};

if (require.main === module) {
    main();
}
```

------
#### [ Python ]

```
from amazon.ion.simpleion import dumps, loads
from array import array
from boto3 import client
from functools import reduce
from hashlib import sha256
from pyqldb.driver.qldb_driver import QldbDriver

ledger_name = 'vehicle-registration'
table_name = 'VehicleRegistration'
vin = 'KM8SRDHF6EU074761'
qldb_client = client('qldb')
hash_length = 32


def query_doc_revision(txn):
    query = "SELECT blockAddress, hash, metadata.id FROM _ql_committed_{} WHERE data.VIN = '{}'".format(table_name, vin)
    return txn.execute_statement(query)


def block_address_to_dictionary(ion_dict):
    """
    Convert a block address from IonPyDict into a dictionary.
    Shape of the dictionary must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"}

    :type ion_dict: :py:class:`amazon.ion.simple_types.IonPyDict`/str
    :param ion_dict: The block address value to convert.

    :rtype: dict
    :return: The converted dict.
    """
    block_address = {'IonText': {}}
    if not isinstance(ion_dict, str):
        py_dict = '{{strandId: "{}", sequenceNo:{}}}'.format(ion_dict['strandId'], ion_dict['sequenceNo'])
        ion_dict = py_dict
    block_address['IonText'] = ion_dict
    return block_address


def dot(hash1, hash2):
    """
    Takes two hashes, sorts them, concatenates them, and then returns the
    hash of the concatenated array.

    :type hash1: bytes
    :param hash1: The hash value to compare.

    :type hash2: bytes
    :param hash2: The hash value to compare.

    :rtype: bytes
    :return: The new hash value generated from concatenated hash values.
    """
    if len(hash1) != hash_length or len(hash2) != hash_length:
        raise ValueError('Illegal hash.')

    hash_array1 = array('b', hash1)
    hash_array2 = array('b', hash2)

    difference = 0
    for i in range(len(hash_array1) - 1, -1, -1):
        difference = hash_array1[i] - hash_array2[i]
        if difference != 0:
            break

    if difference < 0:
        concatenated = hash1 + hash2
    else:
        concatenated = hash2 + hash1

    new_hash_lib = sha256()
    new_hash_lib.update(concatenated)
    new_digest = new_hash_lib.digest()
    return new_digest


# Get a digest
get_digest_response = qldb_client.get_digest(Name=ledger_name)

# expected_digest is the buffer we will later use to compare against our calculated digest
expected_digest = get_digest_response.get('Digest')
digest_tip_address = get_digest_response.get('DigestTipAddress')

qldb_driver = QldbDriver(ledger_name=ledger_name)

# Retrieve info for the given vin's document revisions
result = qldb_driver.execute_lambda(query_doc_revision)

print("Verifying document revisions for vin '{}' in table '{}' in ledger '{}'".format(vin, table_name, ledger_name))

for value in result:
    # Get the requested fields
    block_address = value['blockAddress']
    document_hash = value['hash']
    metadata_id = value['id']

    print("Verifying document revision for id '{}'".format(metadata_id))

    # Submit a request for the revision and get a result back
    proof_response = qldb_client.get_revision(Name=ledger_name,
                                              BlockAddress=block_address_to_dictionary(block_address),
                                              DocumentId=metadata_id,
                                              DigestTipAddress=digest_tip_address)

    proof_text = proof_response.get('Proof').get('IonText')
    proof_hashes = loads(proof_text)

    # Calculate digest
    calculated_digest = reduce(dot, proof_hashes, document_hash)

    verified = calculated_digest == expected_digest
    if verified:
        print("Successfully verified document revision for id '{}'!".format(metadata_id))
    else:
        print("Document revision for id '{}' verification failed!".format(metadata_id))

    # Submit a request for the block and get a result back
    block_response = qldb_client.get_block(Name=ledger_name, BlockAddress=block_address_to_dictionary(block_address),
                                           DigestTipAddress=digest_tip_address)

    block_text = block_response.get('Block').get('IonText')
    block = loads(block_text)

    block_hash = block.get('blockHash')

    proof_text = block_response.get('Proof').get('IonText')
    proof_hashes = loads(proof_text)

    # Calculate digest
    calculated_digest = reduce(dot, proof_hashes, block_hash)

    verified = calculated_digest == expected_digest
    if verified:
        print("Block address '{}' successfully verified!".format(dumps(block_address,
                                                                       binary=False,
                                                                       omit_version_marker=True)))
    else:
        print("Block address '{}' verification failed!".format(block_address))
```

------

# 常见的验证错误
<a name="verification.errors"></a>

**重要**  
终止支持通知：现有客户将能够使用 Amazon QLDB，直到 2025 年 7 月 31 日终止支持。有关更多详细信息，请参阅[将亚马逊 QLDB 账本迁移到亚马逊 Aurora PostgreSQL](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)。

本节介绍了 Amazon QLDB 为验证请求引发的运行时错误。

以下是该服务返回的常见异常列表。每个异常包括具体的错误消息，接着是可能引发异常的 API 操作、一个简短的描述以及可能的解决方案建议。<a name="verification.errors.varlist"></a>

**IllegalArgumentException**  
消息：提供的 Ion 值无效，无法解析。  
API 操作：`GetDigest, GetBlock, GetRevision`  
在重试请求之前，请务必提供有效的 [Amazon Ion](ion.md) 值。

**IllegalArgumentException**  
消息：提供的块地址无效。  
API 操作：`GetDigest, GetBlock, GetRevision`  
在重试请求之前，请务必提供有效的块地址。地址是一种包含两个字段的 Amazon Ion 结构：即 `strandId` 和 `sequenceNo`。  
例如：`{strandId:"BlFTjlSXze9BIh1KOszcE3",sequenceNo:14}`

**IllegalArgumentException**  
消息：提供的摘要提示地址的序列号超出了该链路的最新提交记录。  
API 操作：`GetDigest, GetBlock, GetRevision`  
您提供的摘要提示地址的序号必须小于或等于日记链最新提交的记录的序号。在重试请求之前，请务必提供带有有效序号的摘要提示地址。

**IllegalArgumentException**  
消息：提供的块地址的链路 ID 无效。  
API 操作：`GetDigest, GetBlock, GetRevision`  
您提供的块地址必须具有与该日记账的链 ID 相匹配的链 ID。在重试请求之前，请务必提供一个具有有效链路 ID 的块地址。

**IllegalArgumentException**  
消息：提供的摘要区块的序列号超出了该组的最新提交记录。  
API 操作：`GetBlock, GetRevision`  
您提供的区块地址的序号必须小于或等于链最新提交的记录的序号。在重试请求之前，请务必提供带有有效序号的区块地址。

**IllegalArgumentException**  
消息：提供的块地址的链 ID 必须与提供的摘要提示地址的链 ID 相匹配。  
API 操作：`GetBlock, GetRevision`  
只有当文档修订或块存在于与提供的摘要相同的日记账链中时，才能验证文档修订或块。

**IllegalArgumentException**  
消息：提供的块地址的序列号不得大于所提供的摘要提示地址的序号。  
API 操作：`GetBlock, GetRevision`  
只有当您提供的摘要包含文档修订或块时，您才能验证它。这意味着它是在摘要提示地址之前提交给日记账的。

**IllegalArgumentException**  
消息：在指定区块地址的区块中找不到提供的文档 ID。  
API 操作：`GetRevision`  
您提供的文档 ID 必须存在于您提供的块地址中。在重试请求之前，请确保这两个参数是一致的。