자습서: 를 사용하여 데이터 확인 AWS SDK - Amazon Quantum 원장 데이터베이스(Amazon QLDB)

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

자습서: 를 사용하여 데이터 확인 AWS SDK

중요

지원 종료 알림: 기존 고객은 07/31/2025에 지원이 종료될 QLDB 때까지 Amazon을 사용할 수 있습니다. 자세한 내용은 Amazon QLDB Ledger를 Amazon Aurora Postgre로 마이그레이션을SQL 참조하세요.

이 자습서에서는 를 QLDB API 통해 를 사용하여 Amazon QLDB 원장의 문서 개정 해시와 저널 블록 해시를 확인합니다 AWS SDK. QLDB 드라이버를 사용하여 문서 개정을 쿼리할 수도 있습니다.

차량 식별 번호(VIN)가 인 차량의 데이터가 포함된 문서 개정이 있는 예를 생각해 보세요KM8SRDHF6EU074761. 문서 수정본은 vehicle-registration라는 이름의 원장에 있는 VehicleRegistration 테이블에 있습니다. 이 차량에 대한 문서 수정본과 수정본이 포함된 저널 블록 모두의 무결성을 확인한다고 가정해 보겠습니다.

참고

실제 사용 사례의 맥락에서 암호화 검증의 가치를 설명하는 자세한 AWS 블로그 게시물은 Amazon 을 사용한 실제 암호화 검증QLDB을 참조하세요.

사전 조건

시작하기 전에 다음을 수행해야 합니다.

  1. 의 각 사전 조건을 완료하여 원하는 언어에 대한 QLDB 드라이버를 설정합니다Amazon QLDB 드라이버 시작하기. 여기에는 에 가입 AWS, 개발을 위한 프로그래밍 방식 액세스 권한 부여, 개발 환경 구성이 포함됩니다.

  2. Amazon QLDB 콘솔 시작하기의 1~2단계에 따라 vehicle-registration라는 이름의 원장을 생성하고 사전 정의된 샘플 데이터와 함께 로드하세요.

그런 다음, 다음 단계를 검토하여 확인 작동 방식을 알아본 다음 전체 코드 예제를 처음부터 끝까지 실행합니다.

1단계: 다이제스트 요청

데이터를 확인하려면 먼저 나중에 사용할 수 있도록 원장 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단계: 문서 수정본 쿼리

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단계: 수정에 대한 증명 요청

쿼리 결과를 반복해서 살펴보고 각 블록 주소 및 문서 ID를 원장 이름과 함께 사용하여 GetRevision 요청을 제출합니다. 수정에 대한 증명을 가져오려면 이전에 저장한 다이제스트의 팁 주소도 제공해야 합니다. 이 API 작업은 문서 개정과 개정 증명이 포함된 객체를 반환합니다.

저널 블록 구조 및 내용에 대한 자세한 내용은 문서 메타데이터 쿼리 섹션을 참조하세요.

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을 참조하세요.

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단계: 수정본에서 다이제스트 다시 계산하기

증명의 해시 목록을 사용하여 수정 해시부터 시작하여 다이제스트를 다시 계산합니다. 이전에 저장한 다이제스트를 알고 외부에서 신뢰할 수 있는 한, 다시 계산된 다이제스트 해시가 저장된 다이제스트 해시와 일치하면 문서 개정의 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단계: 저널 블록에 대한 증명 요청

다음으로, 문서 수정본이 포함된 저널 블록을 확인합니다.

1단계에서 저장한 다이제스트의 블록 주소와 팁 주소를 사용하여 GetBlock 요청을 제출합니다. 2단계GetRevision 요청과 마찬가지로, 저장된 다이제스트의 팁 주소를 다시 입력해야 블록에 대한 증명을 받을 수 있습니다. 이 API 작업은 블록과 블록에 대한 증거가 포함된 객체를 반환합니다.

저널 블록 구조 및 콘텐츠에 대한 자세한 콘텐츠는 아마존의 저널 콘텐츠 QLDB 섹션을 참조하세요.

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단계: 블록의 다이제스트 다시 계산하기

증명의 해시 목록을 사용하여 블록 해시부터 시작하여 다이제스트를 다시 계산합니다. 이전에 저장한 다이제스트를 알고 외부에서 신뢰할 수 있는 한 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

전체 코드 예제 실행

다음과 같이 전체 코드 예제를 실행하여 이전 단계를 처음부터 끝까지 모두 수행합니다.

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