

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# Amazon QLDB에서의 데이터 확인
<a name="verification"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB를 사용하면 정확한 애플리케이션 데이터 변경 기록을 확인하고 신뢰할 수 있습니다. QLDB는 *저널*이라고 하는 변경 불가능한 트랜잭션 로그를 데이터 스토리지에 사용합니다. 저널은 커밋된 데이터의 모든 변경 내용을 추적하고 시간이 지나면서 완전하고 확인 가능한 시간대별 변경 기록을 유지합니다.

QLDB는 Merkle 트리 기반 모델과 함께 SHA-256 해시 함수를 사용하여 *다이제스트*라고하는 저널의 암호화 표현을 생성합니다. 다이제스트는 특정 시점을 기준으로 데이터의 전체 변경 기록에 대한 고유한 서명 역할을 합니다. 다이제스트를 사용하여 해당 서명과 관련된 문서 수정본의 무결성을 확인할 수 있습니다.

**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 SDK를 사용한 데이터 검증](verification.tutorial-block-hash.md)
+ [확인을 위한 일반적인 오류](verification.errors.md)

## QLDB에서 확인할 수 있는 데이터 종류는 무엇입니까?
<a name="verification.structure"></a>

QLDB에서 각 원장에는 정확히 하나의 저널이 있습니다. 저널에는 저널의 파티션인 *스트랜드*가 여러 개 있을 수 있습니다.

**참고**  
QLDB는 현재 단일 스트랜드가 포함된 저널만 지원합니다.

*블록*은 트랜잭션 중에 저널 스트랜드에 커밋되는 객체입니다. 이 블록에는 트랜잭션으로 인한 문서 수정본을 나타내는 *항목* 객체가 들어 있습니다. QLDB에서 개별 수정 또는 전체 저널 블록을 확인할 수 있습니다.

다음 다이어그램은 이 저널 구조를 보여줍니다.

![\[스트랜드를 구성하는 해시 체인 블록 세트와 각 블록의 시퀀스 번호 및 해시를 보여주는 Amazon QLDB 저널 구조 다이어그램.\]](http://docs.aws.amazon.com/ko_kr/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/ko_kr/qldb/latest/developerguide/images/sha256.png)


SHA-256 해시 함수는 단방향으로, 출력이 주어졌을 때 입력을 계산하는 것이 수학적으로 불가능합니다. 다음 다이어그램은 출력 해시 값이 주어지면 입력 QLDB 문서를 계산하는 것이 불가능하다는 것을 보여줍니다.

![\[출력 해시 값이 주어졌을 때 입력 QLDB 문서를 계산하는 것이 불가능하다는 것을 보여주는 다이어그램.\]](http://docs.aws.amazon.com/ko_kr/qldb/latest/developerguide/images/sha256-one-way.png)


다음 데이터 입력은 확인을 위해 QLDB에서 해싱됩니다.
+ 문서 수정
+ PATIL 문
+ 수정 항목
+ 저널 블록

### 다이제스트
<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 트리 Wikipedia 페이지](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/ko_kr/qldb/latest/developerguide/images/verification/hash-tree.png)


노드 **A**가 해시를 확인하고자 하는 문서 수정본 버전이 들어 있는 블록이라고 가정해 보겠습니다. **B**, **E**, **G**는 노드는 QLDB가 증명에 제공하는 순서가 지정된 해시 목록을 나타냅니다. 이러한 해시는 해시 **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/)을 참조하세요.

원장에서 다이제스트를 요청하고 데이터를 확인하는 방법에 대한 단계별 지침은 다음을 참조하세요.
+ [1단계: QLDB에서 다이제스트 요청](verification.digest.md)
+ [2단계: QLDB에서 데이터 확인](verification.verify.md)

# 1단계: QLDB에서 다이제스트 요청
<a name="verification.digest"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB는 원장에 있는 저널의 현재 *팁*을 다루는 다이제스트를 요청하는 API를 제공합니다. 저널 팁은 QLDB가 요청을 수신한 시점을 기준으로 가장 최근에 커밋된 블록을 나타냅니다. AWS Management Console, AWS SDK 또는 AWS Command Line Interface (AWS CLI)를 사용하여 다이제스트를 가져올 수 있습니다.

**Topics**
+ [AWS Management Console](#verification.digest.con)
+ [QLDB API](#verification.digest.api)

## AWS Management Console
<a name="verification.digest.con"></a>

다음 단계에 따라 QLDB 콘솔을 사용하여 리소스를 복원합니다.

**다이제스트를 요청하려면(콘솔)**

1. 에 로그인 AWS Management Console하고 [https://console.aws.amazon.com/qldb](https://console.aws.amazon.com/qldb) Amazon QLDB 콘솔을 엽니다.

1. 탐색 창에서 **원장**을 선택합니다.

1. 원장 목록에서 다이제스트를 요청하려는 원장명을 선택합니다.

1. **다이제스트 가져오기**를 선택합니다. **다이제스트 가져오기** 대화 상자에는 다음과 같은 다이제스트 세부 정보가 표시됩니다.
   + **다이제스트** - 요청한 다이제스트의 SHA-256 해시 값.
   + **다이제스트 팁 주소** - 요청한 다이제스트에 포함된 저널의 최신 블록 위치. 주소에는 다음과 같은 두 가지 필드가 있습니다.
     + `strandId` - 블록을 포함하는 저널 스트랜드의 고유 ID입니다.
     + `sequenceNo` - 스트랜드 내 블록의 위치를 지정하는 인덱스 번호.
   + **원장** - 다이제스트를 요청한 원장 이름.
   + **날짜** - 다이제스트를 요청한 시점의 타임스탬프.

1. 다이제스트 정보를 검토합니다. 그런 다음 **저장**을 선택합니다. 기본 파일 이름을 유지하거나 새 이름을 입력할 수 있습니다.
**참고**  
원장의 데이터를 수정하지 않아도 다이제스트 해시 및 팁 주소 값이 변경되는 것을 확인할 수 있습니다. 이는 *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와 함께 사용하여 원장의 다이제스트를 요청할 수도 있습니다. 애플리케이션 프로그램에서 사용할 수 있는 다음과 같은 기능이 API에서 제공됩니다.
+ [GetDigest](https://docs.aws.amazon.com/qldb/latest/developerguide/API_GetDigest.html) - 저널에서 가장 최근에 커밋된 블록의 원장 다이제스트를 반환합니다. 응답에는 256비트 해시 값과 블록 주소가 포함됩니다.

를 사용하여 다이제스트를 요청하는 방법에 대한 자세한 AWS CLI내용은 명령 참조의 [get-digest](https://docs.aws.amazon.com/cli/latest/reference/qldb/get-digest.html) *AWS CLI 명령을 참조*하세요.

### 샘플 애플리케이션
<a name="verification.digest.api.sample"></a>

Java 코드 예제는 GitHub 리포지토리 [aws-samples/amazon-qldb-dmv-sample-java](https://github.com/aws-samples/amazon-qldb-dmv-sample-java)를 참조하세요. 이 샘플 애플리케이션을 다운로드하여 설치하는 방법에 대한 자세한 내용은 [Amazon QLDB Java 샘플 애플리케이션 설치](sample-app.java.md) 섹션을 참조하세요. 다이제스트를 요청하기 전에 [Java 자습서](getting-started.java.tutorial.md)의 1\$13단계에 따라 샘플 원장을 생성하고 샘플 데이터와 함께 로드해야 합니다.

[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>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

Amazon QLDB는 지정된 문서 ID 및 연결된 블록에 대한 증명을 요청하는 API를 제공합니다. [1단계: QLDB에서 다이제스트 요청](verification.digest.md)에 설명된 대로 이전에 저장한 다이제스트의 팁 주소도 제공해야 합니다. AWS Management Console, AWS SDK 또는를 사용하여 증명을 AWS CLI 얻을 수 있습니다.

그런 다음 QLDB에서 반환한 증명을 사용하여 클라이언트측 API를 사용해 저장된 다이제스트와 비교하여 문서 수정본을 확인할 수 있습니다. 이를 통해 데이터 확인에 사용하는 알고리즘을 제어할 수 있습니다.

**Topics**
+ [AWS Management Console](#verification.verify.con)
+ [QLDB API](#verification.verify.api)

## AWS Management Console
<a name="verification.verify.con"></a>

이 단원에서는 Amazon QLDB 콘솔을 사용하여 이전에 저장한 다이제스트와 비교하여 문서 수정본을 확인하는 단계를 설명합니다.

시작하기 전에 먼저 [1단계: QLDB에서 다이제스트 요청](verification.digest.md)의 단계를 따라야 합니다. 확인을 위해서는 확인하려는 수정본을 포함하는 이전에 저장한 다이제스트가 필요합니다.

**문서 수정본을 확인하려면(콘솔)**

1. [https://console.aws.amazon.com/qldb](https://console.aws.amazon.com/qldb)에서 Amazon QLDB 콘솔을 엽니다.

1. 먼저 원장에서 확인하려는 수정본의 `id` 및 `blockAddress`를 쿼리합니다. 이 필드는 문서의 메타데이터에 포함되며, *커밋된 뷰*에서 쿼리할 수 있습니다.

   문서 `id`는 시스템에서 할당한 고유 ID 문자열입니다. `blockAddress`는 개정이 커밋된 블록 위치를 지정하는 Ion 구조입니다.

   탐색 창에서 **PartiQL 편집기**를 선택합니다.

1. 수정본을 확인하려는 원장 이름을 선택합니다.

1. 쿼리 편집기에 다음 구문에서 `SELECT` 문을 입력한 다음 **실행**을 선택합니다.

   ```
   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. 문서 및 다이제스트 입력 파라미터를 검토한 다음 **검증**을 선택합니다.

   콘솔은 다음 두 단계를 자동화합니다.

   1. 지정된 문서에 대한 증거를 QLDB에 요청합니다.

   1. QLDB에서 반환한 증거를 사용하여 제공된 다이제스트에 대해 문서 개정을 검증하는 클라이언트 측 API를 호출합니다. 이 확인 알고리즘을 검사하려면 다음 [QLDB API](#verification.verify.api) 섹션을 참조하여 코드 예제를 다운로드하세요.

   콘솔은 **검증 결과** 카드에 요청 결과를 표시합니다. 자세한 내용은 [확인 결과](verification.results.md)을 참조하십시오.

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

Amazon QLDB API를 AWS SDK 또는 AWS CLI와 함께 사용하여 문서 수정본을 확인할 수도 있습니다. 애플리케이션 프로그램에서 사용할 수 있는 다음과 같은 기능이 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 리포지토리 [aws-samples/amazon-qldb-dmv-sample-java](https://github.com/aws-samples/amazon-qldb-dmv-sample-java)를 참조하세요. 이 샘플 애플리케이션을 다운로드하여 설치하는 방법에 대한 자세한 내용은 [Amazon QLDB Java 샘플 애플리케이션 설치](sample-app.java.md) 섹션을 참조하세요. 확인을 수행하기 전에 [Java 자습서](getting-started.java.tutorial.md)의 1\$13단계에 따라 샘플 원장을 생성하고 샘플 데이터와 함께 로드해야 합니다.

[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>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 섹션에서는 AWS Management Console에서 Amazon QLDB 데이터 확인 요청을 통해 반환된 결과를 설명합니다. 확인 요청을 제출하는 방법에 대한 자세한 단계는 [2단계: QLDB에서 데이터 확인](verification.verify.md) 섹션을 참조하세요.

QLDB 콘솔의 **확인** 페이지에서 요청 결과가 **확인 결과** 카드에 표시됩니다. **증명** 탭에는 지정된 문서 수정본 및 다이제스트에 대해 QLDB에서 반환한 증명의 콘텐츠가 표시됩니다. 여기에는 다음 이벤트가 포함됩니다.
+ **수정 해시** - 확인 중인 문서 수정본을 고유하게 나타내는 SHA-256 값.
+ **증명 해시** - 지정된 다이제스트를 다시 계산하는 데 사용되는 QLDB에서 제공하는 해시의 정렬된 목록. 콘솔은 **수정 해시**로 시작하여 다시 계산된 다이제스트로 끝날 때까지 각 증명 해시와 순차적으로 결합합니다.

  목록은 기본적으로 축소되어 있으므로 목록을 확장하여 해시 값을 표시할 수 있습니다. 필요에 따라 [증명을 사용하여 다이제스트를 다시 계산하기](#verification.results.recalc)에 설명된 단계에 따라 직접 해시 계산을 시도할 수도 있습니다.
+ **다이제스트 계산** - **수정 해시**에서 수행된 일련의 **해시 계산**의 결과로 생성된 해시. 이 값이 이전에 저장한 **다이제스트**와 일치하면 확인이 성공합니다.

**블록** 탭에는 확인 중인 수정본이 포함된 블록의 콘텐츠가 표시됩니다. 여기에는 다음 이벤트가 포함됩니다.
+ **거래 ID** - 이 블록을 커밋한 거래의 고유 ID.
+ **트랜잭션 시간** - 이 블록이 스트랜드에 커밋된 시점의 타임스탬프.
+ **블록 해시** - 이 블록과 모든 콘텐츠를 고유하게 나타내는 SHA-256 값.
+ **블록 주소** - 이 블록이 커밋된 원장 일지 내 위치. 주소에는 다음과 같은 두 가지 필드가 있습니다.
  + **스트랜드 ID** - 이 블록을 포함하는 저널 스트랜드의 고유 ID.
  + **시퀀스 번호** - 스트랜드 내에서 이 블록의 위치를 지정하는 인덱스 번호.
+ **문** - 이 블록의 항목을 커밋하기 위해 수행된 PartiQL 문.
**참고**  
파라미터화된 문을 프로그래밍 방식으로 실행하면 리터럴 데이터 대신 바인드 파라미터를 사용하여 저널 블록에 기록됩니다. 예를 들어, 저널 블록에서 다음 명령문을 볼 수 있는데, 여기서 물음표(`?`)는 문서 콘텐츠의 변수 자리 표시자입니다.  

  ```
  INSERT INTO Vehicle ?
  ```
+ **문서 항목** - 이 블록에서 커밋된 문서 수정본.

요청에서 문서 수정본 확인에 실패한 경우 가능한 원인에 대한 자세한 내용은 [확인을 위한 일반적인 오류](verification.errors.md) 섹션을 참조하세요.

## 증명을 사용하여 다이제스트를 다시 계산하기
<a name="verification.results.recalc"></a>

QLDB가 문서 확인 요청에 대한 증명을 반환한 후 사용자가 직접 해시 계산을 시도할 수 있습니다. 이 섹션에서는 제공된 증명을 사용하여 다이제스트를 다시 계산하는 상위 단계를 설명합니다.

먼저 **수정 해시**를 **증명 해시** 목록의 첫 번째 해시와 페어링합니다. 그리고 다음을 수행합니다.

1. 두 해시를 정렬합니다. 리틀 엔디안 순서의 *서명된* 바이트 값을 기준으로 해시를 비교합니다.

1. 두 해시를 정렬된 순서대로 연결합니다.

1. SHA-256 해시 생성기를 사용하여 연결된 쌍을 해싱합니다.

1. 새 해시를 증명의 다음 해시와 페어링하고 1\$13단계를 반복합니다. 마지막 증명 해시를 처리한 후에는 새 해시가 재계산된 다이제스트가 됩니다.

재계산된 다이제스트가 이전에 저장한 다이제스트와 일치하면 문서가 성공적으로 확인됩니다.

이러한 확인 단계를 보여주는 코드 예제가 포함된 단계별 자습서를 보려면 [자습서: AWS SDK를 사용한 데이터 검증](verification.tutorial-block-hash.md) 섹션을 참조하세요.

# 자습서: AWS SDK를 사용한 데이터 검증
<a name="verification.tutorial-block-hash"></a>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon Aurora PostgreSQL로 마이그레이션](https://aws.amazon.com/blogs/database/migrate-an-amazon-qldb-ledger-to-amazon-aurora-postgresql/)을 참조하세요.

이 자습서에서는 AWS SDK를 통해 QLDB API를 사용하여 Amazon QLDB 원장의 문서 개정 해시와 저널 블록 해시를 확인합니다. 또한 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\$12단계에 따라 `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 `KM8SRDHF6EU074761`과 연결된 블록 주소, 해시 및 문서 ID를 쿼리합니다.

------
#### [ 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를 원장 이름과 함께 사용하여 `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 Cookbook](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`를 사용하여 블록과 증명을 각각의 Ion 데이터그램에 로드합니다.

```
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>

**중요**  
지원 종료 공지: 기존 고객은 07/31/2025에 지원이 종료될 때까지 Amazon QLDB를 사용할 수 있습니다. 자세한 내용은 [Amazon QLDB 원장을 Amazon 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`  
요청을 다시 시도하기 전에 유효한 블록 주소를 입력했는지 확인하세요. 블록 주소는 `strandId` 및 `sequenceNo`라는 두 개의 필드를 갖는 Amazon Ion 구조입니다.  
예: `{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는 입력한 블록 주소에 있어야 합니다. 요청을 재시도하기 전에 이 두 파라미터가 일치하는지 확인하세요.