├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.html ├── index.js ├── lib ├── config.js ├── constants.js ├── evaluate.js ├── get.js ├── submit.js ├── submitFiles.js ├── utils │ ├── helpers.js │ ├── index.js │ ├── network.js │ └── proofs.js └── verify.js ├── package.json ├── tests ├── config-test.js ├── data │ ├── hashes.json │ ├── nodes.json │ ├── proof-handles.json │ ├── proofs-response.json │ ├── proofs.json │ └── submit-hashes.json ├── e2e-test.js ├── evaluate-test.js ├── get-test.js ├── helpers-test.js ├── helpers.js ├── network-test.js ├── proof-test.js ├── submit-test.js ├── submitFile-test.js └── verify-test.js ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.txt] 17 | trim_trailing_whitespace = false 18 | 19 | [Makefile] 20 | indent_style = tab -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "overrides": { 13 | "files": ["tests/**/*-test.js"], 14 | "env": { 15 | "mocha": true 16 | } 17 | }, 18 | "plugins": ["prettier"], 19 | "rules": { 20 | "prettier/prettier": "error", 21 | "linebreak-style": ["error", "unix"], 22 | "no-console": "off", 23 | "camelcase": [ 24 | "error", 25 | { 26 | "properties": "never", 27 | "ignoreDestructuring": true 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .nyc_output 4 | node_modules/ 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | .vscode 9 | node_modules/ 10 | dist/ 11 | yarn-error.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | cache: 5 | directories: 6 | - node_modules 7 | before_script: yarn run webpack 8 | script: 9 | - yarn run test:$TEST_TYPE 10 | after_success: yarn run coverage 11 | matrix: 12 | include: 13 | - name: 'style tests' 14 | env: TEST_TYPE=lint 15 | - name: 'unit tests' 16 | env: TEST_TYPE=unit 17 | - name: 'e2e tests' 18 | env: TEST_TYPE=e2e 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important: This package is now deprecated. Please use [chainpoint-js](https://github.com/chainpoint/chainpoint-js) instead. 2 | 3 | # Chainpoint Client (JavaScript) 4 | 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 6 | [![npm](https://img.shields.io/npm/l/chainpoint-client.svg)](https://www.npmjs.com/package/chainpoint-client) 7 | [![npm](https://img.shields.io/npm/v/chainpoint-client.svg)](https://www.npmjs.com/package/chainpoint-client) 8 | [![Build Status](https://travis-ci.com/chainpoint/chainpoint-client-js.svg?branch=master)](https://travis-ci.com/chainpoint/chainpoint-client-js) 9 | [![Coverage Status](https://coveralls.io/repos/github/chainpoint/chainpoint-client-js/badge.svg?branch=master)](https://coveralls.io/github/chainpoint/chainpoint-client-js?branch=master) 10 | 11 | ## About 12 | 13 | A client for creating and verifying [Chainpoint](https://chainpoint.org) proofs. 14 | 15 | The Chainpoint Client handles communication with a distributed network of Nodes that make up the Chainpoint Network. 16 | 17 | The Chainpoint Client lets you submit hashes to a Chainpoint Node on the Chainpoint Network. Nodes periodically aggregate hashes and send data to Core for anchoring the hash to public blockchains. 18 | 19 | The Chainpoint Client lets you retrieve and verify a Chainpoint proof. Each proof cryptographically proves the integrity and existence of data at a point in time. 20 | 21 | This client can be used in both Browser and Node.js based JavaScript applications using `callback` functions, Promises (using `.then`, `.catch`), or Promises (using `async`/`await`) functional styles. 22 | 23 | **Important:** This library has been updated for v2 of the Chainpoint network. This means that it won't work for older proofs and instead interacts with nodes on the new network. 24 | If you would like to still use this library for older proofs, please downgrade to v1.x.x 25 | 26 | ## Proof Creation and Verification Overview 27 | 28 | Creating a Chainpoint proof is an asynchronous process. This client handles all the steps for submitting hashes, retrieving proofs, and verifying proofs. 29 | 30 | ### Submit Hash(es) 31 | 32 | This is an HTTP request that passes an Array of hash(es) to a Node. The Node will return a Version 1 UUID for each hash submitted. This `hashidNode` is used later for retrieving a proof. 33 | 34 | ### Get Proof(s) 35 | 36 | Proofs are first anchored to the 'Calendar' chain maintained by every Chainpoint Core. This takes up to ten seconds. Retrieving a `hashIdNode` at this stage returns a proof anchored to the Calendar. 37 | 38 | Proofs are appended with data as they are anchored to additional blockchains. For example, it takes 60 - 90 minutes to anchor a proof to Bitcoin. Calling getProofs will now append the first proof with data that anchors it to the Bitcoin Blockchain. 39 | 40 | Nodes retain proofs for 24 hours. Each client must retrieve and permanently store each Chainpoint proof. 41 | 42 | ### Verify Proof(s) 43 | 44 | Anyone with a Chainpoint proof can verify that it cryptographically anchors to one or more of the public blockchains. The verification process performs the operations in the proof to re-create a Merkle root. This value is compared to a Merkle root stored in the public blockchain. If the values match, the proof is valid. 45 | 46 | ### Evaluate Proof(s) 47 | 48 | This function is similar to the Verify function. The difference with this function is that it only calculates and returns the expected values for each anchor. This function does not verify that the expected values exist on the public blockchains. In most common cases, you will want to use Verify instead. 49 | 50 | ## TL;DR 51 | 52 | [Try It Out with RunKit](https://runkit.com/grempe/tierion-chainpoint-client-async-example) 53 | 54 | ```javascript 55 | const chp = require('chainpoint-client') 56 | 57 | async function runIt() { 58 | // A few sample SHA-256 proofs to anchor 59 | let hashes = [ 60 | '1d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 61 | '2d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 62 | '3d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a' 63 | ] 64 | 65 | // Submit each hash to three randomly selected Nodes 66 | let proofHandles = await chp.submitHashes(hashes) 67 | console.log('Submitted Proof Objects: Expand objects below to inspect.') 68 | console.log(proofHandles) 69 | 70 | // Wait for Calendar proofs to be available 71 | console.log('Sleeping 12 seconds to wait for proofs to generate...') 72 | await new Promise(resolve => setTimeout(resolve, 12000)) 73 | 74 | // Retrieve a Calendar proof for each hash that was submitted 75 | let proofs = await chp.getProofs(proofHandles) 76 | console.log('Proof Objects: Expand objects below to inspect.') 77 | console.log(proofs) 78 | 79 | // Verify every anchor in every Calendar proof 80 | let verifiedProofs = await chp.verifyProofs(proofs) 81 | console.log('Verified Proof Objects: Expand objects below to inspect.') 82 | console.log(verifiedProofs) 83 | } 84 | 85 | runIt() 86 | ``` 87 | 88 | ## Public API 89 | 90 | The following public functions are exported from this client. All functions in the client library are written 91 | using Promises in the async/await style where possible. Previous versions were written in the Nodejs callback 92 | style, but that has since been deprecated. 93 | 94 | Additionally, the output of each function in the process has been designed so that it can be used as the input to the next with no need to manipulate the data. 95 | 96 | ### `submitHashes(hashes, uris)` 97 | 98 | #### Description 99 | 100 | Use this function to submit an Array of hashes, and receive back the information needed to later retrieve a proof for each of those hashes using the `getProofs()` function. 101 | 102 | By default hashes are submitted to three Nodes to help ensure a proof will become available at the appropriate time. Only one such proof need be permanently stored, the others provide redundancy. 103 | 104 | #### Arguments 105 | 106 | The `hashes` argument expects an Array of hashes, where each hash is a Hexadecimal String `[a-fA-F0-9]` between 160 bits (20 Bytes, 40 Hex characters) and 512 bits (64 Bytes, 128 Hex characters) in length. The Hex string must be an even length. 107 | 108 | We recommend using the SHA-256 cryptographic one-way hash function for all hashes submitted. 109 | 110 | The optional `uris` argument accepts an Array of Node URI's as returned by the `getNodes()` function. Each element of the returned Array is a full URI with `scheme://hostname[:port]` (e.g. `http://127.0.0.1` or `http://127.0.0.1:80`). 111 | 112 | #### Return Values 113 | 114 | The return value from this function is an Array of Objects, one for each hash submitted. Each result Object has the information needed to retrieve a proof for a submitted hash. There will be one Object for every Node a hash was submitted to. 115 | 116 | The Array of Objects, referred to as `proofHandles` can also be submitted directly as the argument to the `getProofs()` function. It typically takes about 10 seconds for initial Calendar proofs to become available. 117 | 118 | The Object will contain: 119 | 120 | `uri` : The URI of the Node(s) the hash was submitted to. This is the only Node that can retrieve this particular proof. 121 | 122 | `hash` : A copy of the hash that was originally submitted that will be embedded in a future proof. This allows for easier correlation between hashes submitted and the Hash ID handle needed to retrieve a proof. 123 | 124 | `hashIdNode` : The Version 1 UUID that can be used to retrieve the proof for a submitted hash from the `/proofs/:id` endpoint of the Node it was submitted to. 125 | 126 | `groupId` : A Version 1 UUID which is used to group Proof Handles that have the same corresponding hash. The groupId can later be used to optimize the proof retrieval process. 127 | 128 | Example Return Value 129 | 130 | ```javascript 131 | ;[ 132 | { 133 | uri: 'http://0.0.0.0', 134 | hash: '1d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 135 | proofId: 'df500460-d7d1-11e8-992b-0178d9540713', 136 | groupId: 'dfa4b410-d7d1-11e8-a6e3-c763418c848e' 137 | }, 138 | { 139 | uri: 'http://0.0.0.0', 140 | hash: '2d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 141 | proofId: 'df502b70-d7d1-11e8-992b-0187b4b6e491', 142 | groupId: 'dfa4b411-d7d1-11e8-a6e3-c763418c848e' 143 | } 144 | ] 145 | ``` 146 | 147 | ### `submitFileHashes(paths, uris)` 148 | 149 | #### Description 150 | 151 | Use this function to submit hashes calculated from an Array of file paths, and receive back the information needed to later retrieve a proof for each of those hashes using the `getProofs()` function. 152 | 153 | By default hashes are submitted to three Nodes to help ensure a proof will become available at the appropriate time. Only one such proof need be permanently stored, the others provide redundancy. 154 | 155 | #### Arguments 156 | 157 | The `paths` argument expects an Array of valid file paths. 158 | 159 | The SHA-256 cryptographic one-way hash function will be used on all files in the paths submitted. 160 | 161 | The optional `uris` argument accepts an Array of Node URI's as returned by the `getNodes()` function. Each element of the returned Array is a full URI with `scheme://hostname[:port]` (e.g. `http://127.0.0.1` or `http://127.0.0.1:80`). 162 | 163 | #### Return Values 164 | 165 | The return value from this function is an Array of Objects, one for each hash submitted. Each result Object has the information needed to retrieve a proof for a submitted hash. There will be one Object for every Node a hash was submitted to. 166 | 167 | The Array of Objects, referred to as `proofHandles` can also be submitted directly as the argument to the `getProofs()` function. It typically takes about 10 seconds for initial Calendar proofs to become available. 168 | 169 | The Object will contain: 170 | 171 | `uri` : The URI of the Node(s) the hash was submitted to. This is the only Node that can retrieve this particular proof. 172 | 173 | `hash` : A copy of the hash that was originally submitted that will be embedded in a future proof. This allows for easier correlation between hashes submitted and the Hash ID handle needed to retrieve a proof. 174 | 175 | `hashIdNode` : The Version 1 UUID that can be used to retrieve the proof for a submitted hash from the `/proofs/:id` endpoint of the Node it was submitted to. 176 | 177 | `path` : The path of the file represented by this object. 178 | 179 | `groupId` : A Version 1 UUID which is used to group Proof Handles that have the same corresponding hash. The groupId can later be used to optimize the proof retrieval process. 180 | 181 | Example Return Value 182 | 183 | ```javascript 184 | ;[ 185 | { 186 | uri: 'http://0.0.0.0', 187 | hash: '9d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 188 | proofId: 'a512e430-d3cb-11e7-aeb7-01eecbb37e34', 189 | path: './datafile.json', 190 | groupId: 'dc1c8cd0-d7d3-11e8-8a5c-7fe62f82e5c3' 191 | }, 192 | { 193 | uri: 'http://0.0.0.0', 194 | hash: '9d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 195 | proofId: 'a4b6e180-d3cb-11e7-90bc-014342a27e15', 196 | path: './folder/otherfile.csv', 197 | groupId: 'dc1c8cd1-d7d3-11e8-8a5c-7fe62f82e5c3' 198 | } 199 | ] 200 | ``` 201 | 202 | ### `getProofs(proofHandles)` 203 | 204 | #### Description 205 | 206 | This function is used to retrieve Chainpoint proofs from the Nodes that are responsible for creating each proof. 207 | 208 | #### Arguments 209 | 210 | The `proofHandles` argument accepts an Array of Objects. Each object must have the `uri` and `hashIdNode` properties. The argument is of the same form as the output from the `submitHashes()` function. 211 | 212 | The `uri` property should be the base URI (e.g. `http://0.0.0.0`) of an online Node that is responsible for generating a particular proof. 213 | 214 | The `hashIdNode` property is a valid Version 1 UUID as provided by the return of the `submitHashes()` function. 215 | 216 | #### Return Values 217 | 218 | This function will return an Array of Objects, each composed of the following properties: 219 | 220 | ```javascript 221 | { 222 | hashIdNode: "", 223 | proof: "", 224 | anchorsComplete: [] 225 | } 226 | ``` 227 | 228 | `hashIdNode` : The Version 1 UUID used to retrieve the proof. 229 | 230 | `proof` : The Base64 encoded binary form of the proof. See [https://github.com/chainpoint/chainpoint-binary](https://github.com/chainpoint/chainpoint-binary) for more information about proof formats. That library can also be used to convert from one form to another. If the proof is not yet available, or cannot be retrieved, this will be set to `null`. 231 | 232 | `anchorsComplete` : An Array of Strings that indicates which blockchains the proof is anchored to at the time of retrieval. One or more of `cal` (Calendar), `btc` (Bitcoin), or `eth` (Ethereum) (you may also see `tcal` or `tbtc` for 233 | testnet calendar anchors and testnet bitcoin anchors respectively). If the proof is not yet available, or cannot be retrieved, this will be set to `[]` (An empty Array). 234 | 235 | Example Return Value 236 | 237 | ```javascript 238 | ;[ 239 | { 240 | proofId: 'e47f00b0-d3fb-11e7-9dd9-015eb614885c', 241 | proof: 242 | 'eJylVc1qZFUQ9jlcu+10VZ3/rAKCT+DKTahzqk7SENKhux3HZRRcO/oEo5EZB1wIrn2PgA/jdzMZBye9EDzQNPfce+vUV9/P/e712dheH/z54c/Lw+Fmf7pefxU2drLdXazHpW6ub7ab68P6Wbw7fH3jbz79Z+vuUveX92cTS4qITA85G/Xaau0lsIzcNZN7FIlcrUnPMmz0wtOlNXYvofHrm912O883dv9JcoohGK1iNl0xu660NF8Rl8ktJkmFf1tOPd/58M0ztz+EhFYkKwqfSzhlOpX6xeu+0+tx6fsX3/5ypd2v3ujFxc4v9LDd/by92X//5IHfh16dL1vb3fnbe8tzP96+vLr/4XqzPzyTU06VSuFIdGpJtcxpappm7m3kEgOuJabBJl2GBoCvjv+USubKo1aZpQwfpkEDRauucTYlaaEGppa4FhRLSh2VuVtp1oKpdDWLyW1Gwu3c2G5/2t682l/qSlK+ffW2bUB54OfuACi/PkLBTM8Kpza9JurZrCeeM5GNaDF2Vg0ZPFVPohbbFJ6Wa+dhNErRqTna3Ze7zf7FfV+0AWmEEw7pJMUTkbTGUX5tulv/30PWpgd9z8o3j6z0w3jKyl8ffXz7cgfdxSJubc5AENdE6WYFs7c6eY7SKI3Yi1DsOgTjJHTHKUBxrZah+V9TBNFnXQuVnspsrgapFsy/hcjFZuvoP1QAaD59cPIIdgc0nafW0lWotA8LHojp7WLOTCkPOCR0cJgwnjFnjR6oJ2MgIYtauIh2ibk76gaTisLSa31XB2s+Lsj+gyWS0cYyms+oOmwHwO/Oz/hFFfU0CP2Tt9BhNEmN2oxQbKylhJKxM8a7txY8bx7xrJ7Lw4wwuJGB2VKdnSquLJeStSpVgmAJ7A7vLoaThMOoLTt24JQ+Q9ZjNQOZxuGhSx6Jos/u8FVHY1181NnIK4dGwTKlJTtyVsCNM4DrPtuTmhCHyezchKMs7eYM1OitthkCmAsJUikt8yzRfUQei1o7DN47tIJkOlYzBsScLuFGGMCkwX2Bamla8bYYthVkHQQBpAHe56acyEchDNjisZo12GgKqI20gg1UcYytN4+Ihe5hlh7YJViKozLrAPTpS9dtQp7HaqJLR5MTIcTQCQIpDlbOyF1TpUixjp6RWAvkjkypHGud8C7AxTSP1SzuME9VnGo+Ug8jtw6CK/YlB1F2CIgKNyR1aosXuaWpHmJJedajHFGKnA0yQu5Dsci90BGqYXmTvWFkzXoVyLiARSmlGK4sFITlSPlYTWU4N9tMMLgoOKKJbgesBA/2aAKbxTLr4Iqp41ihNnpY1Kw08/iw5pN0RSS9T9dXnHOssf6njMTnDiKLCHumPvISB8IemuJjWQgx06pDB6CENKFj6DZQJoP78WXo5SEj/wbUW3Ki', 243 | anchorsComplete: ['tcal', 'tbtc'] 244 | } 245 | ] 246 | ``` 247 | 248 | ### `verifyProofs (proofs, uri)` 249 | 250 | #### Description 251 | 252 | This function is used to verify proofs by comparing the values arrived at by parsing and performing the operations in a proof against the Chainpoint calendar or public blockchains. 253 | 254 | #### Arguments 255 | 256 | The `proofs` argument accepts an Array of Strings or Objects. 257 | 258 | If a String, it is expected to be a Chainpoint 4.0 proof in either Base64 encoded binary, or JSON-LD form. 259 | 260 | If an Object it can be a Chainpoint 4.0 proof as an Object, or have a `proof` property containing a String proof as described above as is created by the output of `getProofs()`. 261 | 262 | Proof types can be mixed freely in the `proofs` arg Array. 263 | 264 | The `uri` property should be the base URI (e.g. `http://0.0.0.0`) of an online Node that will be responsible for providing a hash value from the Calendar block specified in the proof. The hash value provided will then be compared to the result of calculating all of the operations in the proof locally. If the locally calculated values matches the server provided value it verifies that the proof is valid. 265 | 266 | At no time is the proof sent over the Internet during this process (although it is safe to do so). 267 | 268 | #### Return Values 269 | 270 | This function will return an Array of Objects. Each object represents an Anchor in a proof along with all of the relevant data. 271 | 272 | For example, a single proof that is anchored to both the Chainpoint Calendar, and to the Bitcoin blockchain, will return two objects. One for each of those anchors. 273 | 274 | Example Return Value 275 | 276 | ```javascript 277 | ;[ 278 | { 279 | hash: 'daeaedcd320c0fb2adefaab15ec03a424bb7a89aa0ec918c6c4906c366c67e36', 280 | proof_id: '5e0433d0-46da-11ea-a79e-017f19452571', 281 | hash_received: '2020-02-03T23:10:28Z', 282 | uri: 'http://127.0.0.1/calendar/695928/hash', 283 | type: 'cal', 284 | anchorId: '695928', 285 | expectedValue: 'ff0fb5903d3b6deed2ee2ebc033813e7b0357de4af2e7b1d52784baad40a0d13', 286 | verified: true, 287 | verifiedAt: '2017-11-28T22:52:20Z' 288 | }, 289 | { 290 | hash: 'daeaedcd320c0fb2adefaab15ec03a424bb7a89aa0ec918c6c4906c366c67e36', 291 | proof_id: '5e0433d0-46da-11ea-a79e-017f19452571', 292 | hash_received: '2020-02-03T23:10:28Z', 293 | uri: 'http://127.0.0.1/calendar/696030/data', 294 | type: 'btc', 295 | anchorId: '496469', 296 | expectedValue: 'de999f26afcdd855552ca91184aba496baa48bf59a7125180d7c1d7d520ea88b', 297 | verified: true, 298 | verifiedAt: '2017-11-28T22:52:20Z' 299 | } 300 | ] 301 | ``` 302 | 303 | ### `evaluateProofs (proofs)` 304 | 305 | #### Description 306 | 307 | This function is used to evaluate proofs and returns the values arrived at by parsing and performing the operations in a proof. 308 | 309 | For example, this can be used to easily verify that a proof that is anchored to the Bitcoin blockchain is valid, without trusting any other third party service. The only thing required is a copy of the Bitcoin block headers, available from any BTC full node or block explorer. 310 | 311 | #### Arguments 312 | 313 | The `proofs` argument accepts an Array of Strings or Objects. 314 | 315 | If a String, it is expected to be a Chainpoint 4.0 proof in either Base64 encoded binary, or JSON-LD form. 316 | 317 | If an Object it can be a Chainpoint 4.0 proof as an Object, or have a `proof` property containing a String proof as described above as is created by the output of `getProofs()`. 318 | 319 | Proof types can be mixed freely in the `proofs` arg Array. 320 | 321 | This process is handled entirely offline. At no time is the proof sent over the Internet during this process (although it is safe to do so). 322 | 323 | #### Return Values 324 | 325 | This function will return an Array of Objects. Each object represents an Anchor in a proof along with all of the relevant data. 326 | 327 | For example, a single proof that is anchored to both the Chainpoint Calendar, and to the Bitcoin blockchain, will return two objects. One for each of those anchors. 328 | 329 | Example Return Value 330 | 331 | ```javascript 332 | ;[ 333 | { 334 | hash: 'daeaedcd320c0fb2adefaab15ec03a424bb7a89aa0ec918c6c4906c366c67e36', 335 | proof_id: '5e0433d0-46da-11ea-a79e-017f19452571', 336 | hash_received: '2020-02-03T23:10:28Z', 337 | uri: 'http://127.0.0.1/calendar/695928/hash', 338 | type: 'cal', 339 | anchorId: '695928', 340 | expectedValue: 'ff0fb5903d3b6deed2ee2ebc033813e7b0357de4af2e7b1d52784baad40a0d13' 341 | }, 342 | { 343 | hash: 'daeaedcd320c0fb2adefaab15ec03a424bb7a89aa0ec918c6c4906c366c67e36', 344 | proof_id: '5e0433d0-46da-11ea-a79e-017f19452571', 345 | hash_received: '2020-02-03T23:10:28Z', 346 | uri: 'http://127.0.0.1/calendar/696030/data', 347 | type: 'btc', 348 | anchorId: '496469', 349 | expectedValue: 'de999f26afcdd855552ca91184aba496baa48bf59a7125180d7c1d7d520ea88b' 350 | } 351 | ] 352 | ``` 353 | 354 | In this case, you can use a block explorer to confirm that BTC block ID `496469` has a block Merkle root value (`expectedValue`) of `de999f26afcdd855552ca91184aba496baa48bf59a7125180d7c1d7d520ea88b`. If it does, that means this proof can be provably said to anchor its hash to that Bitcoin block. 355 | 356 | ### `getCores (num)` 357 | 358 | #### Description 359 | 360 | This is a utility function that allows you to perform DNS based auto-discovery of a available Core instance URI addresses. 361 | 362 | This function is not required to be explicitly called when using the main functions of this library. It will be called internally as needed. 363 | 364 | #### Arguments 365 | 366 | The optional `num` argument determines the maximum number of Cores that should be returned in a single request in randomized order. 367 | 368 | #### Return Values 369 | 370 | This function returns an Array of String URIs. 371 | 372 | ### `getNodes (num)` 373 | 374 | #### Description 375 | 376 | This is a utility function that allows you to perform DNS based auto-discovery of Node URIs. 377 | 378 | This function is not required to be explicitly called when using the main functions of this library. It will be called internally as needed. 379 | 380 | #### Arguments 381 | 382 | The optional `num` argument determines the maximum number of Nodes that should be returned in a single request in randomized order. The number of URI's returned are ultimately limited by the number of Nodes returned by Core's auto-discovery mechanism. 383 | 384 | #### Return Values 385 | 386 | This function returns an Array of String URIs. The list of Nodes returned are for Nodes that have recently been audited and found to be healthy. 387 | 388 | ## Usage : Functional Styles 389 | 390 | This client can be used with several popular JavaScript API styles in both Node.js and the Browser. 391 | The choice of API style is left to the developer and should be based on support for 392 | each style in the intended runtime platform and the developer's preference. 393 | 394 | The callback style is fully supported everywhere, 395 | while Promises and `async/await` support will depend on the version of Node.js or Browser 396 | being targetted. Each public API exported from this module supports each style equally. 397 | 398 | ### Callback Style Example [DEPRECATED] 399 | 400 | ```javascript 401 | var cp = require('chainpoint-client') 402 | 403 | let hashes = ['9d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a'] 404 | 405 | cp.submitHashes(hashes, function(err, data) { 406 | if (err) { 407 | // `err` will contain any returned Error object and halt execution 408 | throw err 409 | } 410 | 411 | // If no error `data` will contain the returned values 412 | console.log(JSON.stringify(data, null, 2)) 413 | }) 414 | ``` 415 | 416 | ### Promises `.then/.catch` Style Example 417 | 418 | ```javascript 419 | var cp = require('chainpoint-client') 420 | 421 | let hashes = ['9d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a'] 422 | 423 | cp.submitHashes(hashes, testNodesArray) 424 | .then(function(data) { 425 | // `data` will contain the returned values 426 | console.log(JSON.stringify(data, null, 2)) 427 | }) 428 | .catch(function(err) { 429 | // `err` will contain any returned Error object 430 | console.log(err) 431 | }) 432 | ``` 433 | 434 | ### Promises `async/await` Style Example 435 | 436 | ```javascript 437 | var cp = require('chainpoint-client') 438 | 439 | let hashes = ['9d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a'] 440 | 441 | async function runIt() { 442 | let data = await cp.submitHashes(hashes) 443 | console.log(JSON.stringify(data, null, 2)) 444 | } 445 | 446 | runIt() 447 | ``` 448 | 449 | ### JavaScript Client-Side Frameworks Example 450 | 451 | Note: If you are using any client-side JavaScript framework (ex. Angular, React, etc) remember to import chainpoint-client in the following manner: 452 | 453 | ```js 454 | import chainpoint from 'chainpoint-client/dist/bundle.web' 455 | ``` 456 | 457 | or 458 | 459 | ```js 460 | const chainpoint = require('chainpoint-client/dist/bundle.web') 461 | ``` 462 | 463 | ### Browser Script Tag Example 464 | 465 | You can copy `dist/bundle.web.js` into your app to be served from your own web server and included in a script tag. 466 | 467 | Or install the `npm` package in a place available to your web server pages and set the `script src` tag as shown in the example below. A set of window global functions (e.g. `chainpointClient.submitHashes()`) will then be available for use in a fashion similar to that shown in the examples above. 468 | 469 | ```html 470 | 38 | 39 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import utils from './lib/utils' 15 | import _submitHashes from './lib/submit' 16 | import _submitFileHashes from './lib/submitFiles' 17 | import _getProofs from './lib/get' 18 | import _verifyProofs from './lib/verify' 19 | import _evaluateProofs from './lib/evaluate' 20 | 21 | const { flattenBtcBranches, normalizeProofs, parseProofs, getCores: _getCores, getNodes: _getNodes } = utils 22 | 23 | /** 24 | * retrieve raw btc tx objects for corresponding proofs 25 | * @param {Array} proofs - An Array of String, or Object proofs from getProofs(), to be evaluated. Proofs can be in any of the supported JSON-LD or Binary formats. 26 | * @returns {Object[]} - array of objects with relevant hash data 27 | */ 28 | 29 | export function getProofTxs(proofs) { 30 | let normalizedProofs = normalizeProofs(proofs) 31 | let parsedProofs = parseProofs(normalizedProofs) 32 | let flatProofs = flattenBtcBranches(parsedProofs) 33 | return flatProofs 34 | } 35 | 36 | // Need this to keep expected import structure for backwards compatibility 37 | // with downstream dependencies 38 | export const submitHashes = _submitHashes 39 | export const submitFileHashes = _submitFileHashes 40 | export const getProofs = _getProofs 41 | export const verifyProofs = _verifyProofs 42 | export const evaluateProofs = _evaluateProofs 43 | export const getNodes = _getNodes 44 | export const getCores = _getCores 45 | 46 | export default { 47 | getCores, 48 | getNodes, 49 | submitHashes, 50 | submitFileHashes, 51 | getProofs, 52 | verifyProofs, 53 | evaluateProofs, 54 | getProofTxs 55 | } 56 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import Config from 'bcfg' 15 | 16 | let config = null 17 | 18 | function getConfig(options = {}) { 19 | // create a new config module for chainpoint 20 | // this will set the prefix to `~/.chainpoint` 21 | // and also parse env vars that are prefixed with `CHAINPOINT_` 22 | config = new Config('chainpoint') 23 | config.inject(options) 24 | config.load({ 25 | // Parse URL hash 26 | hash: true, 27 | // Parse querystring 28 | query: true, 29 | // Parse environment 30 | env: true, 31 | // Parse args 32 | argv: true 33 | }) 34 | 35 | // Will parse [PREFIX]/chainpoint.conf (throws on FS error). 36 | // PREFIX defaults to `~/.chainpoint` 37 | // can change the prefix by passing in a `prefix` option 38 | config.open('chainpoint.conf') 39 | return config 40 | } 41 | 42 | export default getConfig 43 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | // NETWORK CONSTANTS 15 | export const NODE_PROXY_URI = 'https://node-proxy.chainpoint.org:443' 16 | export const DNS_CORE_DISCOVERY_ADDR = '_core.addr.chainpoint.org' 17 | -------------------------------------------------------------------------------- /lib/evaluate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import { normalizeProofs, parseProofs, flattenProofs } from './utils/proofs' 14 | /** 15 | * Evaluates the expected anchor values for a collection of proofs 16 | * 17 | * @param {Array} proofs - An Array of String, or Object proofs from getProofs(), to be evaluated. Proofs can be in any of the supported JSON-LD or Binary formats. 18 | */ 19 | export function evaluateProofs(proofs) { 20 | let normalizedProofs = normalizeProofs(proofs) 21 | let parsedProofs = parseProofs(normalizedProofs) 22 | let flatProofs = flattenProofs(parsedProofs) 23 | 24 | return flatProofs 25 | } 26 | 27 | export default evaluateProofs 28 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { isEmpty, forEach, map, every, reject, keys, flatten, mapKeys, camelCase } from 'lodash' 15 | 16 | import { isValidNodeURI } from './utils/network' 17 | import { isValidProofHandle } from './utils/proofs' 18 | import { isSecureOrigin, isValidUUID, fetchEndpoints, testArrayArg } from './utils/helpers' 19 | import getConfig from './config' 20 | import { NODE_PROXY_URI } from './constants' 21 | 22 | let config = getConfig() 23 | /** 24 | * Retrieve a collection of proofs for one or more hash IDs from the appropriate Node(s) 25 | * The output of `submitProofs()` can be passed directly as the `proofHandles` arg to 26 | * this function. 27 | * 28 | * @param {Array<{uri: String, hashIdNode: String}>} proofHandles - An Array of Objects, each Object containing 29 | * all info needed to retrieve a proof from a specific Node. 30 | * @param {function} callback - An optional callback function. 31 | * @return {Array<{uri: String, hashIdNode: String, proof: String}>} - An Array of Objects, each returning the 32 | * URI the proof was returned from and the Proof in Base64 encoded binary form. 33 | */ 34 | async function getProofs(proofHandles) { 35 | // Validate all proofHandles provided 36 | testArrayArg(proofHandles) 37 | if ( 38 | !every(proofHandles, h => { 39 | return isValidProofHandle(h) 40 | }) 41 | ) 42 | throw new Error('proofHandles Array contains invalid Objects') 43 | if (proofHandles.length > 250) throw new Error('proofHandles arg must be an Array with <= 250 elements') 44 | 45 | // Validate that *all* URI's provided are valid or throw 46 | let badHandleURIs = reject(proofHandles, function(u) { 47 | return isValidNodeURI(u.uri) 48 | }) 49 | if (!isEmpty(badHandleURIs)) 50 | throw new Error( 51 | `some proof handles contain invalid URI values : ${map(badHandleURIs, h => { 52 | return h.uri 53 | }).join(', ')}` 54 | ) 55 | 56 | // Validate that *all* hashIdNode's provided are valid or throw 57 | let badHandleUUIDs = reject(proofHandles, function(u) { 58 | return isValidUUID(u.proofId) 59 | }) 60 | if (!isEmpty(badHandleUUIDs)) 61 | throw new Error( 62 | `some proof handles contain invalid hashIdNode UUID values : ${map(badHandleUUIDs, h => { 63 | return h.proofId 64 | }).join(', ')}` 65 | ) 66 | 67 | try { 68 | // Collect together all proof UUIDs destined for a single Node 69 | // so they can be submitted to the Node in a single request. 70 | let uuidsByNode = {} 71 | forEach(proofHandles, handle => { 72 | if (isEmpty(uuidsByNode[handle.uri])) { 73 | uuidsByNode[handle.uri] = [] 74 | } 75 | uuidsByNode[handle.uri].push(handle.proofId) 76 | }) 77 | 78 | // For each Node construct a set of GET options including 79 | // the `hashids` header with a list of all hash ID's to retrieve 80 | // proofs for from that Node. 81 | let nodesWithGetOpts = map(keys(uuidsByNode), node => { 82 | let headers = Object.assign( 83 | { 84 | accept: 'application/json', 85 | 'content-type': 'application/json' 86 | }, 87 | { 88 | hashids: uuidsByNode[node].join(',') 89 | }, 90 | isSecureOrigin() 91 | ? { 92 | 'X-Node-Uri': node 93 | } 94 | : {} 95 | ) 96 | let getOptions = { 97 | method: 'GET', 98 | uri: (isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) : node) + '/proofs', 99 | body: {}, 100 | headers, 101 | timeout: 10000 102 | } 103 | return getOptions 104 | }) 105 | 106 | // Perform parallel GET requests to all Nodes with proofs 107 | const parsedBody = await fetchEndpoints(nodesWithGetOpts) 108 | // fetchEndpoints returns an Array entry for each host it submits to. 109 | let flatParsedBody = flatten(parsedBody) 110 | 111 | let proofsResponse = [] 112 | 113 | forEach(flatParsedBody, proofResp => { 114 | // Set to empty Array if unset of null 115 | proofResp.anchors_complete = proofResp.anchors_complete || [] 116 | // Camel case object keys 117 | let proofRespCamel = mapKeys(proofResp, (v, k) => camelCase(k)) 118 | proofsResponse.push(proofRespCamel) 119 | }) 120 | return proofsResponse 121 | } catch (err) { 122 | console.error(err.message) 123 | throw err 124 | } 125 | } 126 | 127 | export default getProofs 128 | -------------------------------------------------------------------------------- /lib/submit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { isEmpty, reject, uniq, map, forEach } from 'lodash' 15 | 16 | import { isHex, isSecureOrigin, fetchEndpoints, validateHashesArg, validateUrisArg } from './utils/helpers' 17 | import { isValidNodeURI, getNodes } from './utils/network' 18 | import { mapSubmitHashesRespToProofHandles } from './utils/proofs' 19 | import { NODE_PROXY_URI } from './constants' 20 | import getConfig from './config' 21 | 22 | let config = getConfig() 23 | /** 24 | * Submit hash(es) to one or more Nodes, returning an Array of proof handle objects, one for each submitted hash and Node combination. 25 | * @param {Array} hashes - An Array of String Hashes in Hexadecimal form. 26 | * @param {Array} uris - An Array of String URI's. Each hash will be submitted to each Node URI provided. If none provided three will be chosen at random using service discovery. 27 | * @return {Array<{uri: String, hash: String, hashIdNode: String, groupId: String}>} An Array of Objects, each a handle that contains all info needed to retrieve a proof. 28 | */ 29 | export async function submitHashes(hashes, uris) { 30 | uris = uris || [] 31 | let nodes 32 | 33 | // Validate args before doing anything else 34 | validateHashesArg(hashes, h => isHex(h)) 35 | validateUrisArg(uris) 36 | 37 | if (isEmpty(uris)) { 38 | // get a list of nodes via service discovery 39 | nodes = await getNodes(3) 40 | } else { 41 | // eliminate duplicate URIs 42 | uris = uniq(uris) 43 | 44 | // non-empty, check that *all* are valid or throw 45 | let badURIs = reject(uris, function(h) { 46 | return isValidNodeURI(h) 47 | }) 48 | if (!isEmpty(badURIs)) throw new Error(`uris arg contains invalid URIs : ${badURIs.join(', ')}`) 49 | // all provided URIs were valid 50 | nodes = uris 51 | } 52 | 53 | try { 54 | // Setup an options Object for each Node we'll submit hashes to. 55 | // Each Node will then be sent the full Array of hashes. 56 | let nodesWithPostOpts = map(nodes, node => { 57 | let uri = isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) : node 58 | let headers = Object.assign( 59 | { 60 | 'Content-Type': 'application/json', 61 | Accept: 'application/json' 62 | }, 63 | isSecureOrigin() 64 | ? { 65 | 'X-Node-Uri': node 66 | } 67 | : {} 68 | ) 69 | 70 | let postOptions = { 71 | method: 'POST', 72 | uri: uri + '/hashes', 73 | body: { 74 | hashes: hashes 75 | }, 76 | headers, 77 | timeout: 10000 78 | } 79 | return postOptions 80 | }) 81 | 82 | // All requests succeed in parallel or all fail. 83 | const parsedBody = await fetchEndpoints(nodesWithPostOpts) 84 | 85 | // Nodes cannot be guaranteed to know what IP address they are reachable 86 | // at, so we need to amend each result with the Node URI it was submitted 87 | // to so that proofs may later be retrieved from the appropriate Node(s). 88 | // This mapping relies on that fact that fetchEndpoints returns results in the 89 | // same order that options were passed to it so the results can be mapped to 90 | // the Nodes submitted to. 91 | forEach(nodes, (uri, index) => { 92 | if (parsedBody[index]) parsedBody[index].meta.submitted_to = uri 93 | }) 94 | 95 | // Map the API response to a form easily consumable by getProofs 96 | let proofHandles = mapSubmitHashesRespToProofHandles(parsedBody) 97 | 98 | return proofHandles 99 | } catch (err) { 100 | console.error(err.message) 101 | throw err 102 | } 103 | } 104 | 105 | // Expose functions 106 | export default submitHashes 107 | -------------------------------------------------------------------------------- /lib/submitFiles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import * as submit from './submit' 15 | import { getFileHashes, validateUrisArg } from './utils/helpers' 16 | 17 | /** 18 | * Submit hash(es) of selected file(s) to one or more Nodes, returning an Array of proof handle objects, one for each submitted hash and Node combination. 19 | * @param {Array} paths - An Array of paths of the files to be hashed. 20 | * @param {Array} uris - An Array of String URI's. Each hash will be submitted to each Node URI provided. If none provided three will be chosen at random using service discovery. 21 | * @return {Array<{path: String, uri: String, hash: String, hashIdNode: String, groupId: String}>} An Array of Objects, each a handle that contains all info needed to retrieve a proof. 22 | */ 23 | async function submitFileHashes(paths, uris) { 24 | uris = uris || [] 25 | const hashObjs = await getFileHashes(paths) 26 | const hashes = hashObjs.map(hashObj => hashObj.hash) 27 | // Validate all Node URIs provided 28 | validateUrisArg(uris) 29 | 30 | const proofHandles = await submit.submitHashes(hashes, uris) 31 | return proofHandles.map(proofHandle => { 32 | proofHandle.path = hashObjs.find(hashObj => hashObj.hash === proofHandle.hash).path 33 | return proofHandle 34 | }) 35 | } 36 | 37 | export default submitFileHashes 38 | -------------------------------------------------------------------------------- /lib/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import crypto from 'crypto' 3 | import uuidValidate from 'uuid-validate' 4 | import fetch from 'node-fetch' 5 | import { isEmpty, isArray, reject, isFunction } from 'lodash' 6 | 7 | /** 8 | * Checks if value is a hexadecimal string 9 | * 10 | * @param {string} value - The value to check 11 | * @returns {bool} true if value is a hexadecimal string, otherwise false 12 | */ 13 | export function isHex(value) { 14 | var hexRegex = /^[0-9a-f]{2,}$/i 15 | var isHex = hexRegex.test(value) && !(value.length % 2) 16 | return isHex 17 | } 18 | 19 | /** 20 | * Checks if a UUID is a valid v1 UUID. 21 | * 22 | * @param {string} uuid - The uuid to check 23 | * @returns {bool} true if uuid is valid, otherwise false 24 | */ 25 | export function isValidUUID(uuid) { 26 | if (uuidValidate(uuid, 1)) return true 27 | return false 28 | } 29 | 30 | /** 31 | * Check if client is being used over an https connection 32 | * @returns {bool} true if served over https 33 | */ 34 | export function isSecureOrigin() { 35 | return typeof window === 'object' && window.location.protocol === 'https:' 36 | } 37 | 38 | export function sha256FileByPath(path) { 39 | return new Promise((resolve, reject) => { 40 | let sha256 = crypto.createHash('sha256') 41 | let readStream = fs.createReadStream(path) 42 | readStream.on('data', data => sha256.update(data)) 43 | readStream.on('end', () => { 44 | let hash = sha256.digest('hex') 45 | resolve({ 46 | path, 47 | hash 48 | }) 49 | }) 50 | readStream.on('error', err => { 51 | if (err.code === 'EACCES') { 52 | resolve({ 53 | path: path, 54 | hash: null, 55 | error: 'EACCES' 56 | }) 57 | } 58 | reject(err) 59 | }) 60 | }) 61 | } 62 | 63 | export function fetchEndpoints(arr) { 64 | return Promise.all( 65 | arr.map(async currVal => { 66 | let obj = JSON.parse(JSON.stringify(currVal)) 67 | let method = obj.method 68 | let uri = obj.uri 69 | let body = obj.body && Object.keys(obj.body).length ? JSON.stringify(obj.body) : undefined 70 | 71 | delete obj.method 72 | delete obj.uri 73 | delete obj.body 74 | let res = await fetch(uri, { method, ...obj, body }) 75 | let res1 = res.clone() 76 | 77 | return res.json().catch(() => res1.text()) 78 | }) 79 | ) 80 | } 81 | 82 | /* 83 | * Helper function to validate a hashes argument that would be passed to other functions 84 | * @param {Array} hashes - An Array of String Hashes in Hexadecimal form. 85 | * @param {Function} validator - a function to validate the array of items being validated 86 | * @returns {void} 87 | */ 88 | export function validateHashesArg(args, validator) { 89 | // Validate all hashes provided 90 | if (!isArray(args)) throw new Error('1st arg must be an Array') 91 | if (isEmpty(args)) throw new Error('1st arg must be a non-empty Array') 92 | if (args.length > 250) throw new Error('1st arg must be an Array with <= 250 elements') 93 | 94 | if (!validator || !isFunction(validator)) throw new Error('Need a validator function to test argument') 95 | let rejects = reject(args, validator) 96 | if (!isEmpty(rejects)) throw new Error(`arg contains invalid items : ${rejects.join(', ')}`) 97 | } 98 | 99 | /* 100 | * Helper function to validate a hashes argument that would be passed to other functions 101 | * @param {Array} hashes - An Array of String Hashes in Hexadecimal form. 102 | * @returns {void} 103 | */ 104 | export function validateUrisArg(uris) { 105 | if (!isArray(uris)) throw new Error('uris arg must be an Array of String URIs') 106 | if (uris.length > 5) throw new Error('uris arg must be an Array with <= 5 elements') 107 | } 108 | 109 | /** 110 | * Get SHA256 hash(es) of selected file(s) and prepare for submitting to a node 111 | * @param {Array} paths - An Array of paths of the files to be hashed. 112 | * @param {Array} uris - An Array of String URI's. Each hash will be submitted to each Node URI provided. 113 | * If none provided three will be chosen at random using service discovery. 114 | * @returns {Array<{path: String, hash: String} An Array of Objects, each a handle that contains all info needed to retrieve a proof. 115 | */ 116 | export async function getFileHashes(paths) { 117 | // Validate all paths provided 118 | // Criteria is the same as for hashes arg so can reuse the helper 119 | // except need a different validator function 120 | validateHashesArg(paths, path => fs.existsSync(path) && fs.lstatSync(path).isFile()) 121 | 122 | let hashObjs = [] 123 | hashObjs = await Promise.all(paths.map(path => sha256FileByPath(path))) 124 | 125 | // filter out any EACCES errors 126 | hashObjs = hashObjs.filter(hashObj => { 127 | if (hashObj.error === 'EACCES') console.error(`Insufficient permission to read file '${hashObj.path}', skipping`) 128 | return hashObj.error !== 'EACCES' 129 | }) 130 | return hashObjs 131 | } 132 | 133 | /** 134 | * throws if the first arg is not an array or is an empty array 135 | */ 136 | export function testArrayArg(arg) { 137 | if (!isArray(arg)) throw new Error('Argument must be an Array') 138 | if (isEmpty(arg)) throw new Error(`Argument ${arg} must be a non-empty Array`) 139 | } 140 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | // Entry point for client utilities 15 | 16 | import * as helpers from './helpers' 17 | import * as proofs from './proofs' 18 | import * as network from './network' 19 | 20 | export { helpers, proofs, network } 21 | export default { ...helpers, ...proofs, ...network } 22 | -------------------------------------------------------------------------------- /lib/utils/network.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | /* 15 | * helper functions related to interacting with chainpoint network objects 16 | * such as nodes and cores 17 | */ 18 | 19 | import { resolveTxt } from 'dns' 20 | import { parse } from 'url' 21 | import { promisify } from 'util' 22 | import { isInteger, isFunction, isEmpty, slice, map, shuffle, filter, first, isString } from 'lodash' 23 | import { isURL, isIP } from 'validator' 24 | const { AbortController, abortableFetch } = require('abortcontroller-polyfill/dist/cjs-ponyfill') 25 | const { fetch } = abortableFetch(require('node-fetch')) 26 | 27 | import getConfig from '../config' 28 | import { DNS_CORE_DISCOVERY_ADDR } from '../constants' 29 | import { testArrayArg } from './helpers' 30 | 31 | let config = getConfig() 32 | 33 | /** 34 | * Check if valid Core URI 35 | * 36 | * @param {string} coreURI - The Core URI to test for validity 37 | * @returns {bool} true if coreURI is a valid Core URI, otherwise false 38 | */ 39 | export function isValidCoreURI(coreURI) { 40 | if (isEmpty(coreURI) || !isString(coreURI)) return false 41 | 42 | try { 43 | return isURL(coreURI, { 44 | protocols: ['https'], 45 | require_protocol: true, 46 | host_whitelist: [/^[a-z]\.chainpoint\.org$/] 47 | }) 48 | } catch (error) { 49 | return false 50 | } 51 | } 52 | 53 | /** 54 | * Check if valid Node URI 55 | * 56 | * @param {string} nodeURI - The value to check 57 | * @returns {bool} true if value is a valid Node URI, otherwise false 58 | */ 59 | export function isValidNodeURI(nodeURI) { 60 | if (!isString(nodeURI)) return false 61 | 62 | try { 63 | let isValidURI = isURL(nodeURI, { 64 | protocols: ['http', 'https'], 65 | require_protocol: true, 66 | host_blacklist: ['0.0.0.0'] 67 | }) 68 | 69 | let parsedURI = parse(nodeURI).hostname 70 | 71 | // Valid URI w/ IPv4 address? 72 | return isValidURI && isIP(parsedURI, 4) 73 | } catch (error) { 74 | return false 75 | } 76 | } 77 | 78 | /** 79 | * Retrieve an Array of discovered Core URIs. Returns one Core URI by default. 80 | * 81 | * @param {Integer} num - Max number of Core URI's to return. 82 | * @param {function} callback - An optional callback function. 83 | * @returns {string} - Returns either a callback or a Promise with an Array of Core URI strings. 84 | */ 85 | export async function getCores(num) { 86 | num = num || 1 87 | 88 | if (!isInteger(num) || num < 1) throw new Error('num arg must be an Integer >= 1') 89 | 90 | if (resolveTxt && isFunction(resolveTxt)) { 91 | let resolveTxtAsync = promisify(resolveTxt) 92 | let coreDiscovery = config.str('core-discovery-addr', DNS_CORE_DISCOVERY_ADDR) 93 | let records = await resolveTxtAsync(coreDiscovery) 94 | 95 | if (isEmpty(records)) throw new Error('no core addresses available') 96 | 97 | let cores = map(records, coreIP => { 98 | return 'https://' + coreIP 99 | }) 100 | 101 | // randomize the order 102 | let shuffledCores = shuffle(cores) 103 | // only return cores with valid addresses (should be all) 104 | let filteredCores = filter(shuffledCores, function(c) { 105 | return isValidCoreURI(c) 106 | }) 107 | // only return num cores 108 | return slice(filteredCores, 0, num) 109 | } else { 110 | // `dns` module is not available in the browser 111 | // fallback to simple random selection of Cores 112 | let cores = config.array('cores', ['http://3.135.54.225']) 113 | return slice(shuffle(cores), 0, num) 114 | } 115 | } 116 | 117 | /** 118 | * Retrieve an Array of discovered Node URIs. Returns three Node URIs by default. 119 | * Can only return up to the number of Nodes that Core provides. 120 | * 121 | * @param {Integer} num - Max number of Node URIs to return. 122 | * @param {function} callback - An optional callback function. 123 | * @returns {Array} - Returns either a callback or a Promise with an Array of Node URI strings 124 | */ 125 | export async function getNodes(num) { 126 | num = num || 3 127 | 128 | if (!isInteger(num) || num < 1) throw new Error('num arg must be an Integer >= 1') 129 | 130 | // get cores uri from configs, if none, then check with getCores 131 | let coreURI = config.array('cores') 132 | if (!coreURI) coreURI = await getCores(1) 133 | let getNodeURI = first(coreURI) + '/nodes/random' 134 | let response = await fetch(getNodeURI) 135 | response = await response.json() 136 | 137 | let nodes = map(response, 'public_uri') 138 | // randomize the order 139 | let shuffledNodes = shuffle(nodes) 140 | // only return nodes with valid addresses (should be all) 141 | let filteredNodes = filter(shuffledNodes, function(n) { 142 | return isValidNodeURI(n) 143 | }) 144 | 145 | let failedNodes = [] 146 | 147 | // since not all nodes returned from a core are guaranteed to work 148 | // we need to test each one 149 | let testedNodes = await testNodeEndpoints(filteredNodes, failedNodes) 150 | 151 | // remove any that have failed and slice to requested number 152 | let slicedNodes = testedNodes.filter(node => node).slice(0, num) 153 | 154 | if (failedNodes.length === testedNodes.length) 155 | throw new Error(`Could not connect to any nodes provided by core ${first(coreURI)}.`) 156 | else if (failedNodes.length) 157 | console.error( 158 | `Could not connect to (${failedNodes.length}) of (${testedNodes.length}) nodes provided by core ${first(coreURI)}` 159 | ) 160 | 161 | // We should never return an empty array of nodes 162 | if (!slicedNodes.length) 163 | throw new Error('There seems to be an issue retrieving a list of available nodes. Please try again.') 164 | 165 | return slicedNodes 166 | } 167 | 168 | /** 169 | * Test an array of node uris to see if they are responding to requests. 170 | * Adds cross-platform support for a timeout to make the check faster. The browser's fetch 171 | * does not support a timeout paramater so need to add with AbortController 172 | * 173 | * @param {String[]} nodes - array of node URIs 174 | * @param {Array} failures - Need an external array to be passed to track failures 175 | * @returns {Promise} - returns a Promise.all that resolves to an array of urls. Any that fail return as undefined 176 | * and should be filtered out of the final result 177 | */ 178 | export function testNodeEndpoints(nodes, failures = [], timeout = 150) { 179 | testArrayArg(nodes) 180 | return Promise.all( 181 | nodes.map(async node => { 182 | try { 183 | isValidNodeURI(node) 184 | let controller, signal, timeoutId 185 | if (AbortController) { 186 | controller = new AbortController() 187 | signal = controller.signal 188 | timeoutId = setTimeout(() => controller.abort(), timeout) 189 | } 190 | await fetch(node, { timeout, method: 'GET', signal }) 191 | 192 | clearTimeout(timeoutId) 193 | return node 194 | } catch (e) { 195 | failures.push(node) 196 | } 197 | }) 198 | ) 199 | } 200 | -------------------------------------------------------------------------------- /lib/utils/proofs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import cpp from 'chainpoint-parse' 15 | import uuidv1 from 'uuid/v1' 16 | import { isJSON, isBase64 } from 'validator' 17 | import { isEmpty, isString, has, isObject, forEach, isBuffer } from 'lodash' 18 | import { isHex, testArrayArg } from './helpers' 19 | 20 | /** 21 | * Checks if a proof handle Object has valid params. 22 | * 23 | * @param {Object} handle - The proof handle to check 24 | * @returns {bool} true if handle is valid Object with expected params, otherwise false 25 | */ 26 | export function isValidProofHandle(handle) { 27 | if (!isEmpty(handle) && isObject(handle) && has(handle, 'uri') && has(handle, 'proofId')) return true 28 | return false 29 | } 30 | 31 | /** 32 | * Map the JSON API response from submitting a hash to a Node to a 33 | * more accessible form that can also be used as the input arg to 34 | * getProofs function. 35 | * 36 | * @param {Array} respArray - An Array of responses, one for each Node submitted to 37 | * @returns {Array<{uri: String, hash: String, hashIdNode: String}>} An Array of proofHandles 38 | */ 39 | export function mapSubmitHashesRespToProofHandles(respArray) { 40 | testArrayArg(respArray) 41 | 42 | let proofHandles = [] 43 | let groupIdList = [] 44 | if (respArray[0] && respArray[0].hashes) { 45 | forEach(respArray[0].hashes, () => { 46 | groupIdList.push(uuidv1()) 47 | }) 48 | } 49 | 50 | forEach(respArray, resp => { 51 | forEach(resp.hashes, (hash, idx) => { 52 | let handle = {} 53 | 54 | handle.uri = resp.meta.submitted_to 55 | handle.hash = hash.hash 56 | handle.proofId = hash.proof_id 57 | handle.groupId = groupIdList[idx] 58 | proofHandles.push(handle) 59 | }) 60 | }) 61 | 62 | return proofHandles 63 | } 64 | 65 | /** 66 | * Parse an Array of proofs, each of which can be in any supported format. 67 | * 68 | * @param {Array} proofs - An Array of proofs in any supported form 69 | * @returns {Array} An Array of parsed proofs 70 | */ 71 | export function parseProofs(proofs) { 72 | testArrayArg(proofs) 73 | let parsedProofs = [] 74 | 75 | forEach(proofs, proof => { 76 | if (isObject(proof)) { 77 | // OBJECT 78 | parsedProofs.push(cpp.parse(proof)) 79 | } else if (isJSON(proof)) { 80 | // JSON-LD 81 | parsedProofs.push(cpp.parse(JSON.parse(proof))) 82 | } else if (isBase64(proof) || isBuffer(proof) || isHex(proof)) { 83 | // BINARY 84 | parsedProofs.push(cpp.parse(proof)) 85 | } else { 86 | throw new Error('unknown proof format') 87 | } 88 | }) 89 | 90 | return parsedProofs 91 | } 92 | 93 | /** 94 | * validate and normalize proofs for actions such as parsing 95 | * @param {Array} proofs - An Array of String, or Object proofs from getProofs(), to be verified. Proofs can be in any of the supported JSON-LD or Binary formats. 96 | @return {Array} - An Array of Objects, one for each proof submitted. 97 | */ 98 | export function normalizeProofs(proofs) { 99 | // Validate proofs arg 100 | testArrayArg(proofs) 101 | 102 | // If any entry in the proofs Array is an Object, process 103 | // it assuming the same form as the output of getProofs(). 104 | let normalized = [] 105 | forEach(proofs, proof => { 106 | if (isObject(proof) && has(proof, 'proof') && isString(proof.proof)) { 107 | // Probably result of `submitProofs()` call. Extract proof String 108 | normalized.push(proof.proof) 109 | } else if (isObject(proof) && has(proof, 'type') && proof.type === 'Chainpoint') { 110 | // Probably a JS Object Proof 111 | normalized.push(proof) 112 | } else if (isString(proof) && (isJSON(proof) || isBase64(proof))) { 113 | // Probably a JSON String or Base64 encoded binary proof 114 | normalized.push(proof) 115 | } else if (isObject(proof) && !proof.proof && has(proof, 'proofId')) { 116 | console.error(`no proof for hashIdNode ${proof.proofId}`) 117 | } else { 118 | console.error('proofs arg Array has elements that are not Objects or Strings') 119 | } 120 | }) 121 | 122 | return normalized 123 | } 124 | 125 | /** 126 | * Flatten an Array of proof branches where each proof anchor in 127 | * each branch is represented as an Object with all relevant data for that anchor. 128 | * 129 | * @param {Array} proofBranchArray - An Array of branches for a given level in a proof 130 | * @returns {Array} An Array of flattened proof anchor objects for each branch 131 | */ 132 | export function flattenProofBranches(proofBranchArray) { 133 | testArrayArg(proofBranchArray) 134 | let flatProofAnchors = [] 135 | 136 | forEach(proofBranchArray, proofBranch => { 137 | let anchors = proofBranch.anchors 138 | forEach(anchors, anchor => { 139 | let flatAnchor = {} 140 | flatAnchor.branch = proofBranch.label || undefined 141 | flatAnchor.uri = anchor.uris[0] 142 | flatAnchor.type = anchor.type 143 | flatAnchor.anchor_id = anchor.anchor_id 144 | flatAnchor.expected_value = anchor.expected_value 145 | flatProofAnchors.push(flatAnchor) 146 | }) 147 | if (proofBranch.branches) { 148 | flatProofAnchors = flatProofAnchors.concat(flattenProofBranches(proofBranch.branches)) 149 | } 150 | }) 151 | return flatProofAnchors 152 | } 153 | 154 | /** 155 | * Flatten an Array of parsed proofs where each proof anchor is 156 | * represented as an Object with all relevant proof data. 157 | * 158 | * @param {Array} parsedProofs - An Array of previously parsed proofs 159 | * @returns {Array} An Array of flattened proof objects 160 | */ 161 | export function flattenProofs(parsedProofs) { 162 | testArrayArg(parsedProofs) 163 | 164 | let flatProofAnchors = [] 165 | 166 | forEach(parsedProofs, parsedProof => { 167 | let proofAnchors = flattenProofBranches(parsedProof.branches) 168 | forEach(proofAnchors, proofAnchor => { 169 | let flatProofAnchor = {} 170 | flatProofAnchor.hash = parsedProof.hash 171 | flatProofAnchor.proof_id = parsedProof.proof_id 172 | flatProofAnchor.hash_received = parsedProof.hash_received 173 | flatProofAnchor.branch = proofAnchor.branch 174 | flatProofAnchor.uri = proofAnchor.uri 175 | flatProofAnchor.type = proofAnchor.type 176 | flatProofAnchor.anchor_id = proofAnchor.anchor_id 177 | flatProofAnchor.expected_value = proofAnchor.expected_value 178 | flatProofAnchors.push(flatProofAnchor) 179 | }) 180 | }) 181 | 182 | return flatProofAnchors 183 | } 184 | 185 | /** 186 | * Get raw btc transactions for each hash_id_node 187 | * @param {Array} proofs - array of previously parsed proofs 188 | * @return {Obect[]} - an array of objects with hash_id_node and raw btc tx 189 | */ 190 | export function flattenBtcBranches(proofBranchArray) { 191 | testArrayArg(proofBranchArray) 192 | let flattenedBranches = [] 193 | 194 | forEach(proofBranchArray, proofBranch => { 195 | let btcAnchor = {} 196 | btcAnchor.proof_id = proofBranch.proof_id 197 | 198 | if (proofBranch.branches) { 199 | forEach(proofBranch.branches, branch => { 200 | // sub branches indicate other anchors 201 | // we want to find the sub-branch that anchors to btc 202 | if (branch.branches) { 203 | // get the raw tx from the btc_anchor_branch 204 | let btcBranch = branch.branches.find(element => element.label === 'btc_anchor_branch') 205 | btcAnchor.raw_btc_tx = btcBranch.rawTx 206 | // get the btc anchor 207 | let anchor = btcBranch.anchors.find(anchor => anchor.type === 'btc' || anchor.type === 'tbtc') 208 | // add expected_value (i.e. the merkle root of anchored block) 209 | btcAnchor.expected_value = anchor.expected_value 210 | // add anchor_id (i.e. the anchored block height) 211 | btcAnchor.anchor_id = anchor.anchor_id 212 | } 213 | }) 214 | } 215 | 216 | flattenedBranches.push(btcAnchor) 217 | }) 218 | 219 | return flattenedBranches 220 | } 221 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import url from 'url' 15 | import { isEmpty, isString, forEach, map, first, uniqWith, isEqual, uniq, flatten, mapKeys, camelCase } from 'lodash' 16 | 17 | import { isValidNodeURI, getNodes } from './utils/network' 18 | import { fetchEndpoints, isSecureOrigin } from './utils/helpers' 19 | // need to import evaluate this way so that tests can stub it and confirm 20 | // it was called 21 | import * as evaluate from './evaluate' 22 | import { NODE_PROXY_URI } from './constants' 23 | import getConfig from './config' 24 | 25 | let config = getConfig() 26 | 27 | /** 28 | * Verify a collection of proofs using an optionally provided Node URI 29 | * 30 | * @param {Array} proofs - An Array of String, or Object proofs from getProofs(), to be verified. Proofs can be in any of the supported JSON-LD or Binary formats. 31 | * @param {String} uri - [Optional] The Node URI to submit proof(s) to for verification. If not provided a Node will be selected at random. All proofs will be verified by a single Node. 32 | * @param {function} callback - An optional callback function. 33 | * @return {Array} - An Array of Objects, one for each proof submitted, with vefification info. 34 | */ 35 | export default async function verifyProofs(proofs, uri) { 36 | let evaluatedProofs = evaluate.evaluateProofs(proofs) 37 | 38 | // Validate and return an Array with a single Node URI 39 | // if provided or get an Array of Nodes via service discovery. 40 | let nodes 41 | if (isEmpty(uri)) { 42 | nodes = await getNodes(1) 43 | } else { 44 | if (!isString(uri)) throw new Error('uri arg must be a String') 45 | if (!isValidNodeURI(uri)) throw new Error(`uri arg contains invalid Node URI : ${uri}`) 46 | nodes = [uri] 47 | } 48 | 49 | let node = first(nodes) 50 | 51 | // Assign all flat proofs to the same Node URI for verification 52 | let singleNodeFlatProofs = map(evaluatedProofs, proof => { 53 | let oldProofURI = url.parse(proof.uri) 54 | proof.uri = node + oldProofURI.path 55 | return proof 56 | }) 57 | 58 | let flatProofs = uniqWith(singleNodeFlatProofs, isEqual) 59 | let anchorURIs = [] 60 | forEach(flatProofs, proof => { 61 | anchorURIs.push(proof.uri) 62 | }) 63 | 64 | let uniqAnchorURIs = uniq(anchorURIs) 65 | 66 | let nodesWithGetOpts = map(uniqAnchorURIs, anchorURI => { 67 | let headers = Object.assign( 68 | { 69 | 'Content-Type': 'application/json', 70 | Accept: 'application/json' 71 | }, 72 | isSecureOrigin() 73 | ? { 74 | 'X-Node-Uri': url.parse(anchorURI).protocol + '//' + url.parse(anchorURI).host 75 | } 76 | : {} 77 | ) 78 | 79 | let uri = isSecureOrigin() ? config.str('node-proxy-uri', NODE_PROXY_URI) + url.parse(anchorURI).path : anchorURI 80 | 81 | return { 82 | method: 'GET', 83 | uri: uri, 84 | body: {}, 85 | headers, 86 | timeout: 10000 87 | } 88 | }) 89 | 90 | let parsedBody = await fetchEndpoints(nodesWithGetOpts) 91 | 92 | // fetchEndpoints returns an Array entry for each host it submits to. 93 | let flatParsedBody = flatten(parsedBody) 94 | 95 | let hashesFound = {} 96 | 97 | forEach(nodesWithGetOpts, (getOpt, index) => { 98 | // only add blockHeight to hashesFound map if a hash was returned 99 | // this avoids adding a uri with an empty value to the hashes found map 100 | if (flatParsedBody[index].length) { 101 | let uriSegments = getOpt.uri.split('/') 102 | let blockHeight = uriSegments[uriSegments.length - 2] 103 | hashesFound[blockHeight] = flatParsedBody[index] 104 | } 105 | }) 106 | 107 | if (isEmpty(hashesFound)) throw new Error('No hashes were found.') 108 | let results = [] 109 | 110 | forEach(flatProofs, flatProof => { 111 | let uriSegments = flatProof.uri.split('/') 112 | let blockHeight = uriSegments[uriSegments.length - 2] 113 | if (flatProof.expected_value === hashesFound[blockHeight]) { 114 | // IT'S GOOD! 115 | flatProof.verified = true 116 | flatProof.verified_at = new Date().toISOString().slice(0, 19) + 'Z' 117 | } else { 118 | // IT'S NO GOOD :-( 119 | flatProof.verified = false 120 | flatProof.verified_at = null 121 | } 122 | 123 | // Camel case object keys 124 | let flatProofCamel = mapKeys(flatProof, (v, k) => camelCase(k)) 125 | 126 | results.push(flatProofCamel) 127 | }) 128 | return results 129 | } 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainpoint-client", 3 | "version": "0.4.3", 4 | "description": "Chainpoint API client", 5 | "main": "dist/bundle.js", 6 | "author": "Jason Bukowski ", 7 | "license": "Apache-2.0", 8 | "repository": "https://github.com/chainpoint/chainpoint-client-js", 9 | "scripts": { 10 | "coverage": "nyc report --reporter=text-lcov | coveralls", 11 | "eslint-check": "eslint --print-config . | eslint-config-prettier-check", 12 | "lint": "eslint lib/**/*.js *.js", 13 | "test": "CHAINPOINT_CORES=http://35.245.211.97 npm run test:lint && nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'", 14 | "test:lint": "npm run lint", 15 | "test:unit": "CHAINPOINT_CORES=http://35.245.211.97 nyc mocha --reporter spec -r esm 'tests/!(e2e)*-test.js'", 16 | "test:watch": "CHAINPOINT_CORES=http://35.245.211.97 nyc mocha --reporter spec -r esm --watch 'tests/!(e2e)*-test.js'", 17 | "test:e2e": "npm run webpack && mocha --reporter spec -r esm tests/e2e-test.js", 18 | "build": "webpack", 19 | "prepare": "npm run build", 20 | "prepublishOnly": "npm run build" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "lint-staged": { 28 | "linters": { 29 | "*.js": [ 30 | "eslint --fix", 31 | "git add" 32 | ], 33 | "*.{json,css,md}": [ 34 | "prettier --write", 35 | "git add" 36 | ] 37 | } 38 | }, 39 | "keywords": [], 40 | "devDependencies": { 41 | "@babel/core": "^7.2.2", 42 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 43 | "@babel/plugin-transform-regenerator": "^7.0.0", 44 | "@babel/preset-env": "^7.2.3", 45 | "babel-loader": "^8.0.5", 46 | "babel-preset-minify": "^0.5.0", 47 | "bfile": "^0.2.1", 48 | "chai": "^4.2.0", 49 | "chainpoint-binary": "^5.0.0", 50 | "coveralls": "^3.0.3", 51 | "eslint": "^5.15.3", 52 | "eslint-config-prettier": "^4.1.0", 53 | "eslint-plugin-prettier": "^3.0.1", 54 | "esm": "^3.2.22", 55 | "husky": "^1.3.1", 56 | "lint-staged": "^8.1.5", 57 | "mocha": "^6.1.3", 58 | "nock": "^10.0.6", 59 | "node-libs-browser": "^2.1.0", 60 | "npm": "^6.5.0", 61 | "nyc": "^14.0.0", 62 | "prettier": "^1.17.0", 63 | "sinon": "^7.3.2", 64 | "terser-webpack-plugin": "^1.2.1", 65 | "webpack": "^4.28.4", 66 | "webpack-cli": "^3.2.1", 67 | "webpack-node-externals": "^1.7.2" 68 | }, 69 | "dependencies": { 70 | "@babel/polyfill": "^7.4.3", 71 | "@ungap/url-search-params": "^0.1.2", 72 | "abortcontroller-polyfill": "^1.3.0", 73 | "bcfg": "^0.1.6", 74 | "chainpoint-parse": "^4.0.0", 75 | "lodash": "^4.17.11", 76 | "node-fetch": "^2.3.0", 77 | "uuid": "^3.3.2", 78 | "uuid-validate": "^0.0.2", 79 | "validator": "^9.1.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/config-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import getConfig from '../lib/config' 14 | import { expect } from 'chai' 15 | import Config from 'bcfg' 16 | import fs from 'bfile' 17 | 18 | describe('config', () => { 19 | let foo, prefix, config 20 | beforeEach(async () => { 21 | foo = 'bar' 22 | prefix = '/tmp/chainpoint_tests' 23 | fs.mkdirSync(prefix) 24 | }) 25 | 26 | afterEach(async () => { 27 | await fs.remove(prefix) 28 | config = null 29 | }) 30 | 31 | it('should return a bcfg object', () => { 32 | config = getConfig() 33 | expect(config).to.be.an.instanceof(Config) 34 | }) 35 | 36 | it('should load env vars', () => { 37 | process.env.CHAINPOINT_FOO = foo 38 | config = getConfig() 39 | expect(config.str('foo')).to.equal(foo) 40 | delete process.env.CHAINPOINT_FOO 41 | }) 42 | 43 | it('should load options that are passed to it', () => { 44 | config = getConfig({ foo }) 45 | expect(config.str('foo')).to.equal(foo) 46 | }) 47 | 48 | it('should load argv', () => { 49 | process.argv.push(`--foo=${foo}`) 50 | config = getConfig() 51 | expect(config.str('foo')).to.equal(foo) 52 | // remove variable from argv 53 | process.argv.pop() 54 | }) 55 | 56 | it('should load configs from a config file', async () => { 57 | fs.writeFileSync(prefix + '/chainpoint.conf', 'foo: bar') 58 | config = getConfig({ prefix }) 59 | expect(config.str('foo')).to.equal(foo) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/data/hashes.json: -------------------------------------------------------------------------------- 1 | [ 2 | "ec30c8df7408e5e55927f6478f88c0ac98c90ad121c2dca4197dd4c3a2324213", 3 | "0b8758aa86911eb678ed215c2814648f08f79b3da446173349c35554bcdd52a5", 4 | "6c96e446a6d6b9f0eaffc1464be86b3cdc12ebbb1ce6221a03a8d897e654946a" 5 | ] 6 | -------------------------------------------------------------------------------- /tests/data/nodes.json: -------------------------------------------------------------------------------- 1 | ["http://35.237.15.174", "http://35.196.109.49"] 2 | -------------------------------------------------------------------------------- /tests/data/proof-handles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "http://35.237.15.174", 4 | "hash": "1d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 5 | "hashIdNode": "21156440-6d1d-11e9-a653-01ef1ab22ee7", 6 | "groupId": "20981df0-6d1d-11e9-8912-4db2bf794884" 7 | }, 8 | { 9 | "uri": "http://35.237.15.174", 10 | "hash": "2d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 11 | "hashIdNode": "21158b50-6d1d-11e9-a653-012091f557f7", 12 | "groupId": "20981df1-6d1d-11e9-8912-4db2bf794884" 13 | }, 14 | { 15 | "uri": "http://35.237.15.174", 16 | "hash": "3d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 17 | "hashIdNode": "2115d970-6d1d-11e9-a653-01595ebb1d42", 18 | "groupId": "20981df2-6d1d-11e9-8912-4db2bf794884" 19 | }, 20 | { 21 | "uri": "http://35.196.109.49", 22 | "hash": "1d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 23 | "hashIdNode": "21153d30-6d1d-11e9-a5a8-014c527fe564", 24 | "groupId": "20981df0-6d1d-11e9-8912-4db2bf794884" 25 | }, 26 | { 27 | "uri": "http://35.196.109.49", 28 | "hash": "2d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 29 | "hashIdNode": "21153d31-6d1d-11e9-a5a8-01ab1e3c2bea", 30 | "groupId": "20981df1-6d1d-11e9-8912-4db2bf794884" 31 | }, 32 | { 33 | "uri": "http://35.196.109.49", 34 | "hash": "3d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a", 35 | "hashIdNode": "21156440-6d1d-11e9-a5a8-0101f7a3589e", 36 | "groupId": "20981df2-6d1d-11e9-8912-4db2bf794884" 37 | }, 38 | { 39 | "uri": "http://35.237.15.174", 40 | "hash": "6806d68d9db70188d2eebbc305614332793a57e2cb6b4cabb6e3578c1d551c70", 41 | "hashIdNode": "21287710-6d1d-11e9-a653-01e94c159e8c", 42 | "groupId": "20aba5f0-6d1d-11e9-8912-4db2bf794884", 43 | "path": "./.editorconfig" 44 | }, 45 | { 46 | "uri": "http://35.237.15.174", 47 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963", 48 | "hashIdNode": "21287711-6d1d-11e9-a653-0117e0470c36", 49 | "groupId": "20aba5f1-6d1d-11e9-8912-4db2bf794884", 50 | "path": "./.eslintignore" 51 | }, 52 | { 53 | "uri": "http://35.237.15.174", 54 | "hash": "5935e9777e5080814f58f8412acac7d548706566f6bef0f7024a8af898d88218", 55 | "hashIdNode": "21287712-6d1d-11e9-a653-01121760b027", 56 | "groupId": "20aba5f2-6d1d-11e9-8912-4db2bf794884", 57 | "path": "./.eslintrc.json" 58 | }, 59 | { 60 | "uri": "http://35.237.15.174", 61 | "hash": "4ce65cb54a7266455b44dc2fa46dd135925b2755a408e19d207dbde6a67995a2", 62 | "hashIdNode": "21287713-6d1d-11e9-a653-01ba817b7c6d", 63 | "groupId": "20aba5f3-6d1d-11e9-8912-4db2bf794884", 64 | "path": "./.gitignore" 65 | }, 66 | { 67 | "uri": "http://35.237.15.174", 68 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963", 69 | "hashIdNode": "21287714-6d1d-11e9-a653-0117e0470c36", 70 | "groupId": "20aba5f4-6d1d-11e9-8912-4db2bf794884", 71 | "path": "./.eslintignore" 72 | }, 73 | { 74 | "uri": "http://35.237.15.174", 75 | "hash": "8570b600fa3d9658a4c133e1d3897c099676e938a7775be06f699f807bcd0243", 76 | "hashIdNode": "21287715-6d1d-11e9-a653-01a98661d85f", 77 | "groupId": "20aba5f5-6d1d-11e9-8912-4db2bf794884", 78 | "path": "./.prettierrc" 79 | }, 80 | { 81 | "uri": "http://35.237.15.174", 82 | "hash": "0ffd76bdd51d7ee1c151360e1f2d411ba46640518d518da856b9b9193ec2dd83", 83 | "hashIdNode": "21287716-6d1d-11e9-a653-016e742c718a", 84 | "groupId": "20aba5f6-6d1d-11e9-8912-4db2bf794884", 85 | "path": "./.travis.yml" 86 | }, 87 | { 88 | "uri": "http://35.237.15.174", 89 | "hash": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1", 90 | "hashIdNode": "21287717-6d1d-11e9-a653-0160de72b4a0", 91 | "groupId": "20aba5f7-6d1d-11e9-8912-4db2bf794884", 92 | "path": "./LICENSE" 93 | }, 94 | { 95 | "uri": "http://35.237.15.174", 96 | "hash": "f3ebb270573fe31fcf1c700f3a4f7fc49891f7269bccac7b6d53e4df3944c05a", 97 | "hashIdNode": "21287718-6d1d-11e9-a653-01cf0c91e2f7", 98 | "groupId": "20aba5f8-6d1d-11e9-8912-4db2bf794884", 99 | "path": "./README.md" 100 | }, 101 | { 102 | "uri": "http://35.237.15.174", 103 | "hash": "eb9481b75a528764fc0fa9a0f266038bf5ff426967333f8b873513d4d2b29f4b", 104 | "hashIdNode": "21287719-6d1d-11e9-a653-016e1c5cab92", 105 | "groupId": "20aba5f9-6d1d-11e9-8912-4db2bf794884", 106 | "path": "./index.html" 107 | }, 108 | { 109 | "uri": "http://35.237.15.174", 110 | "hash": "6dbe14736b3cbeb2f94b9443baacbf06bdae1ddb67029548daa465b7563cc513", 111 | "hashIdNode": "2128771a-6d1d-11e9-a653-01bab60e6173", 112 | "groupId": "20aba5fa-6d1d-11e9-8912-4db2bf794884", 113 | "path": "./index.js" 114 | }, 115 | { 116 | "uri": "http://35.237.15.174", 117 | "hash": "05cb14758c40134663a895d06c932b8caca35ea81702e184c04c94af18557eb1", 118 | "hashIdNode": "2128771b-6d1d-11e9-a653-01f9221b3cd1", 119 | "groupId": "20aba5fb-6d1d-11e9-8912-4db2bf794884", 120 | "path": "./package.json" 121 | }, 122 | { 123 | "uri": "http://35.237.15.174", 124 | "hash": "c0ea98563f7b35161fa8555a64b05f0d0d633bcafe6dc6947ddc3a3a76132c69", 125 | "hashIdNode": "2128771c-6d1d-11e9-a653-019a9cbc038a", 126 | "groupId": "20aba5fc-6d1d-11e9-8912-4db2bf794884", 127 | "path": "./webpack.config.js" 128 | }, 129 | { 130 | "uri": "http://35.237.15.174", 131 | "hash": "0ec2cf9141b51ee9b1fb7df9b31394da804d6a98daa8f234775ea03216800895", 132 | "hashIdNode": "2128771d-6d1d-11e9-a653-015e9e9fda78", 133 | "groupId": "20aba5fd-6d1d-11e9-8912-4db2bf794884", 134 | "path": "./yarn-error.log" 135 | }, 136 | { 137 | "uri": "http://35.237.15.174", 138 | "hash": "4d37338465e1ca44b9edd3fca1b8b33007a59d9ff149ae730d528109bc570009", 139 | "hashIdNode": "2128771e-6d1d-11e9-a653-010d18dca7c1", 140 | "groupId": "20aba5fe-6d1d-11e9-8912-4db2bf794884", 141 | "path": "./yarn.lock" 142 | }, 143 | { 144 | "uri": "http://35.196.109.49", 145 | "hash": "6806d68d9db70188d2eebbc305614332793a57e2cb6b4cabb6e3578c1d551c70", 146 | "hashIdNode": "21296170-6d1d-11e9-a5a8-015e0f0e5b51", 147 | "groupId": "20aba5f0-6d1d-11e9-8912-4db2bf794884", 148 | "path": "./.editorconfig" 149 | }, 150 | { 151 | "uri": "http://35.196.109.49", 152 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963", 153 | "hashIdNode": "21296171-6d1d-11e9-a5a8-01b1b6a63748", 154 | "groupId": "20aba5f1-6d1d-11e9-8912-4db2bf794884", 155 | "path": "./.eslintignore" 156 | }, 157 | { 158 | "uri": "http://35.196.109.49", 159 | "hash": "5935e9777e5080814f58f8412acac7d548706566f6bef0f7024a8af898d88218", 160 | "hashIdNode": "21296172-6d1d-11e9-a5a8-010e5a5f3dca", 161 | "groupId": "20aba5f2-6d1d-11e9-8912-4db2bf794884", 162 | "path": "./.eslintrc.json" 163 | }, 164 | { 165 | "uri": "http://35.196.109.49", 166 | "hash": "4ce65cb54a7266455b44dc2fa46dd135925b2755a408e19d207dbde6a67995a2", 167 | "hashIdNode": "21296173-6d1d-11e9-a5a8-0195708d7979", 168 | "groupId": "20aba5f3-6d1d-11e9-8912-4db2bf794884", 169 | "path": "./.gitignore" 170 | }, 171 | { 172 | "uri": "http://35.196.109.49", 173 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963", 174 | "hashIdNode": "21296174-6d1d-11e9-a5a8-01b1b6a63748", 175 | "groupId": "20aba5f4-6d1d-11e9-8912-4db2bf794884", 176 | "path": "./.eslintignore" 177 | }, 178 | { 179 | "uri": "http://35.196.109.49", 180 | "hash": "8570b600fa3d9658a4c133e1d3897c099676e938a7775be06f699f807bcd0243", 181 | "hashIdNode": "21296175-6d1d-11e9-a5a8-0181224ebb54", 182 | "groupId": "20aba5f5-6d1d-11e9-8912-4db2bf794884", 183 | "path": "./.prettierrc" 184 | }, 185 | { 186 | "uri": "http://35.196.109.49", 187 | "hash": "0ffd76bdd51d7ee1c151360e1f2d411ba46640518d518da856b9b9193ec2dd83", 188 | "hashIdNode": "21298880-6d1d-11e9-a5a8-013396876a61", 189 | "groupId": "20aba5f6-6d1d-11e9-8912-4db2bf794884", 190 | "path": "./.travis.yml" 191 | }, 192 | { 193 | "uri": "http://35.196.109.49", 194 | "hash": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1", 195 | "hashIdNode": "2129fdb0-6d1d-11e9-a5a8-013efcb8790c", 196 | "groupId": "20aba5f7-6d1d-11e9-8912-4db2bf794884", 197 | "path": "./LICENSE" 198 | }, 199 | { 200 | "uri": "http://35.196.109.49", 201 | "hash": "f3ebb270573fe31fcf1c700f3a4f7fc49891f7269bccac7b6d53e4df3944c05a", 202 | "hashIdNode": "2129fdb1-6d1d-11e9-a5a8-016a39edf372", 203 | "groupId": "20aba5f8-6d1d-11e9-8912-4db2bf794884", 204 | "path": "./README.md" 205 | }, 206 | { 207 | "uri": "http://35.196.109.49", 208 | "hash": "eb9481b75a528764fc0fa9a0f266038bf5ff426967333f8b873513d4d2b29f4b", 209 | "hashIdNode": "2129fdb2-6d1d-11e9-a5a8-01476fb7f540", 210 | "groupId": "20aba5f9-6d1d-11e9-8912-4db2bf794884", 211 | "path": "./index.html" 212 | }, 213 | { 214 | "uri": "http://35.196.109.49", 215 | "hash": "6dbe14736b3cbeb2f94b9443baacbf06bdae1ddb67029548daa465b7563cc513", 216 | "hashIdNode": "2129fdb3-6d1d-11e9-a5a8-013b760d9685", 217 | "groupId": "20aba5fa-6d1d-11e9-8912-4db2bf794884", 218 | "path": "./index.js" 219 | }, 220 | { 221 | "uri": "http://35.196.109.49", 222 | "hash": "05cb14758c40134663a895d06c932b8caca35ea81702e184c04c94af18557eb1", 223 | "hashIdNode": "2129fdb4-6d1d-11e9-a5a8-01b9e036446a", 224 | "groupId": "20aba5fb-6d1d-11e9-8912-4db2bf794884", 225 | "path": "./package.json" 226 | }, 227 | { 228 | "uri": "http://35.196.109.49", 229 | "hash": "c0ea98563f7b35161fa8555a64b05f0d0d633bcafe6dc6947ddc3a3a76132c69", 230 | "hashIdNode": "2129fdb5-6d1d-11e9-a5a8-0181066f34f6", 231 | "groupId": "20aba5fc-6d1d-11e9-8912-4db2bf794884", 232 | "path": "./webpack.config.js" 233 | }, 234 | { 235 | "uri": "http://35.196.109.49", 236 | "hash": "0ec2cf9141b51ee9b1fb7df9b31394da804d6a98daa8f234775ea03216800895", 237 | "hashIdNode": "2129fdb6-6d1d-11e9-a5a8-0137e848cdef", 238 | "groupId": "20aba5fd-6d1d-11e9-8912-4db2bf794884", 239 | "path": "./yarn-error.log" 240 | }, 241 | { 242 | "uri": "http://35.196.109.49", 243 | "hash": "4d37338465e1ca44b9edd3fca1b8b33007a59d9ff149ae730d528109bc570009", 244 | "hashIdNode": "2129fdb7-6d1d-11e9-a5a8-018e6ea7dd2a", 245 | "groupId": "20aba5fe-6d1d-11e9-8912-4db2bf794884", 246 | "path": "./yarn.lock" 247 | } 248 | ] 249 | -------------------------------------------------------------------------------- /tests/data/proofs-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "hash_id_node": "21156440-6d1d-11e9-a653-01ef1ab22ee7", 5 | "proof": "eJyllE2OJEUMhbkCd2BJddnx4wjXqiWuwGo2JUfYQaXUVJUqcwZYNmzYNjcYptEMiA0SYsk9SuIwuLrRiJlmMRKrSGWmv7DfexHfv7nuh/1iXy9/7pblOG/W66/ipFeH0xfrvpNpfzxM+2X9It4v3xztl8/evrrfybw7X6MGYePQMmFKYFVDkYBmiphGAawIWRtAoCCjaIFafDFhrtRAfr1gtpNu9we18ycBMZNzVqSoK0TjlVCOK0AbKC0Es/LHQ8n8vH05LYs9Vm5l+T0A8gryCsLnATaZN1CfvcX3w8nxcVA0CP/GNyUvwV6LQKNm8j7+Uvmf+MzP3rST7PvO5rtvf7qRZje/dbnZXl4dTtvHb68Ox/mvjz6+fXlz/vSh00k3HzLl7Y+H4+t5J6uQ6fbl6XxdBUYeWlxYxZiAyWq2UAyElAQq69BobKRskC4a08ikMAqWkdoTIEsJ2oalEbH1AuRIIlaBiDISsYlKA8xFU+y5UwHNJSHF0Eg6PQH2xCys2VWuCd3i1FJFKyU03yJCqVUyK5TEHXKrpFbNbMTuCao5PAHmqLkZNuHWcxZMGhg7seeM0MsZoboIuQ4OAqWMZpa0cVHF3gY/AUJX8TGTlhgUq43WuQsW7JGH57URig3Kru1g9d67gVYVrUIBA7wLdEcfwuGOfkiw3i/+YT/Ny4uwwZypRqAKm5S88RSBs8XEOQxAgT6cFHtyM7NPzcEdqH2ggxuG5jaStpz9QcXH5hgzuxCWsLUANSlX1ES1+pQjYXVvGweNTUhcshxMgksqQumdFl8/5ni+++7h7L/yaP/8T7Qn9fi0YB09LYoOLK6Wn+kmQxFUYx2e0ezSjeR3gBs0muYeSguRgah1uX9+mua7s16uHb91Yr7y/65CrL7S2jezvcpp/X+3Wass8jfwGZIG", 6 | "anchors_complete": ["cal"] 7 | }, 8 | { 9 | "hash_id_node": "21158b50-6d1d-11e9-a653-012091f557f7", 10 | "proof": "eJylVEGOXDUQ5QrcgWV62mW7bFevRuIKrLJpVbnK9JeG7lb/nwDLwIbtcAPIoATEBgmx5B4tcRiqZ1BEZlhEYuUvf79XVe89+7u31/2wX+yr5c/dshznzXr9ZZr06nD6fN13PO2Ph2m/rF+mu+Xro/3y6butux3Pu/N11MhkFAUL5BysaawcwUwB8qgBGgRUCSGWyKNqDa36YkzUigT+9UKznXS7P6idP4kA2ATDqijoCsBoxQXTKkAMBAPRwX/cQ+YX8sW0LPaA3PLyewxAq4CrED+LYYO0Ce35O/p+ODl9GiVZiP+mFy0Ogd4qByli/Jj+gvxPeqTnb+XE+76z+fabn25Y7Oa3zjfby9bhtH349/pwnP/66ONXP9ycn913OunmQ6Z89ePh+Gbe8SpiuYCvY25dWaq2WodpHqEPpCKxc2XC2vNI1qxCB0i1WWnZaZJB8mOY+H3C0/mauEaVYY4D6TWUQKUUUg4JeORCxl4vAFbNqWMvNSjWDCVFKdzLE8KeiZgUXeWWwS3OkhtYrVG8RAq1NUbSUDP1gNKKer9mI3VPUMP4hBCTohgIk3REhqyRoBfynBVwOEFo1hDboMjBZRGzrEJVFboMekIYXEEfM2tNUaHZkE6dwTVLNDyvUoBtFJfVBqn33i1oU9bGJbo5j015dh8Od/RDgvUY/P1+mpeXcQOIpaVQWtjk7I3nFAgtZcI4ArDb7EzJ7cWCPjVFd6D1AU4sEMVtLCqI/qHsY1NKSC6EZRCJoWWlBppLaz7lyNDcW6GoSbiwS4bROLqkzCW/1+KbhxzPt9/e3/3XHu2f/4n2pB4fidbB06LghB5L8DstPBSCamoDUkaXbmR/A9ygIYo9VomJQinS+e7FaZpvz3p5dvzVSXjl565iar6WtRezvfJp/X/LrJUX/hsozJCp", 11 | "anchors_complete": ["cal"] 12 | }, 13 | { 14 | "hash_id_node": "2115d970-6d1d-11e9-a653-01595ebb1d42", 15 | "proof": "eJylVE2OW0UQ5grcgWU8rurfKq8scQVW2VjVXdXYkrEt+yWQ5YQN2+EGIYMSEBsklGXuYSmHoTyDIjLDIhKrfurX9VV9P90/vV32/W6yH6b362k6nBbz+fdxo1f747fzvpbN7rDf7Kb583g7vTjY719/3Lpdy2l9XkYNwsah5YIpgZGGKgHNFDGNCkgIWRtAKEFG1QpUfTFhptJA/rjArDa62u3Vzl8FxKxcYVYUdYZoPJOS4wwwc7bWUFN4d1dyeta+20yT3VeuZPorAPIM8gzCNwEWmRdATz/C9/3R4eMo0SD8G75p8RLsVAVaaSYP4S+V/wmf+enbdpRdX9vp5uWvW2m2/bPLdnXZ2h9X9/9e7w+nD198ef1qe35yN+lGF5/D8vqX/eHNaS2zkMv1q+N52ThoSxEtxFL8tOvaQIu2UmRYpxSsG3YIZNprNGXnShKtYsTS86eA2/OSYu1oblmCFI0zUuWcAlUCGVKBCwoHG9wopdg6aPSWddBorCz0aMKemIU1u8qU0C1OLRFaraGNiBEqkWRWqIm7I1FRIzMbsXuCKD+mnKPmZtiEW89ZMGlg7IU9ZwW9nBHIKGcaHARqHc0saeOqir0NfgQIXaUAJ60xKJKN1rkLVuyRh+e1OWUbJXNx2uqzdwMlFSUpAQM81PDJXTjc0c8J1sPin3eb0/Q8LDDnQhEKwSIlHzxF8BTExDkMQIE+HCn2NHJxjwoHLJH6cLukYWgs1UOQs3+oOG2OMbMLYQlbC0BJmTxQhchZjoQkI12yFJsUcclyMAkuqUhJn4z45j7Hp5sf7+7+a4/2b/9Ee6PnJTcPHKqAogNWV8vvdJOhCKqRBsaUXbqR/A1wg0bT3ENtITKU0rrcPjtuTjdnvTw7/urEfOXnrkIkX8vcm9lO5Tj/v23mKpP8DX1rkJ8=", 16 | "anchors_complete": ["cal"] 17 | }, 18 | { 19 | "hash_id_node": "21287710-6d1d-11e9-a653-01e94c159e8c", 20 | "proof": "eJyllEGOWzcMhnuF3KHLeCxKokR6ZaBX6CobgxSp+gFT27Bf0mSZdNPt9AZtpkhadFOg6LL3MNDDVJ4pgmamiwBdPUF6/yfyJ8Xv3q/bfjf7y/nP7TwfTqvl8ps02dX++NWybWXaHfbTbl6+SLfzq4P/8sWHrdutnLbndaFQrJCxaQ1AZNFdtaWABXJKsXISrB6bFs1NVIsnrNTAEKHV8OsFs5lss9ubnz+PEKlWCItiYAsA54UUTIswVrkBslP7405yeq5fT/Ps98qNzL/HALwIuAjxyxhWyKtAzz7g2/448KmX5CH+G69WhgQaVQkjRpeH+IvyP/HIz97rUXZt66ebNz9di/r1b02uN5et/XFzf/Z2fzj99dmT1z9cn5/eRTrZ6lOyfP3j/vDutJVFxHIRr5M4M6Bb75RC76VrVgOwUjsG7lw9mymp9RYs1eIetTQsnqWmVh4BKdUGHpxyyMkZgSpjHpFRkC41cAHh6J2Vck56gQbU2qkrGwt9DDye1y0zCxsOlymDC2fNBF5r1J4ghUokyBZq5jZIVMzJ3Xtq7EwYHwExGaqDCmtDFMgWGVrhHLzAkDMEckKkzlFCrV19eKBczaBp50fA0ExK4Gw1RQPyro2bQIWWuEPuOlL2XpDLSNtG7M2DkYmRlFG08NDDp3fNMSr6KY31UPz9bjrNL+IKEMuo6HhJq5xH4DkFRk+ZMfYAElofpNRyxzJqVDhCSdT6KJcoRGWpxRRxLExG2pwS8jDCM6jGQNmYwHIhGln2DCQ9K0dLKkWGZRhd4rBUpOSPQnx338enm2/v3v7b0do//9Pak53XrNHHM5ZgMIB1uCXMKt0gmCXqkDIO63q+jIXOXQ1brBoTh1K0ye3z43S6Odtl7Iypk/Bq/HcVE41vWY7LfGdyXP7fa5Yms/wNSmSRzQ==", 21 | "anchors_complete": ["cal"] 22 | }, 23 | { 24 | "hash_id_node": "21287711-6d1d-11e9-a653-0117e0470c36", 25 | "proof": "eJyllE2Om0UQhrkCd2AZj7v6v7yyxBVYZWNV1w+2ZGzL/hLIcsKG7XCDkEEJiA0SyjL3sJTDUJ5BEZlhEYnV119319NV9b7dP71d8n436Q/T+/U0HU6L+fz7tJGr/fHbOa9pszvsN7tp/jzdTi8O+vvXH6du13Ran5eZ89ARiTqjNpTRpZTOrGoJO7QKgUPTWrN2uvwUizGzsXTLWNMfF8xqI6vdXvT8VYTYWwOYVQGZASjOqJY0CwBNQ26BU313F3J6Nr7bTJPeR65o+isGwFkosxC/iWFRcBH604943h8dn6wmDTH8Cz+keghwbxRGHUoP8ZfI/8QXfPp2HGnHaz3dvPx1S0O3fzJtV5ep/XF1v/Z6fzh9+OLL61fb85O7TDey+Jwqr3/ZH96c1jSLpV6/Op6XEAcOJMwRvKGdo5H2EjlJHUmhpdBzA0upGBOT1d5b55QbsyDn8ggYXIzcrbtcYt18PIY2PyI6JBaMIAlbj6Mx4QjDmlHQ6CkaROP8KXB7XrZ02cfV7QBQW+ZYmCQJVnLhYyAoqtxhCJoxE1ULKq14AZZzxUcZliRlKAzHcikEWSICV8xBK/QqCKF7D0o3jBRas6GaZWATAR72GBhYqAbM0lIU6GqDkQkacEKDbKMCqdWCVQ2lg7IG6ULi3nXRwsOSn9yZwxX9HGM9DP55tzlNz+MCSqk9hdrDImdPPKeARVPGEi0ABTYnJc5WavGqXZeaOhs4eFxMQa3KKMUHQl42ugPQG6EZxohuCvF7KNnt4FVahk6WB0ZJgyp5y0pUit5Sl+NTSd/c+/h08+Pd3X/t1v7tH2tv5LzEEZVBKAg4sHm3CHGQCQSR1A1SLt46ywG6C2RDCsc2YsJQ62C6fXbcnG7Ocnl2/NVJ5cr3XcXU/VvnfpjuhI7z/3vMXGiivwHARZK4", 26 | "anchors_complete": ["cal"] 27 | }, 28 | { 29 | "hash_id_node": "21287712-6d1d-11e9-a653-01121760b027", 30 | "proof": "eJylVE2Om0UQ5Qq5A8t4XFXd1T9eWeIKrLKxqrq68ScNtmV/CckyYcN2uAFkUAJig4RYcg9LHIbyDIrIDItIrL5Wf1WvX733ur97v2773dxfzn9u5/lwWi2X34TJrvbHr5ZtK9PusJ928/JFuJ1fHfovX3zYut3KaXtecw3ca865MxQoGAeXUSKSNGnZOJYMiVMaSfuAkYGiFBmlFiuFsPx6gdlMttntrZ8/J6SSM9IiGdoCsdeFJA4LQCTMCRQo/3HXcnquX0/z3O87NzL/ToB1AbwA+pJgxXUF5dkH+LY/OnwYKXQg+Be8WvIWbCULqJOUh/CXzv+E5/rsvR5l17b9dPPmp2vRfv1bk+vNZWt/3Nz/e7s/nP767MnrH67PT++YTrb6lClf/7g/vDttZUGcLs1rDSHnplqx9KCtcM8NE5GOXiBADKSl6UixJ24cOyQarNS55JGz8MeAx/MawMvcLC7NRhm+Vu1Zq9AIgbgSWqi5kOYmVUFHHgLdCfaBNFp8xDCHS11LSoKYcmzETSxYTYLQCAS591ZQrY7Rmkga0C2zJR0xpvqIIQdj7agO25gFo1HFlmqEnrAkqwilF/bEVRLIeWjv0bRmM3QpHgNCM0lQo+VA5joObbUJZmyhDs+uJpQ+EtfUR7WCvXWwYmJFkpsGD0d+ehcOd/RTgvWw+fvddJpf0AqZUwmQCqxidOIxQOUeYmUagAJtOFJofrMS+9TuSwqlDXRgRXK/cjJl9oWJj11D4OpC9IiqBCVaLWgxleJTjoh++6JWsqCSxCVj6kIuqdvxsaXv7nN8uvn27u6/9Wj//E+0Jzuvq2eroQkYOmB2taRWlWEIZqEMDJFduhEBixs01LhRVgoVUtImt8+P0+nmbJdnx1+dwFded0Wh+Dct/bC+Mzku/+8xS5NZ/gbJNJCr", 31 | "anchors_complete": ["cal"] 32 | }, 33 | { 34 | "hash_id_node": "21287713-6d1d-11e9-a653-01ba817b7c6d", 35 | "proof": "eJyllMGOI0UMhnkF3oHjZmJXlV3lnCLxCpz2EtnlKtJSSKKkd4HjwIXr8AbLDtoFcUFCe+Q9IvEw1MygFTvDYSVOLVW3P/f/+3f98HZdD/u5fTP/uZ3n43m1XH4dJ786nL5c1q1O++Nh2s/Ll/F2/vbYfv38/dHtVs/byzrVxlSNkubAnIgsJa+ha2J3jCSBLGQiTVAaigfIbt5YOYuQht/uMJvJN/uDt8tnAUPJGeOCHX2B2GShTHEBaFowW67s7+5Lzi/sq2me20PlRuc/AqAsgBYQvgiwIllBef4eXw+ngY+dY4MA/8Kb8yjBWrKCsTV9jL+r/E88yfO3dtJ93bbzzXc/79Ta7vequ83d0eG0eXj3+nA8//XJp9evdpdn9386+epjVF7/dDi+OW91EYivX50u6+EgYQ3VkpQE1iSBN8ohJ1GlhADFk2JJPMRwN8/cm7ggU2+I5UPg7rJGi0E6Q6aSI1QO3ItSzBmg12puKBA6dSuZQDo1IBhUVwg1GKQnwBxVbIAsKCLnVANV9ejCilADKFJrtaC59NFBlTs0z+RsPSWWJ5IpOlkbpojVESFMHgQrD+WNsfDQNkJViEqXoJBzt9aSm+QRvWr9KRCqK4MkzzE4ltatSlXMWKN0TN0YtXUm4dbFC7bawIurF+UxNHgs+dl9OMZEPyZYj4t/3E/n+WVYIRGXCFxgNXYHa4og1GISCh1QofZBijV1YhqqJSDHUjsOsGEw0cxuY/FEXYdsiWPrhhEtoVmAklwKeuJShsqesGhPJsGjKeuwjELTMCwd4/hwpG8ecny++f5+91+PaP/yT7Qnv6zFQqs40uA4gHm4pSKm3RHcY+kYEw3regIsY0AjkFRDthAFmK3q7YvTdL65+N21M26dSFfju6sQy3jycjRre9fT8v+2WbrO+jc/rJFq", 36 | "anchors_complete": ["cal"] 37 | }, 38 | { 39 | "hash_id_node": "21287714-6d1d-11e9-a653-0117e0470c36", 40 | "proof": "eJyllE+OXDUQxrlC7sAyPV1lu8p2r1riCqyyaZVdZfpJQ3er+yUky4QN2+EGkEEJiA0SypJ7tMRhqJ5BEcywiMTqvWe/+tWf77O/e7/u+91sL+c/tvN8OK2Wy2/ipFf741fLvpVpd9hPu3n5It7Orw72yxcfl263ctqe16mnZi2IlF4tV21FiUrvZiPWgpkROmRjTlbk8kEjhNRH1zJS5fjrBbOZdLPbq50/DxhKzpgWrKgLRKsLYYoLQMwGKUOP/OEu5PS8fT3Ns91HbmT+PQDWBdACwpcBVlRXUJ59xPf90fFxcDQI8A98U/YQ7CULNG4mD/GXyP/EU332vh1l17d2unnz07U0u/6ty/XmsrQ/bu733u4Ppz8/e/L6h+vz07tKJ119Spevf9wf3p22sgjEl+A1B8rDyCymCJZNMDYMFtiLTxQDjM4lhEA1NstEOtBnHJsSJwCoj4DYYqiDIVPJEToHHkUo5gyO6k0bVgiDRiuZoA4yIGiaVSD00CA9AuYotTnI7YDIOfVAXTRqZXHhAwh69b1g0zo8gwgPMM2k3EZK/KDC43lNUakZNsd2IsGkoWLnmsAYC2tFKFbcb6MGgZxHM0vaalbF3sZjIHQVhpo0x6BYbLReu2DGHuvANBqj2GCqbKNqQesGWlTUveuiwcOWn96ZwxX9FGM9DP5+N53mF2GFRFwicIFVSl64y1vJVa4UBqBAH06KPQ1i8q5rQI6lD3SwG6BVyayNyF9UvO0aI1UfhCVsLUBJ6udQE5fiXY6ERUZqNWhswuIjo2ASfKQux78lfXfv49PNt3dn/61b++e/rT3peV1bsI7uBkUHZp+W1NpkKIJqLANjIh/dSIDFBRpuxB5yC7ECc+ty+/w4nW7Oerl2/NaJdOX/XYVY/MlLT2Y7lePy/6ZZqszyF6nIkPs=", 41 | "anchors_complete": ["cal"] 42 | }, 43 | { 44 | "hash_id_node": "21287715-6d1d-11e9-a653-01a98661d85f", 45 | "proof": "eJylVE2Om0UQ5QrcgWU8ruqf6i6vLHEFVtlYVV3d2JKxLftLgOXAhu1wg5BBCYgNEsqSe1jiMJQ9KCIzLCKx+lrd33tV9d7r/uHtsu13U/9m+nM9TYfTYj7/Om7sZn/8ct7Wstkd9pvdNH8Z76dvD/3Xz99v3a/ltD4vay6gBDAkGlOukhrG2NFi5dKAmQp1jlVKKVk70CDmUaFoMwgp/nahWW1stdtbP38WMNRSMM/I0GaInWdCOc4AhSsRWs3j3RVyeqFfbaapPyBXMv0RAHkGeQbhiwCLzAuoz9/Tt/3R6eOg2CHAv+jVyCHYahEfRLs8pr8g/5M+8/O3epRdW/fT3Xc/b0X79vcm29Vla39cPZy93h9Of33y6e2r7fnZtdONLT5mytuf9oc3p7XMQqbbV8fzEqxpCElQWwRlSxF9EVTFXOasDTolI6pVcy9YsAbsoTZJSpLlKWHUGKJmwkQ4hC6edVWwKjE3qFlUohiFkt3jLF6eayNuHEYgifqEMJfaoYUGIKVee7BQKRZsYzSPRg6h1gqK0RAackyUJVP3iZFTtw8Jt+dlJ0YKULVqYqdg46rq1QsN6W5crzC8d6FYrQQC4VYoCQ/XNEN5qmEzIeBkJQbD2oc2buJateiYNJRQ+qDM1Adbxd6662HimpCbBo87fHYNhzv6McF6DP5xtzlNL8MCc6YagSosUjJsKQLnHhPnMDwR0IYzxZZGpoyVOKCP2wY6sWJQlkKmOfvCBN2kGDNr7z2hqouXXDS05LnwKUfCKiMpB4sqJGVoDl0CJ3AV0wctvnnI8enu++vdf+3R/uWfaG/svGQNvaEJGDphcbWEWWW4t+a3f2BM2aUbCbDWwUMtt1A0RAYibXL/4rg53Z3t8uz4qxPzjf93E2L1L829WN+ZHOf/t8zcZJK/AaZejzM=", 46 | "anchors_complete": ["cal"] 47 | }, 48 | { 49 | "hash_id_node": "21287716-6d1d-11e9-a653-016e742c718a", 50 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1d1eWeIXWGVjVXVV4ysNtmXfhGQZ2LAd/gAyKAGxQUIs+Q9L+ZiUZ1CUZLKIxOLqXnXfc+rUqdP90+t13+9mezb/u53nw2m1XH4fJ73aH79d9i1Pu8N+2s3Lp/F2fn6wP75+t3S75dP2vIYxtJCoZtRihh0zRgLDETQhCieiBBmrXh6umaRJwxatB9Ua/7zQbCbd7PZq568ChloK0oIUdYFobcGU4wKQrKTQC1b+5w5yeiLfTfNs98gNz38HwLaAvIDwTYBVbiuoj9/R9/3R6eOgaBDgPXpRcgj2WhiExB7QX5CfpM/t8Ws58q5v7XTzw2/XLHb9V+frzWVpf9zc773cH05vvvjyxS/X50d3SiddfU6XL37dH16dtrwImS7gNUZsPRXSTNpLK24eD+nGyWrHhpBDjMkwWA4WIrByH2hksZJT1g8Jj+d1lBiiZMJEOJgMNZoIaOWYO9TMwpGVQsm5QGbt0mqn1lsYgTjKA8JcqkEPHYBLzVawaKgUC/Yxeq6cQ6i1gmBUBJccE2XOZESILZk+aNmoIQWoUiU1p2jaqohXLzTYfHBWYbh2pli1BAJuvVDiNtzTDOWBQujKBC1piUGxmvvXOqMrjI5JQwjZBuVGNppWtG7uh7J7Qj40+Fjho7tw+EQ/J1gfg3/eTaf5aVhhzlQjUIVVSoo9RWjZYmo5DECGPpwp9jQy+RmiFtDb9dE6sWCQxp4Jydk/lNGHFGNuYmYJRdy85KahJqrVuxzJozWStKBRmLgM8bRwaAncxfSBxFf3OT7d/Hh39l96tH//L9qTntdNgnVUBkUn9DgityY8fLaqsQ6MKbt1IwHWOtoQzT0UCbEBkXS+fXKcTjdnvVw7fuvEfOX/XYVY/U1LL2Y75ePy/5ZZKs/8Fu8Mj9I=", 51 | "anchors_complete": ["cal"] 52 | }, 53 | { 54 | "hash_id_node": "21287717-6d1d-11e9-a653-0160de72b4a0", 55 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1d1eWeIXWGVjVVdVY0vGtuybQJYTNmyHPwgZlIDYIKEs8x+W8jGUZ1BEZlhEYnF1r7rvOX2qzqn+6e1S9rvJfpjer6fpcFrM59/HjV7tj9/OZc2b3WG/2U3z5/F2enGw37/+uHS75tP6vOwJWoTeZVQoKUklScQYujRlyEQJtYRQkCSWOjLl1pvlnG3UGA3/uNCsNrra7dXOXwUMtRQsM1LUGaK1GVOOM0ACtRJ6Ynh3Bzk9699tpsnukSue/gqAbQZ5BuGbAIvcFlCffqSX/dHp46BoEOBf9F3JISi1MHTqxg/pL8j/pM/t6dt+5J2s7XTz8tctd9v+KbxdXZb2x9X93uv94fThiy+vX23PT+6UbnTxOVVe/7I/vDmteRYyXb86npcjcMQeeyWKWpuZjAyQFIlTiYhlDE4jkpj00QdnjUmyBhzJ2uj1U8LteSmtdMu9lNyaokR/pLhJ2olLVmHNGQoVCGzGYVgopiJurEBpQR4pzKUaSBAALjWbF6ihUiwoY0iunEOotUJHPwoEW0yUOZMRIbZk+kihUUMKUHvtqTlF01Z7H4EKDTY3zioMHMwUq6eMgJsUStyG99S1P1IIokzQkpYYFKsNj6kwusLomDQ6IdvwjJKNphVNDLQqa2Vy0+Chwid34XBHPydYD8E/7zan6XlYYM5UI1CFRXI7JUVo2WJqOQxABhnOFCVdhgcrtYBergx04u6D1riQ9pz9Qxmltxh9xswsYe/evORNQ01Uq1c5ElYeqbegsbP7PHoObm5L4F1Mn0h8c5/j082Pd7P/2qP92z/R3uh52XowQR9y9YT14t3i1joP91Y11oExZW/dSIC1Dk+gZgmlh9iAqAvfPjtuTjdnvVw7fuvEfOX/XYVY/U1zP8x2ysf5/z1mrjzx39pvkXE=", 56 | "anchors_complete": ["cal"] 57 | }, 58 | { 59 | "hash_id_node": "21287718-6d1d-11e9-a653-01cf0c91e2f7", 60 | "proof": "eJyllE2OJEUMhbkCd2A51WXHv2vVEldgNZuSww5TKTVVpcqcAZYDG7bNDWAazYDYICGW3KMkDoOrG42YbhYjsUhlKjP9xfPzi/ju7bUc9sv4avlztyzHebNefxknvTqcPl/Ljqf98TDtl/XLeLd8fRy/fPru1d2O59352uLoPVTINdqIaGIoFcAiJ6smiRqh1VCoi7DUXjTHkdQipSSQ+dcLZjvpdn/Qcf4kYGi1YlsVRV0hDlpxyXEFKAZCOILVP+5L5hf9i2lZxkPllpffAyCtIK8gfBZgk2kD7fk7vBxOjo9W4oAA/8J3LfmCb5Whlz74Mf5S+Z/4TM/f9hPvZTfm229+uuE+bn4TvtleXh1O24dvrw/H+a+PPn71w8352b3SSTcf0uWrHw/HN/OOVyGXS/E1mYZYYwNJPRq6bI2tmwgON1O4M48G0PniebRmjQtZDq21zpH5CVCo9pF7rZlIUaJfUmNt2gvXrMKaM9RSIfAYHGyEOlRESAUqBXkfeDpf59oGSBAAri2PilVDK7F6Vya5cQ4XMdDRlwJBiqlkzmWUgkhp6BOFoxCWAK23nsgRpNR6t1BqMR7ugDdsaMwlNvWQAZPUkpjMPXXtTxSCKBegpDUGxTasCwmjK4xek6wX5GElUxlG2nDIAG3K6l760OCxwmf34fCJfkiwHhd/v5/m5WXYYM6lRSgNNin5EFIEyiMmysEAGcScFCVZLhlboYDeru8zB3cMnbgW7Tn7gzJKpxgz9TFGQt+Z0JKbhppKa96lJWxsqVPQ2NnnbD0HHy4lcBfTexLfPOR4vv32fu+/9mj//E+0J/U89jAElUHRgdXdYqLO5rNVT6ZhTNmtswTYmpF1zRJqD5GglC589+I0zbdnvRw7furEfOX/XYXY/F7WvtjYK5/W/3eZtfLCfwNO0ZMN", 61 | "anchors_complete": ["cal"] 62 | }, 63 | { 64 | "hash_id_node": "21287719-6d1d-11e9-a653-016e1c5cab92", 65 | "proof": "eJyllE+OXDUQxrkCd2CZnnb5T9nVq5G4AqtsWuUqF/2kprvV/RLIcsKG7XCDkEEJiA0SyjL3aCmHoWYGRWSGRSRW78n293PV58/+6e2l7Hfz+GF+v5nnw2m1XH6fJr3YH79dyoan3WE/7ebl83QzvziM37/+OHSz4dPmfDk65Qa9Fi6xVcwmwZg4WEQMqXUrZjkiYU0pWeutpgJJs8YeyXL/4xaznnS92+s4fxXBKRVogQq6ABi0YCxpEQAHSBHuFN/dSU7P+nfTPI975Zrnv2JwXSiLEL+JYVVoFdrTj3jZHx2fDNMIMfwL3xVdAtIqh4598EP8rfI/8YWevu1H3slmnK5f/rrlPrZ/Cm/Xt0P74/p+7vX+cPrwxZdXr7bnJ3eVTrr6nC6vftkf3pw2vIgFr14dz5fMNeZCqdZmKMUQK3aRmKUra+eeiJpENACJVrrlWlrVEsPg2Fp4BKwsIVDqAITE2bvPwKQjJknBpBU0C9DSyCU1Yu4NwOvt2mo00/QpcOtZKKUkyo4z1SxBqorIkDaSDKw5lZG5tebRYB5Y3HrwuZAJ0SX2GIgEGIOnpmcyKaTUevdoVTQefnCjBQNjxtS0RgxM4hlkcg9GCfVRy0GUMVDWmqL63taFhKGCJNdk6wg8DAvhMNIGQ0bQ5vY2Rj+08LDCJ3fh8BP9nGA9FP+8m07z87iCUrClgC2sclaQnAKVkTKV6P5zEHNSkmwFCzSkCN6uGDi4Q+zEFbWX4j/KIJ1SKtTHGBl6d/OymwaasTXv0jI09mtHUVNn5Gq9RM8H5eAu5k9KfHOf49P1j3d3/7VH+7d/oj3p+ZJ6HALKQcGB1d1ios6mEFRTM0i5uHWWPUPNyLoWibXHRAE9uHzz7Didrs96++z4q5PKha+7iKn5F5e+2dgpH5f/d5ul8sx/A37kkaM=", 66 | "anchors_complete": ["cal"] 67 | }, 68 | { 69 | "hash_id_node": "2128771a-6d1d-11e9-a653-01bab60e6173", 70 | "proof": "eJylVMuOW0UQ5RfyDyzjcVc/qru8ssQvsMrGqkc3vtJgW/ZNSJYJG7bDH0AGJSA2SIgl/2GJj6E8gyIywyISq3vV3ed0nVOn+rv3a93v5v5y/nM7z4fTarn8Jk12tT9+tdQtT7vDftrNyxfpdn516L988WHpdsun7XmNJh1yTShJpUsclIVyTsKsMgKKcQczwRoildyMOWORWjCpFki/Xmg2k212e+vnzyPEVivwAg1sAdBpwVjSIoCwYOgINf1xBzk9l6+nee73yA3Pv8cAtAhlEeKXMawKrUJ79oFe90enTwNTDzH8i14MHQLaKgdB6fyQ/oL8T/pCz97LkXe67aebNz9ds/Tr35SvN5el/XFzv/d2fzj99dmT1z9cn5/eVTrZ6lNUvv5xf3h32vIiFryA163kEKOiO8wdk0FqWUmoa8ixWXJKZUR3twVvRx/eBBVsQA2GdPqY8HheV9YQKAkAIXF29RmYrMekKQxtBccI0FLPJTVilgbg9Yq1GsewxxX2Ukqi7HTDLGvQaqratfWkHWtOpWdurUVEdgnFrQffC5kQHTIeEyIBxtCkSaahhYyayIhYcXD3xvUWBgxmTM1qxMCkFTPTcE9LqI8kBzXGQNlqiuZ3D1FShgqaHJOHIHAfWAj7IGvQtQfzzFpj9KaFhxU+vQuHd/RTgvUQ/P1uOs0v4gpKwZYCtrDK2UBzClR6ylSi+89BhzMlzaNggYYUweXqACcWiEJcfQhL8R9jUKGUCknvPYOIm5fdNLCMrbnKkaHx8BGN5iOKXIeU2DlSDu5i/qjEd/c5Pt18ezf7bz3aP/8T7cnOa5LYFYyDgRNWd4uJhIdBMEttQMrFrRvZM9QGDbGisUpMFBBF+fb5cTrdnO3y7Pirk8qVn7uKqfkXl35Z3xkfl//3mqXxzH8D55+SIw==", 71 | "anchors_complete": ["cal"] 72 | }, 73 | { 74 | "hash_id_node": "2128771b-6d1d-11e9-a653-01f9221b3cd1", 75 | "proof": "eJylVD2OW0cMzhVyh5TWipwfzlCVgFwhlRuBw+FEAhRJkJ6duFynSbu5geMN7ARpAgQufQ8BPkyo3cCId1MYSPXeG873kfz4Pf70dqn73WQ/TO/X03Q4Lebz7+OmX+2P3851LZvdYb/ZTfPn8XZ6cbDfv/54dLuW0/q8hKwNU8lVE2BMRFEq5w6kHEOrKioxm1QsEAxrUkjKSQbWnIs1/ONCs9r01W7f7fxVwFBLwTajjn2GaDwTynEGODgEbFE7vruDnJ617zbTZPfIlUx/BUCeQZ5B+CbAIvMC6tOP9Lo/On0cFA0C/Iu+dXIIai0CjZrJQ/oL8j/pMz99246y07Wdbl7+upVm2z9VtqvL0f64uo+93h9OH7748vrV9vzkrtJNX3xOl9e/7A9vTmuZhUzXr47n5bAsSKlKwSAE1PogZuNgzQPFa2fVBh6TyC68eaNUUhOWzCW2Twm352UcKQbXw79LKinWqgCawWovoIjIrFa5kCVsGitGTqRYklnm3tMjQss5+x1kGh5W0NJV1bRaVPMcboQktdZAJGKUXXr0GCQmcsh4TEiMFKC22hIP9axcWxveFg0xH5xVGDhEKHrNgUBYCyXh4ZpmKI80BO0uHadeYuieezRldfFQo2PSaIRigzKTDe4VTQ167dKrkA8NHlb45M4cPtHPMdZD8M+7zWl6HhaYM9UIVGGRUkdNEThbTJzDABTQ4UxR08iUsRIH9HbV/6AiDUNjKdRbzv7SBbVxjJmb2WVqzcVLLhr2RLV6lyNhlZEahx6bO6WMloNJ4ASu4qcjfXPv49PNj3f//mu39m//WHvTz0tuwRS7QEcnLK6WMDcZHaH3WIdvg+zSDd8LtQ4erWcNpYXIQNRUbp8dN6ebc7+sHd86MV/5vasQqz9p7sls1+U4/79p5l0m+RtBlJBW", 76 | "anchors_complete": ["cal"] 77 | }, 78 | { 79 | "hash_id_node": "2128771c-6d1d-11e9-a653-019a9cbc038a", 80 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1V1eWeIXWGVjVVd14ysNtmXfhGQZ2LAd/gAyKAGxQUIs+Q9L+ZiUZ1CUZLKIxOre293nVNU55/ZPr9e638392fzvdp4Pp9Vy+X2c7Gp//HapW5l2h/20m5dP4+38/ND/+Prd0u1WTtvzWqEL10xxlBYzEg6pOWeh1CAPMDCKsamMTqbEqZhplCiFMAZf+PNCs5lss9tbP38VMNRSUBdkaAvEzguhHBeALKxNIVb55w5yetK+m+a53yM3Mv8d/NAC8gLCNwFWmVdQH7+j1/3R6eOg2CHAe/TNyCGotQg0av0B/QX5SfrMj1+3o+x02083P/x2La1f/6Vyvbks7Y+b+72X+8PpzRdfvvjl+vzortPJVp8z5Ytf94dXp60sQqYLeG2FeoYURumDsUgTsYg5j4Q5DlZkhaxj5MBjKEO0xiFLEUvCRPiAMI4Ug+vh3yWVFGtVAM3QqxVQRGTWXtnLJmwaK0ZOpFhS75nN0gPC7sb7GWQavq2gxVS1a+1Ru9eIuSeptQYikU7ZpUffg+TtOWQ8JCRGClBbbYmHelWurY1AhYZ0N65XGJ44oeg9BwJXr5CPO1zTDOVDwuN5DWpCwMlKDOa1R1NWQbciOiaNRih9UGZyja1i1w5WTawKuWnwcYeP7sLhjn5OsD4G/7ybTvPTsHIPqUagCquUDDVF4Nxj4hwGoIAOZ4qaRqaMlTigj6sDnbhhaOz/krWc/cUEtXGMmVvvF9eai5dcNLREtfqUHpYqI3kyLDYhKaPl0CVwAlfxQ0tf3ef4dPPj3b//0qP9+3/Rnuy85ha6ogkYOmFxtYS5yTAEs1gHxpRdupEAax08mmUNpYXIQOQ3wu2T43S6Odvl2vFbJ+YrP3cVYvUnLb1Y35kcl/+3zNJklrfOsJGS", 81 | "anchors_complete": ["cal"] 82 | }, 83 | { 84 | "hash_id_node": "2128771d-6d1d-11e9-a653-015e9e9fda78", 85 | "proof": "eJylk79uFEEMxl+GMpezx/P3qki8AlWak2fs4VY6bk+3mwBloKENLVVIUAKiQUKUeY97G+YuKCIRRSSa3dXMfj/bn+0PN0elX436ZrxdjON6mE2nr6mTw37zcloW3K3Wfbcap6d0Nb5d67fn90dXCx4W2yPQYkpNaDE7VE0Zaw5SUyakZIUjWPGcojDHasiG4JSBDPoIEJP7vsPMO5mvetHtM4MmhoAy8dIeiJom7B1NAJ0mTVU4xF97yXCSX3XjqHfKOY8/DWCagJuAeWFg5tIM4vE9vvSbhqfqScHAX/gsvkmwxMCQfVZ+jN8p/4l36fgmb3hVFjqcv/uy5KzLH4WX891Rv5nf3V326+HT2cVye7DPs5PZU2o8+9yvr4cFT4zzZxeb7VGqkGP2nqREKtWJarGEGSwV61v+QYhjjErNagK1ubpc0XmoLhrvHwKXDRg5hKCVoLDDGB14LR6S98U3hlJRCbUIqWsQw1C9y77FwGha9u4x8GDvUyvvKR4/Fn9cdcN4ambYYkWCNhszawVbhZCckk3OVECGUhupFVydbzn71MaIYqnYwBlNThy8ZOfahzCWnIhcyqrapjMbiFZSRLE+RixULUauNicjlNlzqNkZZZMsMHv7IMXru5YO5+/3a3DZuvz1T5c7aV5mowWFQbABg0TklDJXQRChWJGsa22pFprRNdUsrpiQDSXwPhe+Otl0w/lWdhvYFpDcYfvv0FBsbz9twXQlvJn+b5ip8Mi/AcIURXQ=", 86 | "anchors_complete": ["cal"] 87 | }, 88 | { 89 | "hash_id_node": "2128771e-6d1d-11e9-a653-010d18dca7c1", 90 | "proof": "eJylk79uE0EQxl+GMo5ndnb2j6tIvAJVGmt2ZxefZHyW7xKgDDS0oaWCBCUgGiREyXv4bVjbKIJAEYnqTrv3/eabb27e3J7kfjWWF+OPxTiuh9l0+pw6Pe43T6d5Id1q3XercXpO1+PLdfn8+O7oeiHDYntilTxRsI4LZrE2xaJKNQumkIgAvHDUWCvaKMUTKJuAEFNmDwDxyw4z73S+6rVsHxk0wXssE6eoE8QSJ+KYJoCgGDSLz/h9LxnO0rNuHMtBOZfxmwGME+AJmCcGZhxnEE7v8LnfNDxVRwUM/IZP6poEc/ACyaUi9/E75T/xHE9v00ZWeVGGy1cfl5LK8muW5Xx31G/mh7urfj28u3i/3B7tfXY6e0iPFx/69c2wkIlhtxOfRDItag7MMQiiFs/GIOXovW/ea6k+QbCtj6SQvRJ6TtaDa7aLy+FvYJAmLZUgC2MIDK5kB9G57ChooVzU16xUmB0Ygeo4OZKAwTT3fB94tM+ptfeQjO+L3666YTw3M2y1AoELMLNWMVuCyIVsZFMBBXJtJMq2smueXTTYvOaKDZzQpCjeaWJuLyqYUyTimEopFlMyLR6NAdW6EDBTtRikth/WKCVx4mtiU8RECyLO/mHx5jDS4fL1fg2u2pQ//Zpypy3LZEpGlTa/BvQaUGJMUrVNVClUJMtBWzFoQddYk3I2PhmK4FzKcn226YbLre42sC0g8XH77thQaE83bcXKSmUz/d8yU5VRfgJ8/UPB", 91 | "anchors_complete": ["cal"] 92 | } 93 | ], 94 | [ 95 | { 96 | "hash_id_node": "21153d30-6d1d-11e9-a5a8-014c527fe564", 97 | "proof": "eJylVE2OW0UQ5grcgWU8rqr+La9G4gqssrGquqrxkwbbsl8CWU7YsB1uEDIoAbFBQiy5hyUOQ9uDIpjZRMrb9FN111dV3/d1//D+uu22s383/7WZ5/1xtVx+Gya72h2+XraNTNv9btrOy5fhfn6191+//BC638hxc7pGI2Fn0pQxRvBqVITQ3RBjL4AVIZkCUCbpxQrUMhYX5poV5LczzHqy9XZnfvqCEFOwAItsaAtE54UkqQvA2BKV7inHPy8pxxf6zTTP/pC5lvkPAuQFpAXQVwSrxCuozz/At91hwMdgvWn6L7xq9AFvBtxEuupj+HPmY3hcAaygPH+vB9m2jR/vXv98I+o3vze5WZ9Du8P6Ye/tbn/8+7PPb9/cnJ5dOp1s9TFT3v602787bmRBKd++OZyuO1BgHSSjV9BEwZuEmqNx0igVAKqWsVVCN+nYrWJGy8klWbDyFND4opQKUxu5KSQgydTNVbsWzXWUoN5FBdtQrJq1ao0LlFGnwBNAh2xkpfdGNVCJFioYS6Go4rXlboGjWcTcc6IYeXxYC0kKzqWHJ4CkHEwaYrEwjklj0PPQlavWbDEjmVsP3Djm0TY7EpIH4lTIe386steM0DtVMq29Yepk0apo80LYtGULgkwc2ugReoTOxVvosToj/x9wKHoxx1D0Y4z1OPnH7XScX9IKUxpUQ66witGwxQCcPERO1AEFWh8qhhZ7yglrZsIcauuDOVEcFEnJpimNHxtCDcpCYnX3iKoEdRik4uCqVjzPgVV6VCYLKllKH05yIY4g8sh07x58fLz7/nL33w5r//KvtScb4jQKxUCzOGvUgD7Is6H5iCQJ1CSdyeZUhybDXXF0kfrwQSzJEun9i8N0vDu187MzXp2Qriimq8pXBLwctXxrclh+apWlySz/AKXVkcs=", 98 | "anchors_complete": ["cal"] 99 | }, 100 | { 101 | "hash_id_node": "21153d31-6d1d-11e9-a5a8-01ab1e3c2bea", 102 | "proof": "eJylVD2OW0cMzhV8h5TWiuT8kaoWyBVSuRHI4Uz0gI0kSM9OUtpu3G5ukHgDO0GaAIHL3ENADpORNjDi3cZAXjMPnOFH8vu+mTfvr+tuO7fv578287w/rpbL78LkV7vDN8u60Wm7303befki3M0/7NtvX30M3W30uDldk5NKE7KUMUZo7FSUsDVHjL0AMkJyA6BM2osX4DKWpiKcDfT3M8x68vV25+30JSGm4AEX2dEXiE0WmpQXgGrYQiVr+uGScnxu307z3O4z1zr/SYCygLQA+ppglWQF/OwjfN0dBnwM3qsl+A+8WWwD3h2kqnazh/DnzIfwuAJYQXn23g66rZt2vH31y41au/mj6s36HNod1vd7b3f7499fPHn5083p6aXTyVefM+XLn3f7d8eNLijlc/J18BGFljXWnlLsXLmSgFIvGmoB48ZCzIzYq1fgHBXqKISWLKp8Cng4XbtclDIVGqcthQSkmbo3s27FModWqXc1xToUY/fKXqVAKY0LPAJskJ289F6JA5XogcFFC0XTxjV3DxLdI+aeE8Uo40MupCk0KT08AiST4FoRi4dxTKuAVQ3MwsbZY0by5j1IlZhH29KQkFogSYVa749HbpwReicmN+6DnU4endVqK4TVavagKCShjh6hR+hSWg09chOUh6I8vZhjKPo5xnqY/ON2Os4vaIUpDaohM6xidKwxgKQWoiTqwxFQu+cUauwpJ+QshDlw7YO54ZZBkZbsltL48SHUoCwksdZaRDMCji6Mg6thjPMcyNqjCXkwzVq6JWpKEkE1x09afHfv4+Pt68vdfzus/eu/1p58iFMpFAfL2sSiBWyDPB+aj0jSQFXTmWxJPDQZ7oqji9SHD2JJnsjunh+m4+2pnp+d8eqEdEUxXbFcEchy1Gpb18Py/1ZZus76D4Oqk64=", 103 | "anchors_complete": ["cal"] 104 | }, 105 | { 106 | "hash_id_node": "21156440-6d1d-11e9-a5a8-0101f7a3589e", 107 | "proof": "eJylVMGOG0UQ5Rf4B47xuqq6q7vLp5X4BU65WNVd1XgkY1v2JMBx4cJ1+YOQRQmICxLiyH9Y4mNo76Io2b1EylxmVDP16r1Xb/qnt9dtv5v9u/mfzTwfTqvl8tsw2dX++PWybXTaHfbTbl6+DHfz9wf//ct3pbuNnjbn62Ck4kKVE8YIXoyyErobYuwZsCCwVQBKpD1bhpLHzVWkpAr6xwVmPdl6tzc/f0GInAbOIhnaAtFloaxlAQjYswYu4n/ft5xe1G+mefaHzrXOfxGgLIAXQF8RrFhWUJ6/g2/744CPwXqr/D58rdEHvBlIU+21Poa/dD6GxxXACvLzt/Wou7bx0+0Pv261+vbPptv1pbQ/rh/evd4fTv9+9vnNq+352T3TyVYfo/Lml/3hzWmjC+J08+p4vgYOpQgi5EHSOADllpNGl9wIgmRE7TEmMojmYjG2pKReh6pGJX4IuD1fa+xu4zPV0TMWQ84ojRyrDYIVM5aisWbvDUMmkMqonjWXzq0xP2HokIws98u4QDlaKGCimWJVLy11CxLHREw9McUo48KSSTkMET08AaQqwbQhZhsEVJtAbXqxodSSLCYkc+tBmsRkXsWRkDyQcCbv/QmgeUkIvVMhq2Xo4k4WrWhtnglbbcmCopCENjhCj9Alews9FheUxx4+uw/H2OjHBOtx88+76TS/pBUypxIgFVjFaNhiAGEPUZg6oELrlji02DkxliSEKZTWh3NacVikOVllHg+mQ4KEwFLdPWKtBCWaFBxelYIXHVhGSqqQhapJR5aYXEkijBh8mJE3Dzk+3f54/++/HtH+7f9oTzaW0yhkg5rUpcYa0Id5NnY+KqyBmvLFbOEydlI5xMGC+8hBzGxM9e7FcTrdntvl2BmnTuArinxV5GokbTlm+c70uPzUKUvTWf8D162QNg==", 108 | "anchors_complete": ["cal"] 109 | }, 110 | { 111 | "hash_id_node": "21296170-6d1d-11e9-a5a8-015e0f0e5b51", 112 | "proof": "eJylVEGOXEUM5QrcgWV6uuwqV5V7NRJXYJVNyy676JaG7lF3J8AysGE73AAyKAGxQUJZco+ROAzuGRSRmU0k/uLry19+fn7vVf3w9nLsdyf/5vTX5nS6Pq6Wy6/z1i72hy+XYyPb3fV+uzstX+bb07fX/tvn70u3Gzlu7i5rT9VqNzZtCXo3dFcdOVGFkjM2zkLNcWjVMkS1eqbWBxgRjJZ+P8Ost7be7c3vPkNArtDSohrYAsB5ISR9kYA8zeSkBO/uW44v9Kvt6eQPnWs5/YkJeJFokfALTCviVerP38OP/SHgS7Y5lP4Lr1o84M0SD5Gp+hj+3PkYHlYprVJ7/lYPshsbP95898uVqF/9MeRqfS7tD+uHf6/318e/P/n01U9Xd8/umW5t9TFbvvp5f/3muJEFUj03XzrllpCyG87EVTk1ndJI4lVqqW1mgdposHdJDnnirBDKZyNx6U8ApUw3K0WkollgOwEPdFBDAIUWdkrR5nNAbpg4WIk3aX3SGEQfAh6CYUQBrc05sIfzxXJPxtKwqHgfdVrmEhOhzkpYCscDvaHEUhz0nwCicjYZAM2CgMjgpENy79y1VysV0Nxm5sGlmis7hLSekamhz/kE0LxXSHNiR9Mee9FEK9ZFhzeEoaNaqMjIeQTHNEua3HzkWboz8GMNn92HIxz9mGA9bv5xtz2eXuIKiGrPKU7SqhSDUXJi8lyYwmiQNKZVyqNMqgS9MkLNfcxQThRCImnVlCg+TGIFzplY3b2AKqZejDuEVr3DeQ/oMosyWlap0qYSuiCXFDEoH1B885Dj483392f/dUT713+jvbUwZ2BulrSKsxbN4CGehedRIck4hM5iM/XwRCmXYEEzclAaGaHevjhsjzd343ztxK2T6QILXXS+iKQtY5bvTA7L/ztlaXKSfwB5wJEv", 113 | "anchors_complete": ["cal"] 114 | }, 115 | { 116 | "hash_id_node": "21296171-6d1d-11e9-a5a8-01b1b6a63748", 117 | "proof": "eJylVE2Om0UQ5QrcgWU87urqn2qvRuIKrLKxqquq8ScZ27K/BFgObNgONwgZlIDYICGW3MMSh6E8g6JkZhMpu/6qv3r96r3X/dPba9nvZvtu/mczz4fTarn8Fie92h+/XsqGp91hP+3m5Uu8m78/2O9fvivdbfi0OV8nSd16ZCZpVpt20pxJxGxgI6gFgoRqpSQjvnzkEWOSIUojtYJ/XGDWk653e7XzFxFiK1BhURR0AWBtwZlpEaBDL1ywJvr7vuX0on8zzbM9dK55/isGaIuQFyF+FcMqt1Wg5+/gZX90+IQ6pOfwHnzvyRxeNTRhHr0/hr90PoaHVQirUJ+/7UfeycZOtz/8uuVu2z+Ft+tLaX9cP+y93h9O/372+c2r7fnZPdNJVx8z5c0v+8Ob04YXMZebV8fzde3+X40wmIwTBQy1cu9YEKS31iFZ8gXH3oOM0iBLh5KENdWCEZ8ARosjYNShlDuXVjWylcCQIvaCRiTg6BiwABUd5uyQBKWlKtDCh4BbB3TjO+RKLQ4CLUEwlhxiCd6aDXIeoYTKBrV6bDADh5JwCBElk/GUYW+oLABVsXrGpIUujESNujNKBaKaetCcUlHrzcClNYwt12jjKaAaeQbHiBQ9qUPA06hJibuYSytdiiJDiw1dwhxGCqNVExyJrEF7PPKz+3C4ox8TrMfNP++m0/wyrlyXQhgKhVVKCpIwtGyYWnZ/XCIZWjJKGrlkN6JFKG7DAHL7wSXiWrTn7AvlSxQQc+tmlqD3GCip30PXiggucwDxSL1FRfec6+g5GseWAnNJH1B885Dj0+2P93f/tUf7t/+jPambIxGrBs+rte5ugrl4qkheyYxROF/Ebpnck54xOYs8jCTVrDn2uxfH6XR7lsuz468O5quY8hW1qxja0s+ynfJx+amnLJVn/g+EcY//", 118 | "anchors_complete": ["cal"] 119 | }, 120 | { 121 | "hash_id_node": "21296172-6d1d-11e9-a5a8-010e5a5f3dca", 122 | "proof": "eJylVEGOXEUM5QrcgWV62naVq1y9GokrsMqm5bJddEtD96i7E2AZ2LAdbgAZlIDYIKEsucdIHIbqGRSRmU0kdl/+5VfP7z3XD28vbb87xTenvzan0/VxtVx+nbZ+sT98ubSNbnfX++3utHyZbk/fXsdvn78v3W70uLm75JY4Wq01GAQE82AZkpHU1KpzlgqFSxmlx4BRgbKKDmniIoTy+xlmvfX1bu9x9xkhtYKVFsXRF4jRFsoqC0AIVh7JTd/dtxxf9K+2p1M8dK719CcBtgXwAugLghW3Fcjz9/C2P0z4nHxYZ/gPfO85Jrw7NFMdvT+GP3c+hscVwArq87f9oDvbxPHmu1+utMfVH6ZX63Npf1g//Hu9vz7+/cmnr366unt2z3Trq4+Z8tXP++s3x40uiMu5+TLXlgFya9qrWRbtEAQ9K0UXczGDLk7SPEGTQFbNQYLdK6Ve8EPAw90lBQ1I5MOFu5ZWnTQKKObz+RQihqmkBKmgFB+BHZNYsparYYMnDInFOnKVRkPQC1iiwkAFZitPRjygQNXAWi33xKhQchomIjlsPGXYW3I1xOqpkqo16KZJZnr6ZJQLkoeP1Cal4tFb4JQ2EjWuFOMpoIcUhDFIyLsMQx7k2aeWFpXQuhVPio1asjGpjwyj1bA0skTD9njkZ/fhmI5+TLAeN/+42x5PL2k1dSmSoAiscna0PP3jSLnx9GdKZMMLJ5ubVXga0QjLtGGgVO04JdJavDPPD9c5QkuJW4+IjL0TSPY2zchFBM9z4Ny+3Bt5mp5rHZ0plGa0VEv+gOKbhxwfb76/3/3XM9q//hvtrU9zjFJ16EWj9ekmxhTPPcmssCYy5bPYjeeaU+eUJwseIZYrO1O/fXHYHm/u7PzszFcn8QVlvpB2QdCW867YuR6W//eWpetJ/wFBfY98", 123 | "anchors_complete": ["cal"] 124 | }, 125 | { 126 | "hash_id_node": "21296173-6d1d-11e9-a5a8-0195708d7979", 127 | "proof": "eJylVEGOW0UQ5QrcgWU87q7u6u7yaiSuwCobq7qqGn/J2Jb9E2A5sGE73CBkUAJig4RYcg9LHIbyDIqSmU2k7L6qf71+9d6r/unttex3s303/7OZ58NptVx+mya92h+/XsqGp91hP+3m5ct0N39/sN+/fFe62/Bpc77OYgWlY+YKpWTEnrMKDM5FNSYkwA4VkXNoFkkhVO1qhUslQoY/LjDrSde7vdr5C4hAJda0KBp1EaPRgpHbIkTCGppWqvT3fcvpRf9mmmd76Fzz/Bf4T4uAiwBfQVghrUJ7/g5e9keHz0mHkw3vwfeezeFVAwnz6P0x/KXzMXxchbAK9fnbfuSdbOx0+8OvW+62/VN4u76U9sf1w9nr/eH072ef37zanp/dM5109TFT3vyyP7w5bXgBWG5eHc/XVpqQKfcYEHLrIw/WHrWIqqI19K8gkJqI9FhAUgByB5hk5NbCh4Db8zXEGofUUVLLyYJLoyCDWu1EitWBQhlWW+5QFJW5KmETtBAbQ5SngH7aI9ZGMJoTC5KgYIASLHa0iDhCCZUt1iq5J4wcSk5DWmvZZDwZGTolZYmxaqrALBS6cGqNWm9Fc4mgpiORkMfNOll0aS2Bywg2ngKqtRLDGNBAexsScYBmbdzFqo/UpWjiSEBJhlMfOQyqJskVNIr0eORn9+FwRz8mWI+bf95Np/klrFyX0lIoLax8d6LkFAgtZUIYwSWSoQWT5IEFYysE0R2TEVv1LLhEXIt2XzxiZR+Bkm9dN7Mce4fQspKbkUtr8TKHezdyJ9DUuXAdHcEYKAfmkj+g+OYhx6fbH+93/7VH+7f/oz2pm+NZqxp6YaPubkZz8VRT8wpyAmG8iO2RcU86puwscFiTXFER+t2L43S6Pcvl2fFXJ+EVZLxqdAWBln6X7ZSPy0+9Zak88387HY/t", 128 | "anchors_complete": ["cal"] 129 | }, 130 | { 131 | "hash_id_node": "21296174-6d1d-11e9-a5a8-01b1b6a63748", 132 | "proof": "eJyllE2OHEUQhbmC78DSPZ2RvxG9GokrsPKmFRkRSZc0dLe6ygaWtjfeDjcAD7JBbJCQl9xjJA5D9Ayy8MzGEruqLMXLfO99lW/eX8phv9j3y1+7ZTnOm/X6uzTpxeH0zVp2PO2Ph2m/rF+km+WHo/321celmx3Pu9vLLLlbj8woZI20o5aCImYjEUKrECQ0qzUb8vmljBizDFEcmWr6/SyznXS7P6jdfhkhUoWWV1VBVwBGKy6MqwAdeuWaWsYPdyPz8/7ttCx2P7nl5c8YgFahrEL8OoZNoU3AZx/l5XBy+Zx0SC/hP/K9Z3N51UDCPHp/KH+efCgPmxA2oT1730+8l53N169+ueJuV38IX23PS4fT9v7b28Nx/vuLJy9/urp9enfSSTef4/Llz4fju3nHq1jqefiSfKQPxRJCAwxVgACSUey5IaOXQCXXzBpHxWCd0iADBcqaK438SDBCgyFt1IQ5WfBoNMogbJ1IS0sooQ5rmHusWpS5KXm1xQIgR5DHgv61Q2lIcSBoDZJiLSHWYNCLQSkj1NDYoDXHJhXgUHMagojZZHwqeHJBN6EsAE1Tc8aEQhdOiIQdq9uCqKYOmlCu6pYNPFpLnkSLNh4LqqEzOEbE6KQOAadRsyJ3seaWulRNDBQpyfCjjxwGNZM0MhoBPbT89A4Ob/RzwHo4/ON+mpcXceO5VEzBS9vkrCA5BSqWMpU4gkckQ2tJkkepBbBSBG9MBmDjDh4Rt6q9FH9QdguUUqFuZhl6jwGz+n/oWSHC2Yd3N3KnqKlz5TZ6icaRcmCunzLy7p7j+fr13b//1tH+9V+0J/VyJKamwXk16t4mmIenmtBXCqcoXM5hOzLeSS8p+ynKMJTcipbYb56fpvn6Vs7Xjt86qVzEXC6QLmKgte9le+XT+v/uslZe+B9wfI/C", 133 | "anchors_complete": ["cal"] 134 | }, 135 | { 136 | "hash_id_node": "21296175-6d1d-11e9-a5a8-0181224ebb54", 137 | "proof": "eJylVD2Om0cMzRVyh5TWikMO50eVgFwhlRuBHM5EAhRJkD47cblOk3ZzA8cb2AnSBAhS5h4CfBhTu4Fh7zYG3H3gN3zzHt/j/PJ22fa7qf80/beepsNpMZ//SBu72h+/n7e1bHaH/WY3zZ/T7fTi0P/89kPpdi2n9XlZOIMmgCFkNXGR2AJRD0al5ga1ppx6pSI5Z9YOaaRaR4GszQAj/XWBWW1stdtbP3+DAWsKmWfJgs1C6HUmLGUGoQTE2FU5/nvXcnqmP2ymqd93rmT6ByHUGfAM8DuEBdcFlKcf4Nv+6PCRbDRl+AheNXaHN4PaRIbqQ/hL50P4sABYQH76Vo+ya+t+unn5+1a0b/9usl1dSvvj6v7f6/3h9O6rr69fbc9P7phubPE5Kq9/2x/enNYyQ07Xr47npTB0jaPqsJaIG2GGQF2BDQop9OF9AQYxV8CQvIRDJSFTo5HkEWB3BzhXkGypx0i9CFUnwEmgJVXL3WLRkNQosg6pgWNPJTVgcr8fAUasWdkaNyt+Y8lAoeQStaYUGJtk6QBRA7tAja3jCAajxeAlpPop4Pa8hKB+PMYAqXuW0DW69AYDY47WRUcOhDRCCoYDidRidWax5nLJ1yOG1kvyEQ0saFpGCzzQohXR1jOGpi0ZSahYqY3EMCKMmrvPL5ZewyOGT+7C4Y5+TrAeNv+625ym57gIzKkQpAKLGC20SFC5uwjGAcG9GJbcwzg4cSipXrwtbfhoRQNqlZxMmf3DxCVUIq7ae/exKkKJ5qZaTKWEi45QZLgjaOTZkDyUsQvWCCLp09C9uc/x6ebnu91/7dH+4/9ob+y8xIaUzZdfetWoFLoPz3zvvcJCbjdfhl25eNyVKToLHr20mNkY9fbZcXO6ObfLs+OvDvEVRr4q9Qqhzv2uvjM5zr/0lrnJJO8Bo7KPOw==", 138 | "anchors_complete": ["cal"] 139 | }, 140 | { 141 | "hash_id_node": "21298880-6d1d-11e9-a5a8-013396876a61", 142 | "proof": "eJylVDGOWzcQzRV8h5TWikPOkBxVAnKFVG6EGc4w+sBGEqRvJyltN243N0i8gZ0gTYDAZe4hIIcJtRsY8W5jIMUHPoacx/fmPfLN+3Xb72b/fv5rO8+H02q5/C5NdrU/frNsW5l2h/20m5cv0u38w8F/++pj6XYrp+15HXq3ktWMwIo7NCBIOTj0aAiggjljIKh2+aRSVlYGTt6iWU2/X2A2k212e/PzlxEi11rDIhvYAsB5ISR1ESAlzrVkyfDhruX0XL+d5tnvOzcy/xkD8CLQIsSvY1gRr0J99hG+7Y8DHpP1pvRfeFX0AW8WuIl01Yfwl86H8LAKYRXKs/d6lF3b+unm1S/Xon79R5PrzaW0P27u197uD6e/v3jy8qfr89M7ppOtPkfly5/3h3enrSwi5UvzukLl3Jy5g7qRloIFu1USip1bzh5UjUvGxNF6Z9SoDXUcRN2kfQp4PK+9WaDCQYplR0xeJXGFsSyh5YFV3LAqDHcTknZhIPRccwuUuqRHgBi5KFmjNoztQ0dIUEtF5ZyBYpMiHgIqkCopNo8dLPSGMEox8SPJAXRsR4SQvQWOwXvLqYUeh3Rz0V4gxdQhg8UeU1JDHsyQS9Wh7hFD85phRDbWaFr7COslplZFm5cITVu2JMCRU+uZQsfQuXhLHauP1D5k+PQuHMPRzwnWw+Yfd9NpfhFXQJRrCrmGFaJBwxSYfIgYvgYYXnTLlBp2yuMOZY6QU219jFYUorKUbEo0fkyGBE6JWN19jFVjqGjDVMNcK1x0QJU+HImWVLKUrhRdImMQyfgJxXf3OT7dvL67+29HtH/9N9qTndexxVQsaBZnRU3gY3hmqY4KSRp202XYTHXEXSnhYEHda8NCRlFvnx+n0825XZ6d8eokuopIV5WvYuDlOMt3Jsfl/z1laTLLP10jkCE=", 143 | "anchors_complete": ["cal"] 144 | }, 145 | { 146 | "hash_id_node": "2129fdb0-6d1d-11e9-a5a8-013efcb8790c", 147 | "proof": "eJylVE2OY0UM5grcgeWkY7vK9ZNVJK7AajaRXS6TSCGJkjcDLBs2bJsbDNNoBsQGCbHkHpE4DJU0Gs10b0aaVb3n9/z58+fP9dPbZdvvpv7d9M96mg6nxXz+bdjYzf749bytZbM77De7af4y3E/fH/rvX74L3a/ltD4vNUINoNq8QI6xldRiEiRt1QQ4pYiWiTKmFnJxTly1dmbuXkLo+McFZrWx1W5v/fwFIVU3hVkytBlirzNhKTPA0L1pyRXa39eU0wv9ZjNN/SFzJdNfBFhnwDOgrwgWXBdQnr+Db/vjgI/BBgq/D68a+4A3g9pEXPUx/CXzMTwuABaQn7/Vo+zaup/ufvh1K9q3fzbZri6h/XH18O31/nD697PPb19tz8+uTDe2+Jgub3/ZH96c1jIjTrevjuelYcbsIVa1nnKvubt6GGcOkhJVBhyvsfZemrN4cFehoXRB5yL4IeD2vETt5G6uFhqNs5bqXhJICDp4kGnxQYiiIGalLBCS5tJG9aLu/IRhpJqVrXGzEjyVDAFLLlFrSsjUJEsHiIqsyhrbKI8G3iKOEIX6hCGgjt9jREi9QSUY6qTQwCnmaF3UMwYKjgmNnAZvi9VlaDQYNoOnGvaSENypXLtryKPNaEW09UzYtCULgpVqaJ4YPIIPoVvwWHrFJwyfXc0xJvoxxnqc/PNuc5pe0gKZUwmQCixiNGwxQOU+mmByQIHmlji0eFkeLKkSpjBGPKQVHYtWJSdT5vFgMlqoIYwd670PWZWgRKsFLaZS8NIHFvExEbKgkiS7MnWhGkEkxQ8ovnnw8enux+vuvx7W/u1/a2/svKRGIRtokl41asA+xDMLZURYwhg3X8SuXIbdlUMcLNiHOWNmY9L7F8fN6e7cLtfOuHUC31Dkm1JvCOp81Oo7k+P8U6vMTSb5D3IVkuA=", 148 | "anchors_complete": ["cal"] 149 | }, 150 | { 151 | "hash_id_node": "2129fdb1-6d1d-11e9-a5a8-016a39edf372", 152 | "proof": "eJylVEuOI0UQ5QrcgeW4HZ/8emWJK7CajRWRkYFLamzLrhlgObBh29wAptEMiA0SYsk9LHEY0m40munejMSqSlEVL9578TJ/eLtu+93cv5n/3s7z4bRaLr/myW72xy+XbSvT7rCfdvPyJd/P3x76b5+/K91v5bQ9r527KmWImb0zenNsGcBZgmdvoZaKnilVbU1a1mSRezDnGkKDKL9fYDaTbXZ76+fPCKm6KS6SoS0Qe11IlLIATMK1j75Mf11bTi/0q2me+0PnRuY/CbAuIC6AviBYxbqC8vwdfNsfB3xg86YR3oNXDX3Am0FtIq76GP7S+RgeVwAryM/f6lF2bdtPd9/9civab/9ocru5lPbHzcO31/vD6Z9PPn310+352ZXpZKuPUfnq5/3hzWkrC4rp0rwG7s3JsEuN7NiTheo9adVgxgCUq2mi7DzMJis5IVQkphpqQ4lPAFE7uZurcaPxHKtyLwmEWbmPUVocuVMQxDx2LMBJc2k95aLujwCP53WgmjVai80KeyoZGEsuQWtKGKlJlg4QFKNq1NDGeDQYIcFRIq5PJaOO30NASL1BJejeEjdwCjlYF/WMTMOMhEZOg/fFE+FQB8Nm8ISh9TJccadyVdcwDpnBimjrmbBpS8aClSo3TxE8gNfcG3soveIThs+u4Rgb/ZhgPW7+cTed5pe0whhTYUgFViEYtsBQYx8iIjmgQHNLkVvwmCKWVAkTl3HOShZF0io5mcY4XkyGhMocq/beh61KUILVghZSKXjRgUV8bISMVZJk10hdRkZAJIUPKL55yPHp7vvr2X89ov3rf9Ge7LymRpwNNEkfEVTGPswbSSyjEoXHuuPF7BrLiLtGDoNF9F5ayNEi6f2L43S6O7fLtTNuHY43FOJNqTcEdTlm9Z3Jcfl/pyxNZvkXED2TFw==", 153 | "anchors_complete": ["cal"] 154 | }, 155 | { 156 | "hash_id_node": "2129fdb2-6d1d-11e9-a5a8-01476fb7f540", 157 | "proof": "eJylVDuOG0cQ9RV8B4fisru6qj+MCPgKjpQQ9ek2CdAkQY5kK1w5cbq+gaw1JBtODBgOfQ8COoyKu4Yg7SYClM3UTL1+79Xr+uXtUve7qf80/beepsNpMZ//mDZ2tT9+P9c1b3aH/WY3zZ+n2+nFof/57YfS7ZpP6/OyS8MapRAT1JJxaBjcOAzIOaQqg8ZAyC2XlNKoUkuimAwNBNpA+esCs9rYare3fv4GoldNYJYt2izG3mZMXGchYslDyiAM/961nJ7JD5tp6vedK57+gRDbLNAswHcQFtQWoT79AK/7o8NjsqFC4SN4EewObxaaMg+Rh/CXzofwcRHCIpSnb+XIO133083L37csffu38nZ1Ke2Pq/tvr/eH07uvvr5+tT0/uWO6scXnqLz+bX94c1rzDChfvzqel5lHpqHKOWmhOMjEnUbV0nToiIDFXTeCyL2V1iIKJxxpFI4hAz0CrKoYKlLoDWqs3HoMzSeIWSo2q81SiYosRZB6LWhYJJAaCySy8Sng9ryM2U/HUiv7n+5nKamCjRBJWVWz+suogdoIvUAUBUkgI6hgiU71EWCIElAQnX/X0CD0oa7eZaKz6SzD+yCNmKPBgJTEsA1X3UoVtcceWq85hjHAeUkdGskdQ6ssemGkotkSxwYtqbsdBobRStc0sPYW20OGT+7C4RP9nGA9bP51tzlNz2ERiXJNIdewQDR3PIVG3UUQuHccdFimpDgoU6y5Qcyp+sBrYYkgjUs2IfIHY5fQUqImvXeMIuADtlajYa41XnT4oP3aNbAknLkMIegMDQNzxk8ovrnP8enm57u7/9qj/cf/0d7YeQkKyacs2eMmKCl2N8/M73z2MCdQpovZjarHXSj5mjAavSoW8pzI7bPj5nRz1sva8a2T6AqQrmq7gtDmflbfGR/nX3rK3Hji96VakVw=", 158 | "anchors_complete": ["cal"] 159 | }, 160 | { 161 | "hash_id_node": "2129fdb3-6d1d-11e9-a5a8-013b760d9685", 162 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKq6qh9ejcQvsMpmVI9ubGmwR7YTYBnYsB3+ADIoAbFBQlnyH5b4GNozKCIzm0js7u17z6lTp071D28vfbc99m+Of62Px5vDarn8Om3iYrf/culr3Wxvdpvtcfky3R2/vem/ff7+6G6th/XpMod15JKyJbduNBpbY06m6jYgW2jHCMsFqAnXUOUsViQnd8H0+5nmahNX213002eE1EZYWuTAWCD2tlDRugBMVjJEy1Xe3UMOL+yrzfHYH5BXevyTANsCZAH0BcFK2grq8/f0vttPek4x3AT+Q2/GfdJHQHPVYfaY/ox8TI8rgBWU529tr1tf98Ptd79cq/XrP1yvr85Hu/3Vw7fXu5vD3598+uqn69Oze6WbWH1Ml69+3t28Oax1QZLP4MviqSIJjE5VMYtGqt5z50GpOWFCRQKFMJCG3BgkCAQL66gVyoeE+9NldWeoLNAbVazaOkLLPDhb5Ra1RSrorFaMpdfCwWVye6hRkhhPFGIOIS616vxz+llKqhQDUFzdPft8GXWqG9ALoTlZohkSNy6Y+CkhoAEbM0LuDo2gD5+5gTGrcHS1MXGUBmYMmjYkC25DE7dSzQOetBy9ZoQxaOqyOhxlUHBUNT8rcvMcSbFRSz7y9JphtNI9Da69YXus8Nl9OOZEPyZYj8E/bjeH40taoUiuCXKFFXNMxxM06bMJoemdgo/IkpyHZMGaG2Gekx9YixqSNS1zCUXmQ+hsoaUkzXrvjGY0BxytYnCuFc99zEGPuaIUc0WzlmFCXWmmRTXzBxLfPOT4cPv9/e6/ntH+9d9ob+J0SU5pTtmy9mZsCfs0L2Yo54loIlc5m92kzribJJ4qZPTqXGTmxO5e7DeH25Ofr5156yS5IJaL2i4I2nLW6tvQ/fL/VlmGHvUftA2QhQ==", 163 | "anchors_complete": ["cal"] 164 | }, 165 | { 166 | "hash_id_node": "2129fdb4-6d1d-11e9-a5a8-01b9e036446a", 167 | "proof": "eJyllD2Om0cMhnOF3CGltSJnOD9UJSBXSOVG4M9MJECRBOmzE5frNGk3N3C8gZ0gTYAgZe4hwIfxaDcw7N3GgLvv44DvkC+f4S9vl7bfTe2n6b/1NB1Oi/n8x7jxq/3x+7mtZbM77De7af483k4vDu3Pbz+EbtdyWp+XkEyRSqpGgJFyjlI5OWTjGLSamMTUpGKB0LCSARmTdKwplab410VmtfHVbu/t/E3AwN2VZtnRZ4iNZ5KkzgCVG8RMlOXfu5TTM/1hM03tPnMl0z8BkGeQZhC+C7BIvID69IO87Y9DnqJ30wQfyatSG/LuwCbSVR/KXzIfyuMCYAHl6Vs9ys7W7XTz8vetaNv+bbJdXUL74+r+7PX+cHr31dfXr7bnJ3eVbnzxOV1e/7Y/vDmtZRZSvn51PC+7Akjx2qAXjeK5M1vPOZOCS+1S1DQkpp6ptRYTSDUIsWCtuUr/VHB7Xjav3Djlhlw4YZUxsxxzx+IX4d5qbMFJtGSM2AIXFIwlimDJUB4LYvYUqNQqTmX4WUqswTtgGhiYZRs/vULiDq0EVAs6GOlgSmWw81hw+AGkRAi5GXCA1i1Hgz5uIW+ifeSF2DGjhx5iVCfuEolLVXN45KG3mhF6D6Murd0w9dGhV1G7VGRq2aMgB47DgQSdoHNpFjvVxsgPK3xyB8eY6OeA9TD5193mND0PC0wp1wi5woLI0SgCpzaaSGF4J2Ddc4pGPeUxpcwBc6w2XlARxaAsJbumND5cRgscY2IdBBCqBqjkXNEp14qXPsacOykHjypZStcUmgQmEMn0SYlv7jk+3fx89/ZfD7T/+B/tjZ+XwQZbDpqlsZIOQoZ57rGOSJIYTNLFbE514K4p0qgiDahsbIvBid4+O25ON2e7rJ2xdWK6CpSuKl8F4Pm4q+1cjvMvvWXuMsl7UhWREQ==", 168 | "anchors_complete": ["cal"] 169 | }, 170 | { 171 | "hash_id_node": "2129fdb5-6d1d-11e9-a5a8-0181066f34f6", 172 | "proof": "eJyllEGOXEUMhrkCd2CZnnaVy1XlXo3EFVhl07LLVfSThu5W90uAZWDDdrgBZFACYoOEsuQeLXEY/GZQRGY2kdjV85P/8m9/rh/eXrfDfu7fzH/t5vl43qzXX+NkV4fTl+u2k2l/PEz7ef0S7+Zvj/23z9+H7nZy3l2uG3ThShlHUaSQw5BKRJKTAg0wsIyoTUbP1jKnYtZQUEoOGD3w+yKznWy7P1i/fBZD5GFKq2zBViF0XglJXUGoAXIemEZ+d59yfqFfTfPcHzK3Mv8ZIfAKaAXxiwgb4g3U5+/l2+Hk8gltNCX4j7xq6i5vBtxEhupj+SXzsXzYAGygPH+rJ9m3XT/ffvfLjWi/+aPJzXYJHU7bh3+vD8fz3598+uqnm8uz+0on23yMy1c/H45vzjtZRcpL8nU0tEQp8EBCHVCzYqKIgzpjlBpEpVBqAzvm4sduAgUTdy0h83gi2K1yZ8o9cGEKVXLjjHmEYtxGHr1ij5ZEl1mFHrkECVhQJJQM5algyEYxlVrFfM7ApWCNNiBQk9Zabv4xKhAP6CUGbVExupOmqQT3/EQQgkLSlLwpvQFH6KNlbDD8lmRddHied8Cxsziig2aJh7jnUrUZfCh4ulxbrznAGNHr0jpaoOEOrYq2paKmLRtK4MjoHSAYCQaX3nCk2jnw4wqf3cPhE/0YsB4n/7ifzvPLuAlEuSLkCpuULLSEwNTdBEXvnUAblglbGpR9SpljyFjbCLWIhqjsu+QskR9M3AIjEmvvPQXVCDUZ12Ap1xoWHz7nkZSdJpUsZSjFLpETiO/sByW+eeD4fPv9/e6/drR//RftyZzHFtGnrFk6a1InxJtnhtUjJL7cQkuzmarjroTJqyCHqqVCzonevThN59tLW54df3WQrmKiq8pXEXjtd/W9yWn9f29Zm8zyD9B/kI4=", 173 | "anchors_complete": ["cal"] 174 | }, 175 | { 176 | "hash_id_node": "2129fdb6-6d1d-11e9-a5a8-0137e848cdef", 177 | "proof": "eJylkz9vk0EMxr8MY9PYd+f7kykSX4GpS+Q7+8grhbxR3rcFxsLCWlam0qIWxIKEGPke+TZcUoQgUyW2k0/+2Y8f+939vPTrUV+NP5fjuBlm0+lL28lpv30+LUvu1pu+W4/TC3s7vt7ol6d/QrdLHpa7OWgxpSZ0mAlVU8aag9SULdrkhCM48ZyiMMdqrAuBlMEa9BEgJvq6xyw6Wax70d0TgyZVyX7iBWWCqGnCxHECaINGF4to/XFIGc7zi24c9SFzweN3A5gmQBMwzwzMKM0gnv3Bl37b8M5KLZngL3zOThteBFJhrjkf4/eZx3icAcwgnN3nLa/LUoerN59WnHX1rfBqsQ/128XD302/GT5cXq92J4c+O5k9RuPlx35zNyx5YshfXm93c2syeCPBgwZpwy0musopFcco6lUzOeCM2MIBFKuPGkzFCN77egxc7eaUCwUm7zUKmKguOtQGiMbWXNjHWALXBLky+qQIzT82KdhEmpIzx8CTw5yavMfM+Dj5/bobxgszQyIfLbTdmDknWJyFVs66RKYCMpQqnmxxlTxh9KmtkY2lqQytc5MTBy+ZqD2EseRkLaWsqm07s4HoJEUU17RhsdVh5OpyMmIzew41k9Emsc2RvfunxbsHS4ert4czuGkuf/7tcie7uSnGBoHsuR2Aa+5o8SJiY4sQW1OYoscmJTbvM1nXuqCqsbhAQibfnm+74WpX9hfYDtDSqXF0GtOpgTRttXQtvJ3+b5Wp8Mi/AM5XRrM=", 178 | "anchors_complete": ["cal"] 179 | }, 180 | { 181 | "hash_id_node": "2129fdb7-6d1d-11e9-a5a8-018e6ea7dd2a", 182 | "proof": "eJylUz1vU0EQ/DOUsb13u3sfrizxF6jSWHu3d/hJwc/yewlQBhra0FJBggKIBglR8j/8bzg7KALTRKJ72qeZ3Zm5efNpkfv1WF6MP1fjuBnms9lz7HTab5/O8kq69abv1uPsAm/Gl5vy5fH96GYlw2q3IEWPGMhxMVmIUiyqWLOYFBIigBeOGms1FKV4BGUbDMSU2QNA/LqnWXa6XPdado+ssbFq8hOnRifGlDgRljABE4or4lWt/DhAhvP0rBvHcodcyvjdgokT4AnYJxbmHOcQTu/pc79t9IRac2L4gz4lKo1eFWIWqSkd0++Rx/RmDjAHf/opbWWdV2W4evXxTFI5+5blbLkf9dvl3b/rfjO8u3x/tjs53Nnp/CEaLz/0m9thJRPLbg9eSM4BhGwyNgdmIae5JA/ORUVRi5Fq9OSlmS4+7A2WTCYhOe8s4T+EvE9A2LkSFGwoFMgUSSZYrA3rQsheaoRUxbhYDEioYqPHyCVGsseEJwefmryHeHwMfrvuhvHCzg2zCwguwJxITSaEtg4psq1gBHJVx5ipsmMTXLTGYcjVhKbb2BTFO03M7UPF5BQROaZSSjMiWQikMRilps1krGSC1PZgrWISJ74mtqVJJBBx9NeJt3eRDlevDzW4bil//p1yp7uFzRa9QnJSYqKEpmSnrQahTVjQZuHg2qPn0LJPjNSu4FpCJs+tD+nmfNsNV7u8b2ArIPLUEk9DnFqIs7arrFW2s//dMlMZ5Rf2OEZX", 183 | "anchors_complete": ["cal"] 184 | } 185 | ] 186 | ] 187 | -------------------------------------------------------------------------------- /tests/data/proofs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "hashIdNode": "21156440-6d1d-11e9-a653-01ef1ab22ee7", 4 | "proof": "eJyllE2OJEUMhbkCd2BJddnx4wjXqiWuwGo2JUfYQaXUVJUqcwZYNmzYNjcYptEMiA0SYsk9SuIwuLrRiJlmMRKrSGWmv7DfexHfv7nuh/1iXy9/7pblOG/W66/ipFeH0xfrvpNpfzxM+2X9It4v3xztl8/evrrfybw7X6MGYePQMmFKYFVDkYBmiphGAawIWRtAoCCjaIFafDFhrtRAfr1gtpNu9we18ycBMZNzVqSoK0TjlVCOK0AbKC0Es/LHQ8n8vH05LYs9Vm5l+T0A8gryCsLnATaZN1CfvcX3w8nxcVA0CP/GNyUvwV6LQKNm8j7+Uvmf+MzP3rST7PvO5rtvf7qRZje/dbnZXl4dTtvHb68Ox/mvjz6+fXlz/vSh00k3HzLl7Y+H4+t5J6uQ6fbl6XxdBUYeWlxYxZiAyWq2UAyElAQq69BobKRskC4a08ikMAqWkdoTIEsJ2oalEbH1AuRIIlaBiDISsYlKA8xFU+y5UwHNJSHF0Eg6PQH2xCys2VWuCd3i1FJFKyU03yJCqVUyK5TEHXKrpFbNbMTuCao5PAHmqLkZNuHWcxZMGhg7seeM0MsZoboIuQ4OAqWMZpa0cVHF3gY/AUJX8TGTlhgUq43WuQsW7JGH57URig3Kru1g9d67gVYVrUIBA7wLdEcfwuGOfkiw3i/+YT/Ny4uwwZypRqAKm5S88RSBs8XEOQxAgT6cFHtyM7NPzcEdqH2ggxuG5jaStpz9QcXH5hgzuxCWsLUANSlX1ES1+pQjYXVvGweNTUhcshxMgksqQumdFl8/5ni+++7h7L/yaP/8T7Qn9fi0YB09LYoOLK6Wn+kmQxFUYx2e0ezSjeR3gBs0muYeSguRgah1uX9+mua7s16uHb91Yr7y/65CrL7S2jezvcpp/X+3Wass8jfwGZIG", 5 | "anchorsComplete": ["cal"] 6 | }, 7 | { 8 | "hashIdNode": "21158b50-6d1d-11e9-a653-012091f557f7", 9 | "proof": "eJylVEGOXDUQ5QrcgWV62mW7bFevRuIKrLJpVbnK9JeG7lb/nwDLwIbtcAPIoATEBgmx5B4tcRiqZ1BEZlhEYuUvf79XVe89+7u31/2wX+yr5c/dshznzXr9ZZr06nD6fN13PO2Ph2m/rF+mu+Xro/3y6butux3Pu/N11MhkFAUL5BysaawcwUwB8qgBGgRUCSGWyKNqDa36YkzUigT+9UKznXS7P6idP4kA2ATDqijoCsBoxQXTKkAMBAPRwX/cQ+YX8sW0LPaA3PLyewxAq4CrED+LYYO0Ce35O/p+ODl9GiVZiP+mFy0Ogd4qByli/Jj+gvxPeqTnb+XE+76z+fabn25Y7Oa3zjfby9bhtH349/pwnP/66ONXP9ycn913OunmQ6Z89ePh+Gbe8SpiuYCvY25dWaq2WodpHqEPpCKxc2XC2vNI1qxCB0i1WWnZaZJB8mOY+H3C0/mauEaVYY4D6TWUQKUUUg4JeORCxl4vAFbNqWMvNSjWDCVFKdzLE8KeiZgUXeWWwS3OkhtYrVG8RAq1NUbSUDP1gNKKer9mI3VPUMP4hBCTohgIk3REhqyRoBfynBVwOEFo1hDboMjBZRGzrEJVFboMekIYXEEfM2tNUaHZkE6dwTVLNDyvUoBtFJfVBqn33i1oU9bGJbo5j015dh8Od/RDgvUY/P1+mpeXcQOIpaVQWtjk7I3nFAgtZcI4ArDb7EzJ7cWCPjVFd6D1AU4sEMVtLCqI/qHsY1NKSC6EZRCJoWWlBppLaz7lyNDcW6GoSbiwS4bROLqkzCW/1+KbhxzPt9/e3/3XHu2f/4n2pB4fidbB06LghB5L8DstPBSCamoDUkaXbmR/A9ygIYo9VomJQinS+e7FaZpvz3p5dvzVSXjl565iar6WtRezvfJp/X/LrJUX/hsozJCp", 10 | "anchorsComplete": ["cal"] 11 | }, 12 | { 13 | "hashIdNode": "2115d970-6d1d-11e9-a653-01595ebb1d42", 14 | "proof": "eJylVE2OW0UQ5grcgWU8rurfKq8scQVW2VjVXdXYkrEt+yWQ5YQN2+EGIYMSEBsklGXuYSmHoTyDIjLDIhKrfurX9VV9P90/vV32/W6yH6b362k6nBbz+fdxo1f747fzvpbN7rDf7Kb583g7vTjY719/3Lpdy2l9XkYNwsah5YIpgZGGKgHNFDGNCkgIWRtAKEFG1QpUfTFhptJA/rjArDa62u3Vzl8FxKxcYVYUdYZoPJOS4wwwc7bWUFN4d1dyeta+20yT3VeuZPorAPIM8gzCNwEWmRdATz/C9/3R4eMo0SD8G75p8RLsVAVaaSYP4S+V/wmf+enbdpRdX9vp5uWvW2m2/bPLdnXZ2h9X9/9e7w+nD198ef1qe35yN+lGF5/D8vqX/eHNaS2zkMv1q+N52ThoSxEtxFL8tOvaQIu2UmRYpxSsG3YIZNprNGXnShKtYsTS86eA2/OSYu1oblmCFI0zUuWcAlUCGVKBCwoHG9wopdg6aPSWddBorCz0aMKemIU1u8qU0C1OLRFaraGNiBEqkWRWqIm7I1FRIzMbsXuCKD+mnKPmZtiEW89ZMGlg7IU9ZwW9nBHIKGcaHARqHc0saeOqir0NfgQIXaUAJ60xKJKN1rkLVuyRh+e1OWUbJXNx2uqzdwMlFSUpAQM81PDJXTjc0c8J1sPin3eb0/Q8LDDnQhEKwSIlHzxF8BTExDkMQIE+HCn2NHJxjwoHLJH6cLukYWgs1UOQs3+oOG2OMbMLYQlbC0BJmTxQhchZjoQkI12yFJsUcclyMAkuqUhJn4z45j7Hp5sf7+7+a4/2b/9Ee6PnJTcPHKqAogNWV8vvdJOhCKqRBsaUXbqR/A1wg0bT3ENtITKU0rrcPjtuTjdnvTw7/urEfOXnrkIkX8vcm9lO5Tj/v23mKpP8DX1rkJ8=", 15 | "anchorsComplete": ["cal"] 16 | }, 17 | { 18 | "hashIdNode": "21287710-6d1d-11e9-a653-01e94c159e8c", 19 | "proof": "eJyllEGOWzcMhnuF3KHLeCxKokR6ZaBX6CobgxSp+gFT27Bf0mSZdNPt9AZtpkhadFOg6LL3MNDDVJ4pgmamiwBdPUF6/yfyJ8Xv3q/bfjf7y/nP7TwfTqvl8ps02dX++NWybWXaHfbTbl6+SLfzq4P/8sWHrdutnLbndaFQrJCxaQ1AZNFdtaWABXJKsXISrB6bFs1NVIsnrNTAEKHV8OsFs5lss9ubnz+PEKlWCItiYAsA54UUTIswVrkBslP7405yeq5fT/Ps98qNzL/HALwIuAjxyxhWyKtAzz7g2/448KmX5CH+G69WhgQaVQkjRpeH+IvyP/HIz97rUXZt66ebNz9di/r1b02uN5et/XFzf/Z2fzj99dmT1z9cn5/eRTrZ6lOyfP3j/vDutJVFxHIRr5M4M6Bb75RC76VrVgOwUjsG7lw9mymp9RYs1eIetTQsnqWmVh4BKdUGHpxyyMkZgSpjHpFRkC41cAHh6J2Vck56gQbU2qkrGwt9DDye1y0zCxsOlymDC2fNBF5r1J4ghUokyBZq5jZIVMzJ3Xtq7EwYHwExGaqDCmtDFMgWGVrhHLzAkDMEckKkzlFCrV19eKBczaBp50fA0ExK4Gw1RQPyro2bQIWWuEPuOlL2XpDLSNtG7M2DkYmRlFG08NDDp3fNMSr6KY31UPz9bjrNL+IKEMuo6HhJq5xH4DkFRk+ZMfYAElofpNRyxzJqVDhCSdT6KJcoRGWpxRRxLExG2pwS8jDCM6jGQNmYwHIhGln2DCQ9K0dLKkWGZRhd4rBUpOSPQnx338enm2/v3v7b0do//9Pak53XrNHHM5ZgMIB1uCXMKt0gmCXqkDIO63q+jIXOXQ1brBoTh1K0ye3z43S6Odtl7Iypk/Bq/HcVE41vWY7LfGdyXP7fa5Yms/wNSmSRzQ==", 20 | "anchorsComplete": ["cal"] 21 | }, 22 | { 23 | "hashIdNode": "21287711-6d1d-11e9-a653-0117e0470c36", 24 | "proof": "eJyllE2Om0UQhrkCd2AZj7v6v7yyxBVYZWNV1w+2ZGzL/hLIcsKG7XCDkEEJiA0SyjL3sJTDUJ5BEZlhEYnV119319NV9b7dP71d8n436Q/T+/U0HU6L+fz7tJGr/fHbOa9pszvsN7tp/jzdTi8O+vvXH6du13Ran5eZ89ARiTqjNpTRpZTOrGoJO7QKgUPTWrN2uvwUizGzsXTLWNMfF8xqI6vdXvT8VYTYWwOYVQGZASjOqJY0CwBNQ26BU313F3J6Nr7bTJPeR65o+isGwFkosxC/iWFRcBH604943h8dn6wmDTH8Cz+keghwbxRGHUoP8ZfI/8QXfPp2HGnHaz3dvPx1S0O3fzJtV5ep/XF1v/Z6fzh9+OLL61fb85O7TDey+Jwqr3/ZH96c1jSLpV6/Op6XEAcOJMwRvKGdo5H2EjlJHUmhpdBzA0upGBOT1d5b55QbsyDn8ggYXIzcrbtcYt18PIY2PyI6JBaMIAlbj6Mx4QjDmlHQ6CkaROP8KXB7XrZ02cfV7QBQW+ZYmCQJVnLhYyAoqtxhCJoxE1ULKq14AZZzxUcZliRlKAzHcikEWSICV8xBK/QqCKF7D0o3jBRas6GaZWATAR72GBhYqAbM0lIU6GqDkQkacEKDbKMCqdWCVQ2lg7IG6ULi3nXRwsOSn9yZwxX9HGM9DP55tzlNz+MCSqk9hdrDImdPPKeARVPGEi0ABTYnJc5WavGqXZeaOhs4eFxMQa3KKMUHQl42ugPQG6EZxohuCvF7KNnt4FVahk6WB0ZJgyp5y0pUit5Sl+NTSd/c+/h08+Pd3X/t1v7tH2tv5LzEEZVBKAg4sHm3CHGQCQSR1A1SLt46ywG6C2RDCsc2YsJQ62C6fXbcnG7Ocnl2/NVJ5cr3XcXU/VvnfpjuhI7z/3vMXGiivwHARZK4", 25 | "anchorsComplete": ["cal"] 26 | }, 27 | { 28 | "hashIdNode": "21287712-6d1d-11e9-a653-01121760b027", 29 | "proof": "eJylVE2Om0UQ5Qq5A8t4XFXd1T9eWeIKrLKxqrq68ScNtmV/CckyYcN2uAFkUAJig4RYcg9LHIbyDIrIDItIrL5Wf1WvX733ur97v2773dxfzn9u5/lwWi2X34TJrvbHr5ZtK9PusJ928/JFuJ1fHfovX3zYut3KaXtecw3ca865MxQoGAeXUSKSNGnZOJYMiVMaSfuAkYGiFBmlFiuFsPx6gdlMttntrZ8/J6SSM9IiGdoCsdeFJA4LQCTMCRQo/3HXcnquX0/z3O87NzL/ToB1AbwA+pJgxXUF5dkH+LY/OnwYKXQg+Be8WvIWbCULqJOUh/CXzv+E5/rsvR5l17b9dPPmp2vRfv1bk+vNZWt/3Nz/e7s/nP767MnrH67PT++YTrb6lClf/7g/vDttZUGcLs1rDSHnplqx9KCtcM8NE5GOXiBADKSl6UixJ24cOyQarNS55JGz8MeAx/MawMvcLC7NRhm+Vu1Zq9AIgbgSWqi5kOYmVUFHHgLdCfaBNFp8xDCHS11LSoKYcmzETSxYTYLQCAS591ZQrY7Rmkga0C2zJR0xpvqIIQdj7agO25gFo1HFlmqEnrAkqwilF/bEVRLIeWjv0bRmM3QpHgNCM0lQo+VA5joObbUJZmyhDs+uJpQ+EtfUR7WCvXWwYmJFkpsGD0d+ehcOd/RTgvWw+fvddJpf0AqZUwmQCqxidOIxQOUeYmUagAJtOFJofrMS+9TuSwqlDXRgRXK/cjJl9oWJj11D4OpC9IiqBCVaLWgxleJTjoh++6JWsqCSxCVj6kIuqdvxsaXv7nN8uvn27u6/9Wj//E+0Jzuvq2eroQkYOmB2taRWlWEIZqEMDJFduhEBixs01LhRVgoVUtImt8+P0+nmbJdnx1+dwFded0Wh+Dct/bC+Mzku/+8xS5NZ/gbJNJCr", 30 | "anchorsComplete": ["cal"] 31 | }, 32 | { 33 | "hashIdNode": "21287713-6d1d-11e9-a653-01ba817b7c6d", 34 | "proof": "eJyllMGOI0UMhnkF3oHjZmJXlV3lnCLxCpz2EtnlKtJSSKKkd4HjwIXr8AbLDtoFcUFCe+Q9IvEw1MygFTvDYSVOLVW3P/f/+3f98HZdD/u5fTP/uZ3n43m1XH4dJ786nL5c1q1O++Nh2s/Ll/F2/vbYfv38/dHtVs/byzrVxlSNkubAnIgsJa+ha2J3jCSBLGQiTVAaigfIbt5YOYuQht/uMJvJN/uDt8tnAUPJGeOCHX2B2GShTHEBaFowW67s7+5Lzi/sq2me20PlRuc/AqAsgBYQvgiwIllBef4eXw+ngY+dY4MA/8Kb8yjBWrKCsTV9jL+r/E88yfO3dtJ93bbzzXc/79Ta7vequ83d0eG0eXj3+nA8//XJp9evdpdn9386+epjVF7/dDi+OW91EYivX50u6+EgYQ3VkpQE1iSBN8ohJ1GlhADFk2JJPMRwN8/cm7ggU2+I5UPg7rJGi0E6Q6aSI1QO3ItSzBmg12puKBA6dSuZQDo1IBhUVwg1GKQnwBxVbIAsKCLnVANV9ejCilADKFJrtaC59NFBlTs0z+RsPSWWJ5IpOlkbpojVESFMHgQrD+WNsfDQNkJViEqXoJBzt9aSm+QRvWr9KRCqK4MkzzE4ltatSlXMWKN0TN0YtXUm4dbFC7bawIurF+UxNHgs+dl9OMZEPyZYj4t/3E/n+WVYIRGXCFxgNXYHa4og1GISCh1QofZBijV1YhqqJSDHUjsOsGEw0cxuY/FEXYdsiWPrhhEtoVmAklwKeuJShsqesGhPJsGjKeuwjELTMCwd4/hwpG8ecny++f5+91+PaP/yT7Qnv6zFQqs40uA4gHm4pSKm3RHcY+kYEw3regIsY0AjkFRDthAFmK3q7YvTdL65+N21M26dSFfju6sQy3jycjRre9fT8v+2WbrO+jc/rJFq", 35 | "anchorsComplete": ["cal"] 36 | }, 37 | { 38 | "hashIdNode": "21287714-6d1d-11e9-a653-0117e0470c36", 39 | "proof": "eJyllE+OXDUQxrlC7sAyPV1lu8p2r1riCqyyaZVdZfpJQ3er+yUky4QN2+EGkEEJiA0SypJ7tMRhqJ5BEcywiMTqvWe/+tWf77O/e7/u+91sL+c/tvN8OK2Wy2/ipFf741fLvpVpd9hPu3n5It7Orw72yxcfl263ctqe16mnZi2IlF4tV21FiUrvZiPWgpkROmRjTlbk8kEjhNRH1zJS5fjrBbOZdLPbq50/DxhKzpgWrKgLRKsLYYoLQMwGKUOP/OEu5PS8fT3Ns91HbmT+PQDWBdACwpcBVlRXUJ59xPf90fFxcDQI8A98U/YQ7CULNG4mD/GXyP/EU332vh1l17d2unnz07U0u/6ty/XmsrQ/bu733u4Ppz8/e/L6h+vz07tKJ119Spevf9wf3p22sgjEl+A1B8rDyCymCJZNMDYMFtiLTxQDjM4lhEA1NstEOtBnHJsSJwCoj4DYYqiDIVPJEToHHkUo5gyO6k0bVgiDRiuZoA4yIGiaVSD00CA9AuYotTnI7YDIOfVAXTRqZXHhAwh69b1g0zo8gwgPMM2k3EZK/KDC43lNUakZNsd2IsGkoWLnmsAYC2tFKFbcb6MGgZxHM0vaalbF3sZjIHQVhpo0x6BYbLReu2DGHuvANBqj2GCqbKNqQesGWlTUveuiwcOWn96ZwxX9FGM9DP5+N53mF2GFRFwicIFVSl64y1vJVa4UBqBAH06KPQ1i8q5rQI6lD3SwG6BVyayNyF9UvO0aI1UfhCVsLUBJ6udQE5fiXY6ERUZqNWhswuIjo2ASfKQux78lfXfv49PNt3dn/61b++e/rT3peV1bsI7uBkUHZp+W1NpkKIJqLANjIh/dSIDFBRpuxB5yC7ECc+ty+/w4nW7Oerl2/NaJdOX/XYVY/MlLT2Y7lePy/6ZZqszyF6nIkPs=", 40 | "anchorsComplete": ["cal"] 41 | }, 42 | { 43 | "hashIdNode": "21287715-6d1d-11e9-a653-01a98661d85f", 44 | "proof": "eJylVE2Om0UQ5QrcgWU8ruqf6i6vLHEFVtlYVV3d2JKxLftLgOXAhu1wg5BBCYgNEsqSe1jiMJQ9KCIzLCKx+lrd33tV9d7r/uHtsu13U/9m+nM9TYfTYj7/Om7sZn/8ct7Wstkd9pvdNH8Z76dvD/3Xz99v3a/ltD4vay6gBDAkGlOukhrG2NFi5dKAmQp1jlVKKVk70CDmUaFoMwgp/nahWW1stdtbP38WMNRSMM/I0GaInWdCOc4AhSsRWs3j3RVyeqFfbaapPyBXMv0RAHkGeQbhiwCLzAuoz9/Tt/3R6eOg2CHAv+jVyCHYahEfRLs8pr8g/5M+8/O3epRdW/fT3Xc/b0X79vcm29Vla39cPZy93h9Of33y6e2r7fnZtdONLT5mytuf9oc3p7XMQqbbV8fzEqxpCElQWwRlSxF9EVTFXOasDTolI6pVcy9YsAbsoTZJSpLlKWHUGKJmwkQ4hC6edVWwKjE3qFlUohiFkt3jLF6eayNuHEYgifqEMJfaoYUGIKVee7BQKRZsYzSPRg6h1gqK0RAackyUJVP3iZFTtw8Jt+dlJ0YKULVqYqdg46rq1QsN6W5crzC8d6FYrQQC4VYoCQ/XNEN5qmEzIeBkJQbD2oc2buJateiYNJRQ+qDM1Adbxd6662HimpCbBo87fHYNhzv6McF6DP5xtzlNL8MCc6YagSosUjJsKQLnHhPnMDwR0IYzxZZGpoyVOKCP2wY6sWJQlkKmOfvCBN2kGDNr7z2hqouXXDS05LnwKUfCKiMpB4sqJGVoDl0CJ3AV0wctvnnI8enu++vdf+3R/uWfaG/svGQNvaEJGDphcbWEWWW4t+a3f2BM2aUbCbDWwUMtt1A0RAYibXL/4rg53Z3t8uz4qxPzjf93E2L1L829WN+ZHOf/t8zcZJK/AaZejzM=", 45 | "anchorsComplete": ["cal"] 46 | }, 47 | { 48 | "hashIdNode": "21287716-6d1d-11e9-a653-016e742c718a", 49 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1d1eWeIXWGVjVXVV4ysNtmXfhGQZ2LAd/gAyKAGxQUIs+Q9L+ZiUZ1CUZLKIxOLqXnXfc+rUqdP90+t13+9mezb/u53nw2m1XH4fJ73aH79d9i1Pu8N+2s3Lp/F2fn6wP75+t3S75dP2vIYxtJCoZtRihh0zRgLDETQhCieiBBmrXh6umaRJwxatB9Ua/7zQbCbd7PZq568ChloK0oIUdYFobcGU4wKQrKTQC1b+5w5yeiLfTfNs98gNz38HwLaAvIDwTYBVbiuoj9/R9/3R6eOgaBDgPXpRcgj2WhiExB7QX5CfpM/t8Ws58q5v7XTzw2/XLHb9V+frzWVpf9zc773cH05vvvjyxS/X50d3SiddfU6XL37dH16dtrwImS7gNUZsPRXSTNpLK24eD+nGyWrHhpBDjMkwWA4WIrByH2hksZJT1g8Jj+d1lBiiZMJEOJgMNZoIaOWYO9TMwpGVQsm5QGbt0mqn1lsYgTjKA8JcqkEPHYBLzVawaKgUC/Yxeq6cQ6i1gmBUBJccE2XOZESILZk+aNmoIQWoUiU1p2jaqohXLzTYfHBWYbh2pli1BAJuvVDiNtzTDOWBQujKBC1piUGxmvvXOqMrjI5JQwjZBuVGNppWtG7uh7J7Qj40+Fjho7tw+EQ/J1gfg3/eTaf5aVhhzlQjUIVVSoo9RWjZYmo5DECGPpwp9jQy+RmiFtDb9dE6sWCQxp4Jydk/lNGHFGNuYmYJRdy85KahJqrVuxzJozWStKBRmLgM8bRwaAncxfSBxFf3OT7d/Hh39l96tH//L9qTntdNgnVUBkUn9DgityY8fLaqsQ6MKbt1IwHWOtoQzT0UCbEBkXS+fXKcTjdnvVw7fuvEfOX/XYVY/U1LL2Y75ePy/5ZZKs/8Fu8Mj9I=", 50 | "anchorsComplete": ["cal"] 51 | }, 52 | { 53 | "hashIdNode": "21287717-6d1d-11e9-a653-0160de72b4a0", 54 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1d1eWeIXWGVjVVdVY0vGtuybQJYTNmyHPwgZlIDYIKEs8x+W8jGUZ1BEZlhEYnF1r7rvOX2qzqn+6e1S9rvJfpjer6fpcFrM59/HjV7tj9/OZc2b3WG/2U3z5/F2enGw37/+uHS75tP6vOwJWoTeZVQoKUklScQYujRlyEQJtYRQkCSWOjLl1pvlnG3UGA3/uNCsNrra7dXOXwUMtRQsM1LUGaK1GVOOM0ACtRJ6Ynh3Bzk9699tpsnukSue/gqAbQZ5BuGbAIvcFlCffqSX/dHp46BoEOBf9F3JISi1MHTqxg/pL8j/pM/t6dt+5J2s7XTz8tctd9v+KbxdXZb2x9X93uv94fThiy+vX23PT+6UbnTxOVVe/7I/vDmteRYyXb86npcjcMQeeyWKWpuZjAyQFIlTiYhlDE4jkpj00QdnjUmyBhzJ2uj1U8LteSmtdMu9lNyaokR/pLhJ2olLVmHNGQoVCGzGYVgopiJurEBpQR4pzKUaSBAALjWbF6ihUiwoY0iunEOotUJHPwoEW0yUOZMRIbZk+kihUUMKUHvtqTlF01Z7H4EKDTY3zioMHMwUq6eMgJsUStyG99S1P1IIokzQkpYYFKsNj6kwusLomDQ6IdvwjJKNphVNDLQqa2Vy0+Chwid34XBHPydYD8E/7zan6XlYYM5UI1CFRXI7JUVo2WJqOQxABhnOFCVdhgcrtYBergx04u6D1riQ9pz9Qxmltxh9xswsYe/evORNQ01Uq1c5ElYeqbegsbP7PHoObm5L4F1Mn0h8c5/j082Pd7P/2qP92z/R3uh52XowQR9y9YT14t3i1joP91Y11oExZW/dSIC1Dk+gZgmlh9iAqAvfPjtuTjdnvVw7fuvEfOX/XYVY/U1zP8x2ysf5/z1mrjzx39pvkXE=", 55 | "anchorsComplete": ["cal"] 56 | }, 57 | { 58 | "hashIdNode": "21287718-6d1d-11e9-a653-01cf0c91e2f7", 59 | "proof": "eJyllE2OJEUMhbkCd2A51WXHv2vVEldgNZuSww5TKTVVpcqcAZYDG7bNDWAazYDYICGW3KMkDoOrG42YbhYjsUhlKjP9xfPzi/ju7bUc9sv4avlztyzHebNefxknvTqcPl/Ljqf98TDtl/XLeLd8fRy/fPru1d2O59352uLoPVTINdqIaGIoFcAiJ6smiRqh1VCoi7DUXjTHkdQipSSQ+dcLZjvpdn/Qcf4kYGi1YlsVRV0hDlpxyXEFKAZCOILVP+5L5hf9i2lZxkPllpffAyCtIK8gfBZgk2kD7fk7vBxOjo9W4oAA/8J3LfmCb5Whlz74Mf5S+Z/4TM/f9hPvZTfm229+uuE+bn4TvtleXh1O24dvrw/H+a+PPn71w8352b3SSTcf0uWrHw/HN/OOVyGXS/E1mYZYYwNJPRq6bI2tmwgON1O4M48G0PniebRmjQtZDq21zpH5CVCo9pF7rZlIUaJfUmNt2gvXrMKaM9RSIfAYHGyEOlRESAUqBXkfeDpf59oGSBAAri2PilVDK7F6Vya5cQ4XMdDRlwJBiqlkzmWUgkhp6BOFoxCWAK23nsgRpNR6t1BqMR7ugDdsaMwlNvWQAZPUkpjMPXXtTxSCKBegpDUGxTasCwmjK4xek6wX5GElUxlG2nDIAG3K6l760OCxwmf34fCJfkiwHhd/v5/m5WXYYM6lRSgNNin5EFIEyiMmysEAGcScFCVZLhlboYDeru8zB3cMnbgW7Tn7gzJKpxgz9TFGQt+Z0JKbhppKa96lJWxsqVPQ2NnnbD0HHy4lcBfTexLfPOR4vv32fu+/9mj//E+0J/U89jAElUHRgdXdYqLO5rNVT6ZhTNmtswTYmpF1zRJqD5GglC589+I0zbdnvRw7furEfOX/XYXY/F7WvtjYK5/W/3eZtfLCfwNO0ZMN", 60 | "anchorsComplete": ["cal"] 61 | }, 62 | { 63 | "hashIdNode": "21287719-6d1d-11e9-a653-016e1c5cab92", 64 | "proof": "eJyllE+OXDUQxrkCd2CZnnb5T9nVq5G4AqtsWuUqF/2kprvV/RLIcsKG7XCDkEEJiA0SyjL3aCmHoWYGRWSGRSRW78n293PV58/+6e2l7Hfz+GF+v5nnw2m1XH6fJr3YH79dyoan3WE/7ebl83QzvziM37/+OHSz4dPmfDk65Qa9Fi6xVcwmwZg4WEQMqXUrZjkiYU0pWeutpgJJs8YeyXL/4xaznnS92+s4fxXBKRVogQq6ABi0YCxpEQAHSBHuFN/dSU7P+nfTPI975Zrnv2JwXSiLEL+JYVVoFdrTj3jZHx2fDNMIMfwL3xVdAtIqh4598EP8rfI/8YWevu1H3slmnK5f/rrlPrZ/Cm/Xt0P74/p+7vX+cPrwxZdXr7bnJ3eVTrr6nC6vftkf3pw2vIgFr14dz5fMNeZCqdZmKMUQK3aRmKUra+eeiJpENACJVrrlWlrVEsPg2Fp4BKwsIVDqAITE2bvPwKQjJknBpBU0C9DSyCU1Yu4NwOvt2mo00/QpcOtZKKUkyo4z1SxBqorIkDaSDKw5lZG5tebRYB5Y3HrwuZAJ0SX2GIgEGIOnpmcyKaTUevdoVTQefnCjBQNjxtS0RgxM4hlkcg9GCfVRy0GUMVDWmqL63taFhKGCJNdk6wg8DAvhMNIGQ0bQ5vY2Rj+08LDCJ3fh8BP9nGA9FP+8m07z87iCUrClgC2sclaQnAKVkTKV6P5zEHNSkmwFCzSkCN6uGDi4Q+zEFbWX4j/KIJ1SKtTHGBl6d/OymwaasTXv0jI09mtHUVNn5Gq9RM8H5eAu5k9KfHOf49P1j3d3/7VH+7d/oj3p+ZJ6HALKQcGB1d1ios6mEFRTM0i5uHWWPUPNyLoWibXHRAE9uHzz7Didrs96++z4q5PKha+7iKn5F5e+2dgpH5f/d5ul8sx/A37kkaM=", 65 | "anchorsComplete": ["cal"] 66 | }, 67 | { 68 | "hashIdNode": "2128771a-6d1d-11e9-a653-01bab60e6173", 69 | "proof": "eJylVMuOW0UQ5RfyDyzjcVc/qru8ssQvsMrGqkc3vtJgW/ZNSJYJG7bDH0AGJSA2SIgl/2GJj6E8gyIywyISq3vV3ed0nVOn+rv3a93v5v5y/nM7z4fTarn8Jk12tT9+tdQtT7vDftrNyxfpdn516L988WHpdsun7XmNJh1yTShJpUsclIVyTsKsMgKKcQczwRoildyMOWORWjCpFki/Xmg2k212e+vnzyPEVivwAg1sAdBpwVjSIoCwYOgINf1xBzk9l6+nee73yA3Pv8cAtAhlEeKXMawKrUJ79oFe90enTwNTDzH8i14MHQLaKgdB6fyQ/oL8T/pCz97LkXe67aebNz9ds/Tr35SvN5el/XFzv/d2fzj99dmT1z9cn5/eVTrZ6lNUvv5xf3h32vIiFryA163kEKOiO8wdk0FqWUmoa8ixWXJKZUR3twVvRx/eBBVsQA2GdPqY8HheV9YQKAkAIXF29RmYrMekKQxtBccI0FLPJTVilgbg9Yq1GsewxxX2Ukqi7HTDLGvQaqratfWkHWtOpWdurUVEdgnFrQffC5kQHTIeEyIBxtCkSaahhYyayIhYcXD3xvUWBgxmTM1qxMCkFTPTcE9LqI8kBzXGQNlqiuZ3D1FShgqaHJOHIHAfWAj7IGvQtQfzzFpj9KaFhxU+vQuHd/RTgvUQ/P1uOs0v4gpKwZYCtrDK2UBzClR6ylSi+89BhzMlzaNggYYUweXqACcWiEJcfQhL8R9jUKGUCknvPYOIm5fdNLCMrbnKkaHx8BGN5iOKXIeU2DlSDu5i/qjEd/c5Pt18ezf7bz3aP/8T7cnOa5LYFYyDgRNWd4uJhIdBMEttQMrFrRvZM9QGDbGisUpMFBBF+fb5cTrdnO3y7Pirk8qVn7uKqfkXl35Z3xkfl//3mqXxzH8D55+SIw==", 70 | "anchorsComplete": ["cal"] 71 | }, 72 | { 73 | "hashIdNode": "2128771b-6d1d-11e9-a653-01f9221b3cd1", 74 | "proof": "eJylVD2OW0cMzhVyh5TWipwfzlCVgFwhlRuBw+FEAhRJkJ6duFynSbu5geMN7ARpAgQufQ8BPkyo3cCId1MYSPXeG873kfz4Pf70dqn73WQ/TO/X03Q4Lebz7+OmX+2P3851LZvdYb/ZTfPn8XZ6cbDfv/54dLuW0/q8hKwNU8lVE2BMRFEq5w6kHEOrKioxm1QsEAxrUkjKSQbWnIs1/ONCs9r01W7f7fxVwFBLwTajjn2GaDwTynEGODgEbFE7vruDnJ617zbTZPfIlUx/BUCeQZ5B+CbAIvMC6tOP9Lo/On0cFA0C/Iu+dXIIai0CjZrJQ/oL8j/pMz99246y07Wdbl7+upVm2z9VtqvL0f64uo+93h9OH7748vrV9vzkrtJNX3xOl9e/7A9vTmuZhUzXr47n5bAsSKlKwSAE1PogZuNgzQPFa2fVBh6TyC68eaNUUhOWzCW2Twm352UcKQbXw79LKinWqgCawWovoIjIrFa5kCVsGitGTqRYklnm3tMjQss5+x1kGh5W0NJV1bRaVPMcboQktdZAJGKUXXr0GCQmcsh4TEiMFKC22hIP9axcWxveFg0xH5xVGDhEKHrNgUBYCyXh4ZpmKI80BO0uHadeYuieezRldfFQo2PSaIRigzKTDe4VTQ167dKrkA8NHlb45M4cPtHPMdZD8M+7zWl6HhaYM9UIVGGRUkdNEThbTJzDABTQ4UxR08iUsRIH9HbV/6AiDUNjKdRbzv7SBbVxjJmb2WVqzcVLLhr2RLV6lyNhlZEahx6bO6WMloNJ4ASu4qcjfXPv49PNj3f//mu39m//WHvTz0tuwRS7QEcnLK6WMDcZHaH3WIdvg+zSDd8LtQ4erWcNpYXIQNRUbp8dN6ebc7+sHd86MV/5vasQqz9p7sls1+U4/79p5l0m+RtBlJBW", 75 | "anchorsComplete": ["cal"] 76 | }, 77 | { 78 | "hashIdNode": "2128771c-6d1d-11e9-a653-019a9cbc038a", 79 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKof1V1eWeIXWGVjVVd14ysNtmXfhGQZ2LAd/gAyKAGxQUIs+Q9L+ZiUZ1CUZLKIxOre293nVNU55/ZPr9e638392fzvdp4Pp9Vy+X2c7Gp//HapW5l2h/20m5dP4+38/ND/+Prd0u1WTtvzWqEL10xxlBYzEg6pOWeh1CAPMDCKsamMTqbEqZhplCiFMAZf+PNCs5lss9tbP38VMNRSUBdkaAvEzguhHBeALKxNIVb55w5yetK+m+a53yM3Mv8d/NAC8gLCNwFWmVdQH7+j1/3R6eOg2CHAe/TNyCGotQg0av0B/QX5SfrMj1+3o+x02083P/x2La1f/6Vyvbks7Y+b+72X+8PpzRdfvvjl+vzortPJVp8z5Ytf94dXp60sQqYLeG2FeoYURumDsUgTsYg5j4Q5DlZkhaxj5MBjKEO0xiFLEUvCRPiAMI4Ug+vh3yWVFGtVAM3QqxVQRGTWXtnLJmwaK0ZOpFhS75nN0gPC7sb7GWQavq2gxVS1a+1Ru9eIuSeptQYikU7ZpUffg+TtOWQ8JCRGClBbbYmHelWurY1AhYZ0N65XGJ44oeg9BwJXr5CPO1zTDOVDwuN5DWpCwMlKDOa1R1NWQbciOiaNRih9UGZyja1i1w5WTawKuWnwcYeP7sLhjn5OsD4G/7ybTvPTsHIPqUagCquUDDVF4Nxj4hwGoIAOZ4qaRqaMlTigj6sDnbhhaOz/krWc/cUEtXGMmVvvF9eai5dcNLREtfqUHpYqI3kyLDYhKaPl0CVwAlfxQ0tf3ef4dPPj3b//0qP9+3/Rnuy85ha6ogkYOmFxtYS5yTAEs1gHxpRdupEAax08mmUNpYXIQOQ3wu2T43S6Odvl2vFbJ+YrP3cVYvUnLb1Y35kcl/+3zNJklrfOsJGS", 80 | "anchorsComplete": ["cal"] 81 | }, 82 | { 83 | "hashIdNode": "2128771d-6d1d-11e9-a653-015e9e9fda78", 84 | "proof": "eJylk79uFEEMxl+GMpezx/P3qki8AlWak2fs4VY6bk+3mwBloKENLVVIUAKiQUKUeY97G+YuKCIRRSSa3dXMfj/bn+0PN0elX436ZrxdjON6mE2nr6mTw37zcloW3K3Wfbcap6d0Nb5d67fn90dXCx4W2yPQYkpNaDE7VE0Zaw5SUyakZIUjWPGcojDHasiG4JSBDPoIEJP7vsPMO5mvetHtM4MmhoAy8dIeiJom7B1NAJ0mTVU4xF97yXCSX3XjqHfKOY8/DWCagJuAeWFg5tIM4vE9vvSbhqfqScHAX/gsvkmwxMCQfVZ+jN8p/4l36fgmb3hVFjqcv/uy5KzLH4WX891Rv5nf3V326+HT2cVye7DPs5PZU2o8+9yvr4cFT4zzZxeb7VGqkGP2nqREKtWJarGEGSwV61v+QYhjjErNagK1ubpc0XmoLhrvHwKXDRg5hKCVoLDDGB14LR6S98U3hlJRCbUIqWsQw1C9y77FwGha9u4x8GDvUyvvKR4/Fn9cdcN4ambYYkWCNhszawVbhZCckk3OVECGUhupFVydbzn71MaIYqnYwBlNThy8ZOfahzCWnIhcyqrapjMbiFZSRLE+RixULUauNicjlNlzqNkZZZMsMHv7IMXru5YO5+/3a3DZuvz1T5c7aV5mowWFQbABg0TklDJXQRChWJGsa22pFprRNdUsrpiQDSXwPhe+Otl0w/lWdhvYFpDcYfvv0FBsbz9twXQlvJn+b5ip8Mi/AcIURXQ=", 85 | "anchorsComplete": ["cal"] 86 | }, 87 | { 88 | "hashIdNode": "2128771e-6d1d-11e9-a653-010d18dca7c1", 89 | "proof": "eJylk79uE0EQxl+GMo5ndnb2j6tIvAJVGmt2ZxefZHyW7xKgDDS0oaWCBCUgGiREyXv4bVjbKIJAEYnqTrv3/eabb27e3J7kfjWWF+OPxTiuh9l0+pw6Pe43T6d5Id1q3XercXpO1+PLdfn8+O7oeiHDYntilTxRsI4LZrE2xaJKNQumkIgAvHDUWCvaKMUTKJuAEFNmDwDxyw4z73S+6rVsHxk0wXssE6eoE8QSJ+KYJoCgGDSLz/h9LxnO0rNuHMtBOZfxmwGME+AJmCcGZhxnEE7v8LnfNDxVRwUM/IZP6poEc/ACyaUi9/E75T/xHE9v00ZWeVGGy1cfl5LK8muW5Xx31G/mh7urfj28u3i/3B7tfXY6e0iPFx/69c2wkIlhtxOfRDItag7MMQiiFs/GIOXovW/ea6k+QbCtj6SQvRJ6TtaDa7aLy+FvYJAmLZUgC2MIDK5kB9G57ChooVzU16xUmB0Ygeo4OZKAwTT3fB94tM+ptfeQjO+L3666YTw3M2y1AoELMLNWMVuCyIVsZFMBBXJtJMq2smueXTTYvOaKDZzQpCjeaWJuLyqYUyTimEopFlMyLR6NAdW6EDBTtRikth/WKCVx4mtiU8RECyLO/mHx5jDS4fL1fg2u2pQ//Zpypy3LZEpGlTa/BvQaUGJMUrVNVClUJMtBWzFoQddYk3I2PhmK4FzKcn226YbLre42sC0g8XH77thQaE83bcXKSmUz/d8yU5VRfgJ8/UPB", 90 | "anchorsComplete": ["cal"] 91 | }, 92 | { 93 | "hashIdNode": "21153d30-6d1d-11e9-a5a8-014c527fe564", 94 | "proof": "eJylVE2OW0UQ5grcgWU8rqr+La9G4gqssrGquqrxkwbbsl8CWU7YsB1uEDIoAbFBQiy5hyUOQ9uDIpjZRMrb9FN111dV3/d1//D+uu22s383/7WZ5/1xtVx+Gya72h2+XraNTNv9btrOy5fhfn6191+//BC638hxc7pGI2Fn0pQxRvBqVITQ3RBjL4AVIZkCUCbpxQrUMhYX5poV5LczzHqy9XZnfvqCEFOwAItsaAtE54UkqQvA2BKV7inHPy8pxxf6zTTP/pC5lvkPAuQFpAXQVwSrxCuozz/At91hwMdgvWn6L7xq9AFvBtxEuupj+HPmY3hcAaygPH+vB9m2jR/vXv98I+o3vze5WZ9Du8P6Ye/tbn/8+7PPb9/cnJ5dOp1s9TFT3v602787bmRBKd++OZyuO1BgHSSjV9BEwZuEmqNx0igVAKqWsVVCN+nYrWJGy8klWbDyFND4opQKUxu5KSQgydTNVbsWzXWUoN5FBdtQrJq1ao0LlFGnwBNAh2xkpfdGNVCJFioYS6Go4rXlboGjWcTcc6IYeXxYC0kKzqWHJ4CkHEwaYrEwjklj0PPQlavWbDEjmVsP3Djm0TY7EpIH4lTIe386steM0DtVMq29Yepk0apo80LYtGULgkwc2ugReoTOxVvosToj/x9wKHoxx1D0Y4z1OPnH7XScX9IKUxpUQ66witGwxQCcPERO1AEFWh8qhhZ7yglrZsIcauuDOVEcFEnJpimNHxtCDcpCYnX3iKoEdRik4uCqVjzPgVV6VCYLKllKH05yIY4g8sh07x58fLz7/nL33w5r//KvtScb4jQKxUCzOGvUgD7Is6H5iCQJ1CSdyeZUhybDXXF0kfrwQSzJEun9i8N0vDu187MzXp2Qriimq8pXBLwctXxrclh+apWlySz/AKXVkcs=", 95 | "anchorsComplete": ["cal"] 96 | }, 97 | { 98 | "hashIdNode": "21153d31-6d1d-11e9-a5a8-01ab1e3c2bea", 99 | "proof": "eJylVD2OW0cMzhV8h5TWiuT8kaoWyBVSuRHI4Uz0gI0kSM9OUtpu3G5ukHgDO0GaAIHL3ENADpORNjDi3cZAXjMPnOFH8vu+mTfvr+tuO7fv578287w/rpbL78LkV7vDN8u60Wm7303befki3M0/7NtvX30M3W30uDldk5NKE7KUMUZo7FSUsDVHjL0AMkJyA6BM2osX4DKWpiKcDfT3M8x68vV25+30JSGm4AEX2dEXiE0WmpQXgGrYQiVr+uGScnxu307z3O4z1zr/SYCygLQA+ppglWQF/OwjfN0dBnwM3qsl+A+8WWwD3h2kqnazh/DnzIfwuAJYQXn23g66rZt2vH31y41au/mj6s36HNod1vd7b3f7499fPHn5083p6aXTyVefM+XLn3f7d8eNLijlc/J18BGFljXWnlLsXLmSgFIvGmoB48ZCzIzYq1fgHBXqKISWLKp8Cng4XbtclDIVGqcthQSkmbo3s27FModWqXc1xToUY/fKXqVAKY0LPAJskJ289F6JA5XogcFFC0XTxjV3DxLdI+aeE8Uo40MupCk0KT08AiST4FoRi4dxTKuAVQ3MwsbZY0by5j1IlZhH29KQkFogSYVa749HbpwReicmN+6DnU4endVqK4TVavagKCShjh6hR+hSWg09chOUh6I8vZhjKPo5xnqY/ON2Os4vaIUpDaohM6xidKwxgKQWoiTqwxFQu+cUauwpJ+QshDlw7YO54ZZBkZbsltL48SHUoCwksdZaRDMCji6Mg6thjPMcyNqjCXkwzVq6JWpKEkE1x09afHfv4+Pt68vdfzus/eu/1p58iFMpFAfL2sSiBWyDPB+aj0jSQFXTmWxJPDQZ7oqji9SHD2JJnsjunh+m4+2pnp+d8eqEdEUxXbFcEchy1Gpb18Py/1ZZus76D4Oqk64=", 100 | "anchorsComplete": ["cal"] 101 | }, 102 | { 103 | "hashIdNode": "21156440-6d1d-11e9-a5a8-0101f7a3589e", 104 | "proof": "eJylVMGOG0UQ5Rf4B47xuqq6q7vLp5X4BU65WNVd1XgkY1v2JMBx4cJ1+YOQRQmICxLiyH9Y4mNo76Io2b1EylxmVDP16r1Xb/qnt9dtv5v9u/mfzTwfTqvl8tsw2dX++PWybXTaHfbTbl6+DHfz9wf//ct3pbuNnjbn62Ck4kKVE8YIXoyyErobYuwZsCCwVQBKpD1bhpLHzVWkpAr6xwVmPdl6tzc/f0GInAbOIhnaAtFloaxlAQjYswYu4n/ft5xe1G+mefaHzrXOfxGgLIAXQF8RrFhWUJ6/g2/744CPwXqr/D58rdEHvBlIU+21Poa/dD6GxxXACvLzt/Wou7bx0+0Pv261+vbPptv1pbQ/rh/evd4fTv9+9vnNq+352T3TyVYfo/Lml/3hzWmjC+J08+p4vgYOpQgi5EHSOADllpNGl9wIgmRE7TEmMojmYjG2pKReh6pGJX4IuD1fa+xu4zPV0TMWQ84ojRyrDYIVM5aisWbvDUMmkMqonjWXzq0xP2HokIws98u4QDlaKGCimWJVLy11CxLHREw9McUo48KSSTkMET08AaQqwbQhZhsEVJtAbXqxodSSLCYkc+tBmsRkXsWRkDyQcCbv/QmgeUkIvVMhq2Xo4k4WrWhtnglbbcmCopCENjhCj9Alews9FheUxx4+uw/H2OjHBOtx88+76TS/pBUypxIgFVjFaNhiAGEPUZg6oELrlji02DkxliSEKZTWh3NacVikOVllHg+mQ4KEwFLdPWKtBCWaFBxelYIXHVhGSqqQhapJR5aYXEkijBh8mJE3Dzk+3f54/++/HtH+7f9oTzaW0yhkg5rUpcYa0Id5NnY+KqyBmvLFbOEydlI5xMGC+8hBzGxM9e7FcTrdntvl2BmnTuArinxV5GokbTlm+c70uPzUKUvTWf8D162QNg==", 105 | "anchorsComplete": ["cal"] 106 | }, 107 | { 108 | "hashIdNode": "21296170-6d1d-11e9-a5a8-015e0f0e5b51", 109 | "proof": "eJylVEGOXEUM5QrcgWV6uuwqV5V7NRJXYJVNyy676JaG7lF3J8AysGE73AAyKAGxQUJZco+ROAzuGRSRmU0k/uLry19+fn7vVf3w9nLsdyf/5vTX5nS6Pq6Wy6/z1i72hy+XYyPb3fV+uzstX+bb07fX/tvn70u3Gzlu7i5rT9VqNzZtCXo3dFcdOVGFkjM2zkLNcWjVMkS1eqbWBxgRjJZ+P8Ost7be7c3vPkNArtDSohrYAsB5ISR9kYA8zeSkBO/uW44v9Kvt6eQPnWs5/YkJeJFokfALTCviVerP38OP/SHgS7Y5lP4Lr1o84M0SD5Gp+hj+3PkYHlYprVJ7/lYPshsbP95898uVqF/9MeRqfS7tD+uHf6/318e/P/n01U9Xd8/umW5t9TFbvvp5f/3muJEFUj03XzrllpCyG87EVTk1ndJI4lVqqW1mgdposHdJDnnirBDKZyNx6U8ApUw3K0WkollgOwEPdFBDAIUWdkrR5nNAbpg4WIk3aX3SGEQfAh6CYUQBrc05sIfzxXJPxtKwqHgfdVrmEhOhzkpYCscDvaHEUhz0nwCicjYZAM2CgMjgpENy79y1VysV0Nxm5sGlmis7hLSekamhz/kE0LxXSHNiR9Mee9FEK9ZFhzeEoaNaqMjIeQTHNEua3HzkWboz8GMNn92HIxz9mGA9bv5xtz2eXuIKiGrPKU7SqhSDUXJi8lyYwmiQNKZVyqNMqgS9MkLNfcxQThRCImnVlCg+TGIFzplY3b2AKqZejDuEVr3DeQ/oMosyWlap0qYSuiCXFDEoH1B885Dj483392f/dUT713+jvbUwZ2BulrSKsxbN4CGehedRIck4hM5iM/XwRCmXYEEzclAaGaHevjhsjzd343ztxK2T6QILXXS+iKQtY5bvTA7L/ztlaXKSfwB5wJEv", 110 | "anchorsComplete": ["cal"] 111 | }, 112 | { 113 | "hashIdNode": "21296171-6d1d-11e9-a5a8-01b1b6a63748", 114 | "proof": "eJylVE2Om0UQ5QrcgWU87urqn2qvRuIKrLKxqquq8ScZ27K/BFgObNgONwgZlIDYICGW3MMSh6E8g6JkZhMpu/6qv3r96r3X/dPba9nvZvtu/mczz4fTarn8Fie92h+/XsqGp91hP+3m5Uu8m78/2O9fvivdbfi0OV8nSd16ZCZpVpt20pxJxGxgI6gFgoRqpSQjvnzkEWOSIUojtYJ/XGDWk653e7XzFxFiK1BhURR0AWBtwZlpEaBDL1ywJvr7vuX0on8zzbM9dK55/isGaIuQFyF+FcMqt1Wg5+/gZX90+IQ6pOfwHnzvyRxeNTRhHr0/hr90PoaHVQirUJ+/7UfeycZOtz/8uuVu2z+Ft+tLaX9cP+y93h9O/372+c2r7fnZPdNJVx8z5c0v+8Ob04YXMZebV8fzde3+X40wmIwTBQy1cu9YEKS31iFZ8gXH3oOM0iBLh5KENdWCEZ8ARosjYNShlDuXVjWylcCQIvaCRiTg6BiwABUd5uyQBKWlKtDCh4BbB3TjO+RKLQ4CLUEwlhxiCd6aDXIeoYTKBrV6bDADh5JwCBElk/GUYW+oLABVsXrGpIUujESNujNKBaKaetCcUlHrzcClNYwt12jjKaAaeQbHiBQ9qUPA06hJibuYSytdiiJDiw1dwhxGCqNVExyJrEF7PPKz+3C4ox8TrMfNP++m0/wyrlyXQhgKhVVKCpIwtGyYWnZ/XCIZWjJKGrlkN6JFKG7DAHL7wSXiWrTn7AvlSxQQc+tmlqD3GCip30PXiggucwDxSL1FRfec6+g5GseWAnNJH1B885Dj0+2P93f/tUf7t/+jPambIxGrBs+rte5ugrl4qkheyYxROF/Ebpnck54xOYs8jCTVrDn2uxfH6XR7lsuz468O5quY8hW1qxja0s+ynfJx+amnLJVn/g+EcY//", 115 | "anchorsComplete": ["cal"] 116 | }, 117 | { 118 | "hashIdNode": "21296172-6d1d-11e9-a5a8-010e5a5f3dca", 119 | "proof": "eJylVEGOXEUM5QrcgWV62naVq1y9GokrsMqm5bJddEtD96i7E2AZ2LAdbgAZlIDYIKEsucdIHIbqGRSRmU0kdl/+5VfP7z3XD28vbb87xTenvzan0/VxtVx+nbZ+sT98ubSNbnfX++3utHyZbk/fXsdvn78v3W70uLm75JY4Wq01GAQE82AZkpHU1KpzlgqFSxmlx4BRgbKKDmniIoTy+xlmvfX1bu9x9xkhtYKVFsXRF4jRFsoqC0AIVh7JTd/dtxxf9K+2p1M8dK719CcBtgXwAugLghW3Fcjz9/C2P0z4nHxYZ/gPfO85Jrw7NFMdvT+GP3c+hscVwArq87f9oDvbxPHmu1+utMfVH6ZX63Npf1g//Hu9vz7+/cmnr366unt2z3Trq4+Z8tXP++s3x40uiMu5+TLXlgFya9qrWRbtEAQ9K0UXczGDLk7SPEGTQFbNQYLdK6Ve8EPAw90lBQ1I5MOFu5ZWnTQKKObz+RQihqmkBKmgFB+BHZNYsparYYMnDInFOnKVRkPQC1iiwkAFZitPRjygQNXAWi33xKhQchomIjlsPGXYW3I1xOqpkqo16KZJZnr6ZJQLkoeP1Cal4tFb4JQ2EjWuFOMpoIcUhDFIyLsMQx7k2aeWFpXQuhVPio1asjGpjwyj1bA0skTD9njkZ/fhmI5+TLAeN/+42x5PL2k1dSmSoAiscna0PP3jSLnx9GdKZMMLJ5ubVXga0QjLtGGgVO04JdJavDPPD9c5QkuJW4+IjL0TSPY2zchFBM9z4Ny+3Bt5mp5rHZ0plGa0VEv+gOKbhxwfb76/3/3XM9q//hvtrU9zjFJ16EWj9ekmxhTPPcmssCYy5bPYjeeaU+eUJwseIZYrO1O/fXHYHm/u7PzszFcn8QVlvpB2QdCW867YuR6W//eWpetJ/wFBfY98", 120 | "anchorsComplete": ["cal"] 121 | }, 122 | { 123 | "hashIdNode": "21296173-6d1d-11e9-a5a8-0195708d7979", 124 | "proof": "eJylVEGOW0UQ5QrcgWU87q7u6u7yaiSuwCobq7qqGn/J2Jb9E2A5sGE73CBkUAJig4RYcg9LHIbyDIqSmU2k7L6qf71+9d6r/unttex3s303/7OZ58NptVx+mya92h+/XsqGp91hP+3m5ct0N39/sN+/fFe62/Bpc77OYgWlY+YKpWTEnrMKDM5FNSYkwA4VkXNoFkkhVO1qhUslQoY/LjDrSde7vdr5C4hAJda0KBp1EaPRgpHbIkTCGppWqvT3fcvpRf9mmmd76Fzz/Bf4T4uAiwBfQVghrUJ7/g5e9keHz0mHkw3vwfeezeFVAwnz6P0x/KXzMXxchbAK9fnbfuSdbOx0+8OvW+62/VN4u76U9sf1w9nr/eH072ef37zanp/dM5109TFT3vyyP7w5bXgBWG5eHc/XVpqQKfcYEHLrIw/WHrWIqqI19K8gkJqI9FhAUgByB5hk5NbCh4Db8zXEGofUUVLLyYJLoyCDWu1EitWBQhlWW+5QFJW5KmETtBAbQ5SngH7aI9ZGMJoTC5KgYIASLHa0iDhCCZUt1iq5J4wcSk5DWmvZZDwZGTolZYmxaqrALBS6cGqNWm9Fc4mgpiORkMfNOll0aS2Bywg2ngKqtRLDGNBAexsScYBmbdzFqo/UpWjiSEBJhlMfOQyqJskVNIr0eORn9+FwRz8mWI+bf95Np/klrFyX0lIoLax8d6LkFAgtZUIYwSWSoQWT5IEFYysE0R2TEVv1LLhEXIt2XzxiZR+Bkm9dN7Mce4fQspKbkUtr8TKHezdyJ9DUuXAdHcEYKAfmkj+g+OYhx6fbH+93/7VH+7f/oz2pm+NZqxp6YaPubkZz8VRT8wpyAmG8iO2RcU86puwscFiTXFER+t2L43S6Pcvl2fFXJ+EVZLxqdAWBln6X7ZSPy0+9Zak88387HY/t", 125 | "anchorsComplete": ["cal"] 126 | }, 127 | { 128 | "hashIdNode": "21296174-6d1d-11e9-a5a8-01b1b6a63748", 129 | "proof": "eJyllE2OHEUQhbmC78DSPZ2RvxG9GokrsPKmFRkRSZc0dLe6ygaWtjfeDjcAD7JBbJCQl9xjJA5D9Ayy8MzGEruqLMXLfO99lW/eX8phv9j3y1+7ZTnOm/X6uzTpxeH0zVp2PO2Ph2m/rF+km+WHo/321celmx3Pu9vLLLlbj8woZI20o5aCImYjEUKrECQ0qzUb8vmljBizDFEcmWr6/SyznXS7P6jdfhkhUoWWV1VBVwBGKy6MqwAdeuWaWsYPdyPz8/7ttCx2P7nl5c8YgFahrEL8OoZNoU3AZx/l5XBy+Zx0SC/hP/K9Z3N51UDCPHp/KH+efCgPmxA2oT1730+8l53N169+ueJuV38IX23PS4fT9v7b28Nx/vuLJy9/urp9enfSSTef4/Llz4fju3nHq1jqefiSfKQPxRJCAwxVgACSUey5IaOXQCXXzBpHxWCd0iADBcqaK438SDBCgyFt1IQ5WfBoNMogbJ1IS0sooQ5rmHusWpS5KXm1xQIgR5DHgv61Q2lIcSBoDZJiLSHWYNCLQSkj1NDYoDXHJhXgUHMagojZZHwqeHJBN6EsAE1Tc8aEQhdOiIQdq9uCqKYOmlCu6pYNPFpLnkSLNh4LqqEzOEbE6KQOAadRsyJ3seaWulRNDBQpyfCjjxwGNZM0MhoBPbT89A4Ob/RzwHo4/ON+mpcXceO5VEzBS9vkrCA5BSqWMpU4gkckQ2tJkkepBbBSBG9MBmDjDh4Rt6q9FH9QdguUUqFuZhl6jwGz+n/oWSHC2Yd3N3KnqKlz5TZ6icaRcmCunzLy7p7j+fr13b//1tH+9V+0J/VyJKamwXk16t4mmIenmtBXCqcoXM5hOzLeSS8p+ynKMJTcipbYb56fpvn6Vs7Xjt86qVzEXC6QLmKgte9le+XT+v/uslZe+B9wfI/C", 130 | "anchorsComplete": ["cal"] 131 | }, 132 | { 133 | "hashIdNode": "21296175-6d1d-11e9-a5a8-0181224ebb54", 134 | "proof": "eJylVD2Om0cMzRVyh5TWikMO50eVgFwhlRuBHM5EAhRJkD47cblOk3ZzA8cb2AnSBAhS5h4CfBhTu4Fh7zYG3H3gN3zzHt/j/PJ22fa7qf80/beepsNpMZ//SBu72h+/n7e1bHaH/WY3zZ/T7fTi0P/89kPpdi2n9XlZOIMmgCFkNXGR2AJRD0al5ga1ppx6pSI5Z9YOaaRaR4GszQAj/XWBWW1stdtbP3+DAWsKmWfJgs1C6HUmLGUGoQTE2FU5/nvXcnqmP2ymqd93rmT6ByHUGfAM8DuEBdcFlKcf4Nv+6PCRbDRl+AheNXaHN4PaRIbqQ/hL50P4sABYQH76Vo+ya+t+unn5+1a0b/9usl1dSvvj6v7f6/3h9O6rr69fbc9P7phubPE5Kq9/2x/enNYyQ07Xr47npTB0jaPqsJaIG2GGQF2BDQop9OF9AQYxV8CQvIRDJSFTo5HkEWB3BzhXkGypx0i9CFUnwEmgJVXL3WLRkNQosg6pgWNPJTVgcr8fAUasWdkaNyt+Y8lAoeQStaYUGJtk6QBRA7tAja3jCAajxeAlpPop4Pa8hKB+PMYAqXuW0DW69AYDY47WRUcOhDRCCoYDidRidWax5nLJ1yOG1kvyEQ0saFpGCzzQohXR1jOGpi0ZSahYqY3EMCKMmrvPL5ZewyOGT+7C4Y5+TrAeNv+625ym57gIzKkQpAKLGC20SFC5uwjGAcG9GJbcwzg4cSipXrwtbfhoRQNqlZxMmf3DxCVUIq7ae/exKkKJ5qZaTKWEi45QZLgjaOTZkDyUsQvWCCLp09C9uc/x6ebnu91/7dH+4/9ob+y8xIaUzZdfetWoFLoPz3zvvcJCbjdfhl25eNyVKToLHr20mNkY9fbZcXO6ObfLs+OvDvEVRr4q9Qqhzv2uvjM5zr/0lrnJJO8Bo7KPOw==", 135 | "anchorsComplete": ["cal"] 136 | }, 137 | { 138 | "hashIdNode": "21298880-6d1d-11e9-a5a8-013396876a61", 139 | "proof": "eJylVDGOWzcQzRV8h5TWikPOkBxVAnKFVG6EGc4w+sBGEqRvJyltN243N0i8gZ0gTYDAZe4hIIcJtRsY8W5jIMUHPoacx/fmPfLN+3Xb72b/fv5rO8+H02q5/C5NdrU/frNsW5l2h/20m5cv0u38w8F/++pj6XYrp+15HXq3ktWMwIo7NCBIOTj0aAiggjljIKh2+aRSVlYGTt6iWU2/X2A2k212e/PzlxEi11rDIhvYAsB5ISR1ESAlzrVkyfDhruX0XL+d5tnvOzcy/xkD8CLQIsSvY1gRr0J99hG+7Y8DHpP1pvRfeFX0AW8WuIl01Yfwl86H8LAKYRXKs/d6lF3b+unm1S/Xon79R5PrzaW0P27u197uD6e/v3jy8qfr89M7ppOtPkfly5/3h3enrSwi5UvzukLl3Jy5g7qRloIFu1USip1bzh5UjUvGxNF6Z9SoDXUcRN2kfQp4PK+9WaDCQYplR0xeJXGFsSyh5YFV3LAqDHcTknZhIPRccwuUuqRHgBi5KFmjNoztQ0dIUEtF5ZyBYpMiHgIqkCopNo8dLPSGMEox8SPJAXRsR4SQvQWOwXvLqYUeh3Rz0V4gxdQhg8UeU1JDHsyQS9Wh7hFD85phRDbWaFr7COslplZFm5cITVu2JMCRU+uZQsfQuXhLHauP1D5k+PQuHMPRzwnWw+Yfd9NpfhFXQJRrCrmGFaJBwxSYfIgYvgYYXnTLlBp2yuMOZY6QU219jFYUorKUbEo0fkyGBE6JWN19jFVjqGjDVMNcK1x0QJU+HImWVLKUrhRdImMQyfgJxXf3OT7dvL67+29HtH/9N9qTndexxVQsaBZnRU3gY3hmqY4KSRp202XYTHXEXSnhYEHda8NCRlFvnx+n0825XZ6d8eokuopIV5WvYuDlOMt3Jsfl/z1laTLLP10jkCE=", 140 | "anchorsComplete": ["cal"] 141 | }, 142 | { 143 | "hashIdNode": "2129fdb0-6d1d-11e9-a5a8-013efcb8790c", 144 | "proof": "eJylVE2OY0UM5grcgeWkY7vK9ZNVJK7AajaRXS6TSCGJkjcDLBs2bJsbDNNoBsQGCbHkHpE4DJU0Gs10b0aaVb3n9/z58+fP9dPbZdvvpv7d9M96mg6nxXz+bdjYzf749bytZbM77De7af4y3E/fH/rvX74L3a/ltD4vNUINoNq8QI6xldRiEiRt1QQ4pYiWiTKmFnJxTly1dmbuXkLo+McFZrWx1W5v/fwFIVU3hVkytBlirzNhKTPA0L1pyRXa39eU0wv9ZjNN/SFzJdNfBFhnwDOgrwgWXBdQnr+Db/vjgI/BBgq/D68a+4A3g9pEXPUx/CXzMTwuABaQn7/Vo+zaup/ufvh1K9q3fzbZri6h/XH18O31/nD697PPb19tz8+uTDe2+Jgub3/ZH96c1jIjTrevjuelYcbsIVa1nnKvubt6GGcOkhJVBhyvsfZemrN4cFehoXRB5yL4IeD2vETt5G6uFhqNs5bqXhJICDp4kGnxQYiiIGalLBCS5tJG9aLu/IRhpJqVrXGzEjyVDAFLLlFrSsjUJEsHiIqsyhrbKI8G3iKOEIX6hCGgjt9jREi9QSUY6qTQwCnmaF3UMwYKjgmNnAZvi9VlaDQYNoOnGvaSENypXLtryKPNaEW09UzYtCULgpVqaJ4YPIIPoVvwWHrFJwyfXc0xJvoxxnqc/PNuc5pe0gKZUwmQCixiNGwxQOU+mmByQIHmlji0eFkeLKkSpjBGPKQVHYtWJSdT5vFgMlqoIYwd670PWZWgRKsFLaZS8NIHFvExEbKgkiS7MnWhGkEkxQ8ovnnw8enux+vuvx7W/u1/a2/svKRGIRtokl41asA+xDMLZURYwhg3X8SuXIbdlUMcLNiHOWNmY9L7F8fN6e7cLtfOuHUC31Dkm1JvCOp81Oo7k+P8U6vMTSb5D3IVkuA=", 145 | "anchorsComplete": ["cal"] 146 | }, 147 | { 148 | "hashIdNode": "2129fdb1-6d1d-11e9-a5a8-016a39edf372", 149 | "proof": "eJylVEuOI0UQ5QrcgeW4HZ/8emWJK7CajRWRkYFLamzLrhlgObBh29wAptEMiA0SYsk9LHEY0m40munejMSqSlEVL9578TJ/eLtu+93cv5n/3s7z4bRaLr/myW72xy+XbSvT7rCfdvPyJd/P3x76b5+/K91v5bQ9r527KmWImb0zenNsGcBZgmdvoZaKnilVbU1a1mSRezDnGkKDKL9fYDaTbXZ76+fPCKm6KS6SoS0Qe11IlLIATMK1j75Mf11bTi/0q2me+0PnRuY/CbAuIC6AviBYxbqC8vwdfNsfB3xg86YR3oNXDX3Am0FtIq76GP7S+RgeVwAryM/f6lF2bdtPd9/9civab/9ocru5lPbHzcO31/vD6Z9PPn310+352ZXpZKuPUfnq5/3hzWkrC4rp0rwG7s3JsEuN7NiTheo9adVgxgCUq2mi7DzMJis5IVQkphpqQ4lPAFE7uZurcaPxHKtyLwmEWbmPUVocuVMQxDx2LMBJc2k95aLujwCP53WgmjVai80KeyoZGEsuQWtKGKlJlg4QFKNq1NDGeDQYIcFRIq5PJaOO30NASL1BJejeEjdwCjlYF/WMTMOMhEZOg/fFE+FQB8Nm8ISh9TJccadyVdcwDpnBimjrmbBpS8aClSo3TxE8gNfcG3soveIThs+u4Rgb/ZhgPW7+cTed5pe0whhTYUgFViEYtsBQYx8iIjmgQHNLkVvwmCKWVAkTl3HOShZF0io5mcY4XkyGhMocq/beh61KUILVghZSKXjRgUV8bISMVZJk10hdRkZAJIUPKL55yPHp7vvr2X89ov3rf9Ge7LymRpwNNEkfEVTGPswbSSyjEoXHuuPF7BrLiLtGDoNF9F5ayNEi6f2L43S6O7fLtTNuHY43FOJNqTcEdTlm9Z3Jcfl/pyxNZvkXED2TFw==", 150 | "anchorsComplete": ["cal"] 151 | }, 152 | { 153 | "hashIdNode": "2129fdb2-6d1d-11e9-a5a8-01476fb7f540", 154 | "proof": "eJylVDuOG0cQ9RV8B4fisru6qj+MCPgKjpQQ9ek2CdAkQY5kK1w5cbq+gaw1JBtODBgOfQ8COoyKu4Yg7SYClM3UTL1+79Xr+uXtUve7qf80/beepsNpMZ//mDZ2tT9+P9c1b3aH/WY3zZ+n2+nFof/57YfS7ZpP6/OyS8MapRAT1JJxaBjcOAzIOaQqg8ZAyC2XlNKoUkuimAwNBNpA+esCs9rYare3fv4GoldNYJYt2izG3mZMXGchYslDyiAM/961nJ7JD5tp6vedK57+gRDbLNAswHcQFtQWoT79AK/7o8NjsqFC4SN4EewObxaaMg+Rh/CXzofwcRHCIpSnb+XIO133083L37csffu38nZ1Ke2Pq/tvr/eH07uvvr5+tT0/uWO6scXnqLz+bX94c1rzDChfvzqel5lHpqHKOWmhOMjEnUbV0nToiIDFXTeCyL2V1iIKJxxpFI4hAz0CrKoYKlLoDWqs3HoMzSeIWSo2q81SiYosRZB6LWhYJJAaCySy8Sng9ryM2U/HUiv7n+5nKamCjRBJWVWz+suogdoIvUAUBUkgI6hgiU71EWCIElAQnX/X0CD0oa7eZaKz6SzD+yCNmKPBgJTEsA1X3UoVtcceWq85hjHAeUkdGskdQ6ssemGkotkSxwYtqbsdBobRStc0sPYW20OGT+7C4RP9nGA9bP51tzlNz2ERiXJNIdewQDR3PIVG3UUQuHccdFimpDgoU6y5Qcyp+sBrYYkgjUs2IfIHY5fQUqImvXeMIuADtlajYa41XnT4oP3aNbAknLkMIegMDQNzxk8ovrnP8enm57u7/9qj/cf/0d7YeQkKyacs2eMmKCl2N8/M73z2MCdQpovZjarHXSj5mjAavSoW8pzI7bPj5nRz1sva8a2T6AqQrmq7gtDmflbfGR/nX3rK3Hji96VakVw=", 155 | "anchorsComplete": ["cal"] 156 | }, 157 | { 158 | "hashIdNode": "2129fdb3-6d1d-11e9-a5a8-013b760d9685", 159 | "proof": "eJylVMuOW0UQ5Rf4B5bxuKq6qh9ejcQvsMpmVI9ubGmwR7YTYBnYsB3+ADIoAbFBQlnyH5b4GNozKCIzm0js7u17z6lTp071D28vfbc99m+Of62Px5vDarn8Om3iYrf/culr3Wxvdpvtcfky3R2/vem/ff7+6G6th/XpMod15JKyJbduNBpbY06m6jYgW2jHCMsFqAnXUOUsViQnd8H0+5nmahNX213002eE1EZYWuTAWCD2tlDRugBMVjJEy1Xe3UMOL+yrzfHYH5BXevyTANsCZAH0BcFK2grq8/f0vttPek4x3AT+Q2/GfdJHQHPVYfaY/ox8TI8rgBWU529tr1tf98Ptd79cq/XrP1yvr85Hu/3Vw7fXu5vD3598+uqn69Oze6WbWH1Ml69+3t28Oax1QZLP4MviqSIJjE5VMYtGqt5z50GpOWFCRQKFMJCG3BgkCAQL66gVyoeE+9NldWeoLNAbVazaOkLLPDhb5Ra1RSrorFaMpdfCwWVye6hRkhhPFGIOIS616vxz+llKqhQDUFzdPft8GXWqG9ALoTlZohkSNy6Y+CkhoAEbM0LuDo2gD5+5gTGrcHS1MXGUBmYMmjYkC25DE7dSzQOetBy9ZoQxaOqyOhxlUHBUNT8rcvMcSbFRSz7y9JphtNI9Da69YXus8Nl9OOZEPyZYj8E/bjeH40taoUiuCXKFFXNMxxM06bMJoemdgo/IkpyHZMGaG2Gekx9YixqSNS1zCUXmQ+hsoaUkzXrvjGY0BxytYnCuFc99zEGPuaIUc0WzlmFCXWmmRTXzBxLfPOT4cPv9/e6/ntH+9d9ob+J0SU5pTtmy9mZsCfs0L2Yo54loIlc5m92kzribJJ4qZPTqXGTmxO5e7DeH25Ofr5156yS5IJaL2i4I2nLW6tvQ/fL/VlmGHvUftA2QhQ==", 160 | "anchorsComplete": ["cal"] 161 | }, 162 | { 163 | "hashIdNode": "2129fdb4-6d1d-11e9-a5a8-01b9e036446a", 164 | "proof": "eJyllD2Om0cMhnOF3CGltSJnOD9UJSBXSOVG4M9MJECRBOmzE5frNGk3N3C8gZ0gTYAgZe4hwIfxaDcw7N3GgLvv44DvkC+f4S9vl7bfTe2n6b/1NB1Oi/n8x7jxq/3x+7mtZbM77De7af483k4vDu3Pbz+EbtdyWp+XkEyRSqpGgJFyjlI5OWTjGLSamMTUpGKB0LCSARmTdKwplab410VmtfHVbu/t/E3AwN2VZtnRZ4iNZ5KkzgCVG8RMlOXfu5TTM/1hM03tPnMl0z8BkGeQZhC+C7BIvID69IO87Y9DnqJ30wQfyatSG/LuwCbSVR/KXzIfyuMCYAHl6Vs9ys7W7XTz8vetaNv+bbJdXUL74+r+7PX+cHr31dfXr7bnJ3eVbnzxOV1e/7Y/vDmtZRZSvn51PC+7Akjx2qAXjeK5M1vPOZOCS+1S1DQkpp6ptRYTSDUIsWCtuUr/VHB7Xjav3Djlhlw4YZUxsxxzx+IX4d5qbMFJtGSM2AIXFIwlimDJUB4LYvYUqNQqTmX4WUqswTtgGhiYZRs/vULiDq0EVAs6GOlgSmWw81hw+AGkRAi5GXCA1i1Hgz5uIW+ifeSF2DGjhx5iVCfuEolLVXN45KG3mhF6D6Murd0w9dGhV1G7VGRq2aMgB47DgQSdoHNpFjvVxsgPK3xyB8eY6OeA9TD5193mND0PC0wp1wi5woLI0SgCpzaaSGF4J2Ddc4pGPeUxpcwBc6w2XlARxaAsJbumND5cRgscY2IdBBCqBqjkXNEp14qXPsacOykHjypZStcUmgQmEMn0SYlv7jk+3fx89/ZfD7T/+B/tjZ+XwQZbDpqlsZIOQoZ57rGOSJIYTNLFbE514K4p0qgiDahsbIvBid4+O25ON2e7rJ2xdWK6CpSuKl8F4Pm4q+1cjvMvvWXuMsl7UhWREQ==", 165 | "anchorsComplete": ["cal"] 166 | }, 167 | { 168 | "hashIdNode": "2129fdb5-6d1d-11e9-a5a8-0181066f34f6", 169 | "proof": "eJyllEGOXEUMhrkCd2CZnnaVy1XlXo3EFVhl07LLVfSThu5W90uAZWDDdrgBZFACYoOEsuQeLXEY/GZQRGY2kdjV85P/8m9/rh/eXrfDfu7fzH/t5vl43qzXX+NkV4fTl+u2k2l/PEz7ef0S7+Zvj/23z9+H7nZy3l2uG3ThShlHUaSQw5BKRJKTAg0wsIyoTUbP1jKnYtZQUEoOGD3w+yKznWy7P1i/fBZD5GFKq2zBViF0XglJXUGoAXIemEZ+d59yfqFfTfPcHzK3Mv8ZIfAKaAXxiwgb4g3U5+/l2+Hk8gltNCX4j7xq6i5vBtxEhupj+SXzsXzYAGygPH+rJ9m3XT/ffvfLjWi/+aPJzXYJHU7bh3+vD8fz3598+uqnm8uz+0on23yMy1c/H45vzjtZRcpL8nU0tEQp8EBCHVCzYqKIgzpjlBpEpVBqAzvm4sduAgUTdy0h83gi2K1yZ8o9cGEKVXLjjHmEYtxGHr1ij5ZEl1mFHrkECVhQJJQM5algyEYxlVrFfM7ApWCNNiBQk9Zabv4xKhAP6CUGbVExupOmqQT3/EQQgkLSlLwpvQFH6KNlbDD8lmRddHied8Cxsziig2aJh7jnUrUZfCh4ulxbrznAGNHr0jpaoOEOrYq2paKmLRtK4MjoHSAYCQaX3nCk2jnw4wqf3cPhE/0YsB4n/7ifzvPLuAlEuSLkCpuULLSEwNTdBEXvnUAblglbGpR9SpljyFjbCLWIhqjsu+QskR9M3AIjEmvvPQXVCDUZ12Ap1xoWHz7nkZSdJpUsZSjFLpETiO/sByW+eeD4fPv9/e6/drR//RftyZzHFtGnrFk6a1InxJtnhtUjJL7cQkuzmarjroTJqyCHqqVCzonevThN59tLW54df3WQrmKiq8pXEXjtd/W9yWn9f29Zm8zyD9B/kI4=", 170 | "anchorsComplete": ["cal"] 171 | }, 172 | { 173 | "hashIdNode": "2129fdb6-6d1d-11e9-a5a8-0137e848cdef", 174 | "proof": "eJylkz9vk0EMxr8MY9PYd+f7kykSX4GpS+Q7+8grhbxR3rcFxsLCWlam0qIWxIKEGPke+TZcUoQgUyW2k0/+2Y8f+939vPTrUV+NP5fjuBlm0+lL28lpv30+LUvu1pu+W4/TC3s7vt7ol6d/QrdLHpa7OWgxpSZ0mAlVU8aag9SULdrkhCM48ZyiMMdqrAuBlMEa9BEgJvq6xyw6Wax70d0TgyZVyX7iBWWCqGnCxHECaINGF4to/XFIGc7zi24c9SFzweN3A5gmQBMwzwzMKM0gnv3Bl37b8M5KLZngL3zOThteBFJhrjkf4/eZx3icAcwgnN3nLa/LUoerN59WnHX1rfBqsQ/128XD302/GT5cXq92J4c+O5k9RuPlx35zNyx5YshfXm93c2syeCPBgwZpwy0musopFcco6lUzOeCM2MIBFKuPGkzFCN77egxc7eaUCwUm7zUKmKguOtQGiMbWXNjHWALXBLky+qQIzT82KdhEmpIzx8CTw5yavMfM+Dj5/bobxgszQyIfLbTdmDknWJyFVs66RKYCMpQqnmxxlTxh9KmtkY2lqQytc5MTBy+ZqD2EseRkLaWsqm07s4HoJEUU17RhsdVh5OpyMmIzew41k9Emsc2RvfunxbsHS4ert4czuGkuf/7tcie7uSnGBoHsuR2Aa+5o8SJiY4sQW1OYoscmJTbvM1nXuqCqsbhAQibfnm+74WpX9hfYDtDSqXF0GtOpgTRttXQtvJ3+b5Wp8Mi/AM5XRrM=", 175 | "anchorsComplete": ["cal"] 176 | }, 177 | { 178 | "hashIdNode": "2129fdb7-6d1d-11e9-a5a8-018e6ea7dd2a", 179 | "proof": "eJylUz1vU0EQ/DOUsb13u3sfrizxF6jSWHu3d/hJwc/yewlQBhra0FJBggKIBglR8j/8bzg7KALTRKJ72qeZ3Zm5efNpkfv1WF6MP1fjuBnms9lz7HTab5/O8kq69abv1uPsAm/Gl5vy5fH96GYlw2q3IEWPGMhxMVmIUiyqWLOYFBIigBeOGms1FKV4BGUbDMSU2QNA/LqnWXa6XPdado+ssbFq8hOnRifGlDgRljABE4or4lWt/DhAhvP0rBvHcodcyvjdgokT4AnYJxbmHOcQTu/pc79t9IRac2L4gz4lKo1eFWIWqSkd0++Rx/RmDjAHf/opbWWdV2W4evXxTFI5+5blbLkf9dvl3b/rfjO8u3x/tjs53Nnp/CEaLz/0m9thJRPLbg9eSM4BhGwyNgdmIae5JA/ORUVRi5Fq9OSlmS4+7A2WTCYhOe8s4T+EvE9A2LkSFGwoFMgUSSZYrA3rQsheaoRUxbhYDEioYqPHyCVGsseEJwefmryHeHwMfrvuhvHCzg2zCwguwJxITSaEtg4psq1gBHJVx5ipsmMTXLTGYcjVhKbb2BTFO03M7UPF5BQROaZSSjMiWQikMRilps1krGSC1PZgrWISJ74mtqVJJBBx9NeJt3eRDlevDzW4bil//p1yp7uFzRa9QnJSYqKEpmSnrQahTVjQZuHg2qPn0LJPjNSu4FpCJs+tD+nmfNsNV7u8b2ArIPLUEk9DnFqIs7arrFW2s//dMlMZ5Rf2OEZX", 180 | "anchorsComplete": ["cal"] 181 | } 182 | ] 183 | -------------------------------------------------------------------------------- /tests/data/submit-hashes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": { 4 | "submitted_at": "2019-05-02T20:59:08Z", 5 | "processing_hints": { 6 | "cal": "2019-05-02T21:01:08Z", 7 | "btc": "2019-05-02T22:00:00Z" 8 | }, 9 | "submitted_to": "http://35.237.15.174" 10 | }, 11 | "hashes": [ 12 | { 13 | "hash_id_node": "21287710-6d1d-11e9-a653-01e94c159e8c", 14 | "hash": "6806d68d9db70188d2eebbc305614332793a57e2cb6b4cabb6e3578c1d551c70" 15 | }, 16 | { 17 | "hash_id_node": "21287711-6d1d-11e9-a653-0117e0470c36", 18 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963" 19 | }, 20 | { 21 | "hash_id_node": "21287712-6d1d-11e9-a653-01121760b027", 22 | "hash": "5935e9777e5080814f58f8412acac7d548706566f6bef0f7024a8af898d88218" 23 | }, 24 | { 25 | "hash_id_node": "21287713-6d1d-11e9-a653-01ba817b7c6d", 26 | "hash": "4ce65cb54a7266455b44dc2fa46dd135925b2755a408e19d207dbde6a67995a2" 27 | }, 28 | { 29 | "hash_id_node": "21287714-6d1d-11e9-a653-0117e0470c36", 30 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963" 31 | }, 32 | { 33 | "hash_id_node": "21287715-6d1d-11e9-a653-01a98661d85f", 34 | "hash": "8570b600fa3d9658a4c133e1d3897c099676e938a7775be06f699f807bcd0243" 35 | }, 36 | { 37 | "hash_id_node": "21287716-6d1d-11e9-a653-016e742c718a", 38 | "hash": "0ffd76bdd51d7ee1c151360e1f2d411ba46640518d518da856b9b9193ec2dd83" 39 | }, 40 | { 41 | "hash_id_node": "21287717-6d1d-11e9-a653-0160de72b4a0", 42 | "hash": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1" 43 | }, 44 | { 45 | "hash_id_node": "21287718-6d1d-11e9-a653-01cf0c91e2f7", 46 | "hash": "f3ebb270573fe31fcf1c700f3a4f7fc49891f7269bccac7b6d53e4df3944c05a" 47 | }, 48 | { 49 | "hash_id_node": "21287719-6d1d-11e9-a653-016e1c5cab92", 50 | "hash": "eb9481b75a528764fc0fa9a0f266038bf5ff426967333f8b873513d4d2b29f4b" 51 | }, 52 | { 53 | "hash_id_node": "2128771a-6d1d-11e9-a653-01bab60e6173", 54 | "hash": "6dbe14736b3cbeb2f94b9443baacbf06bdae1ddb67029548daa465b7563cc513" 55 | }, 56 | { 57 | "hash_id_node": "2128771b-6d1d-11e9-a653-01f9221b3cd1", 58 | "hash": "05cb14758c40134663a895d06c932b8caca35ea81702e184c04c94af18557eb1" 59 | }, 60 | { 61 | "hash_id_node": "2128771c-6d1d-11e9-a653-019a9cbc038a", 62 | "hash": "c0ea98563f7b35161fa8555a64b05f0d0d633bcafe6dc6947ddc3a3a76132c69" 63 | }, 64 | { 65 | "hash_id_node": "2128771d-6d1d-11e9-a653-015e9e9fda78", 66 | "hash": "0ec2cf9141b51ee9b1fb7df9b31394da804d6a98daa8f234775ea03216800895" 67 | }, 68 | { 69 | "hash_id_node": "2128771e-6d1d-11e9-a653-010d18dca7c1", 70 | "hash": "4d37338465e1ca44b9edd3fca1b8b33007a59d9ff149ae730d528109bc570009" 71 | } 72 | ] 73 | }, 74 | { 75 | "meta": { 76 | "submitted_at": "2019-05-02T20:59:08Z", 77 | "processing_hints": { 78 | "cal": "2019-05-02T21:01:08Z", 79 | "btc": "2019-05-02T22:00:00Z" 80 | }, 81 | "submitted_to": "http://35.196.109.49" 82 | }, 83 | "hashes": [ 84 | { 85 | "hash_id_node": "21296170-6d1d-11e9-a5a8-015e0f0e5b51", 86 | "hash": "6806d68d9db70188d2eebbc305614332793a57e2cb6b4cabb6e3578c1d551c70" 87 | }, 88 | { 89 | "hash_id_node": "21296171-6d1d-11e9-a5a8-01b1b6a63748", 90 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963" 91 | }, 92 | { 93 | "hash_id_node": "21296172-6d1d-11e9-a5a8-010e5a5f3dca", 94 | "hash": "5935e9777e5080814f58f8412acac7d548706566f6bef0f7024a8af898d88218" 95 | }, 96 | { 97 | "hash_id_node": "21296173-6d1d-11e9-a5a8-0195708d7979", 98 | "hash": "4ce65cb54a7266455b44dc2fa46dd135925b2755a408e19d207dbde6a67995a2" 99 | }, 100 | { 101 | "hash_id_node": "21296174-6d1d-11e9-a5a8-01b1b6a63748", 102 | "hash": "4c4beb2aa8c9e79db8d558cceef39817610c07e664e8a610c5f224cfcd8f4963" 103 | }, 104 | { 105 | "hash_id_node": "21296175-6d1d-11e9-a5a8-0181224ebb54", 106 | "hash": "8570b600fa3d9658a4c133e1d3897c099676e938a7775be06f699f807bcd0243" 107 | }, 108 | { 109 | "hash_id_node": "21298880-6d1d-11e9-a5a8-013396876a61", 110 | "hash": "0ffd76bdd51d7ee1c151360e1f2d411ba46640518d518da856b9b9193ec2dd83" 111 | }, 112 | { 113 | "hash_id_node": "2129fdb0-6d1d-11e9-a5a8-013efcb8790c", 114 | "hash": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1" 115 | }, 116 | { 117 | "hash_id_node": "2129fdb1-6d1d-11e9-a5a8-016a39edf372", 118 | "hash": "f3ebb270573fe31fcf1c700f3a4f7fc49891f7269bccac7b6d53e4df3944c05a" 119 | }, 120 | { 121 | "hash_id_node": "2129fdb2-6d1d-11e9-a5a8-01476fb7f540", 122 | "hash": "eb9481b75a528764fc0fa9a0f266038bf5ff426967333f8b873513d4d2b29f4b" 123 | }, 124 | { 125 | "hash_id_node": "2129fdb3-6d1d-11e9-a5a8-013b760d9685", 126 | "hash": "6dbe14736b3cbeb2f94b9443baacbf06bdae1ddb67029548daa465b7563cc513" 127 | }, 128 | { 129 | "hash_id_node": "2129fdb4-6d1d-11e9-a5a8-01b9e036446a", 130 | "hash": "05cb14758c40134663a895d06c932b8caca35ea81702e184c04c94af18557eb1" 131 | }, 132 | { 133 | "hash_id_node": "2129fdb5-6d1d-11e9-a5a8-0181066f34f6", 134 | "hash": "c0ea98563f7b35161fa8555a64b05f0d0d633bcafe6dc6947ddc3a3a76132c69" 135 | }, 136 | { 137 | "hash_id_node": "2129fdb6-6d1d-11e9-a5a8-0137e848cdef", 138 | "hash": "0ec2cf9141b51ee9b1fb7df9b31394da804d6a98daa8f234775ea03216800895" 139 | }, 140 | { 141 | "hash_id_node": "2129fdb7-6d1d-11e9-a5a8-018e6ea7dd2a", 142 | "hash": "4d37338465e1ca44b9edd3fca1b8b33007a59d9ff149ae730d528109bc570009" 143 | } 144 | ] 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /tests/e2e-test.js: -------------------------------------------------------------------------------- 1 | const chp = require('../dist/bundle') 2 | const fs = require('fs') 3 | const { expect } = require('chai') 4 | 5 | describe('E2E tests', function() { 6 | // need a very long timeout now because the 7 | // time from hash submission to proof creation can be up to two minutes 8 | this.timeout(250000) 9 | let hashes, proofHandlesHashes, proofHandlesFiles, proofs, nodes 10 | 11 | before(async () => { 12 | hashes = [ 13 | '1d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 14 | '2d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a', 15 | '3d2a9e92b561440e8d27a21eed114f7018105db00262af7d7087f7dea9986b0a' 16 | ] 17 | 18 | // B/c v2 development is still unstable, sometimes we will be unable to 19 | // find a node. This should resolve after one or two tries 20 | // otherwise the describe's timeout will eventually end 21 | while (!nodes) { 22 | nodes = await chp.getNodes(2) 23 | } 24 | }) 25 | 26 | it('should submit each hash to known good nodes', async () => { 27 | // Submit each hash to three randomly selected Nodes 28 | proofHandlesHashes = await chp.submitHashes(hashes, nodes) 29 | let paths = fs 30 | .readdirSync('./') 31 | .map(file => `./${file}`) 32 | .filter(file => fs.lstatSync(file).isFile()) 33 | 34 | // Submit each hashes to same nodes 35 | proofHandlesFiles = await chp.submitFileHashes(paths, nodes) 36 | expect(proofHandlesFiles).to.exist 37 | }) 38 | 39 | it('should retrieve a calendar proof for each hash that was submitted', async () => { 40 | // Wait for Calendar proofs to be available. This can take a couple of minutes 41 | await new Promise(resolve => setTimeout(resolve, 150000)) 42 | proofs = await chp.getProofs([...proofHandlesHashes, ...proofHandlesFiles]) 43 | expect(proofs).to.exist 44 | }) 45 | 46 | it('should verify every anchor in every calendar proof', async () => { 47 | const verifiedProofs = await chp.verifyProofs(proofs, nodes[0]) 48 | expect(verifiedProofs).to.exist 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/evaluate-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import { expect } from 'chai' 14 | 15 | import { evaluateProofs } from '../index' 16 | import { normalizeProofs, flattenProofs, parseProofs } from '../lib/utils/proofs' 17 | import proofs from './data/proofs' 18 | 19 | describe('evaluateProofs', () => { 20 | it('should return normalized, parsed, and flattened proofs', () => { 21 | let normalized = normalizeProofs(proofs) 22 | let parsed = parseProofs(normalized) 23 | let flattened = flattenProofs(parsed) 24 | 25 | let test = evaluateProofs(proofs) 26 | 27 | flattened.forEach((proof, index) => expect(proof).to.deep.equal(test[index])) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/get-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import { expect } from 'chai' 14 | import nock from 'nock' 15 | import { forEach, isEmpty } from 'lodash' 16 | 17 | import proofHandles from './data/proof-handles' 18 | import proofsResponse from './data/proofs-response' 19 | import { getProofs, evaluateProofs } from '../index' 20 | 21 | describe('getProofs', () => { 22 | let nodes 23 | before(() => { 24 | nodes = {} 25 | forEach(proofHandles, handle => { 26 | if (isEmpty(nodes[handle.uri])) { 27 | nodes[handle.uri] = [] 28 | } 29 | nodes[handle.uri].push(handle.hashIdNode) 30 | }) 31 | }) 32 | 33 | it('should only accept an array of valid proof handles', async () => { 34 | let emptyArray, notArray, tooManyHandles, invalidHandle, badURIs, badUUID 35 | 36 | try { 37 | await getProofs([]) 38 | } catch (e) { 39 | emptyArray = true 40 | } 41 | expect(emptyArray, 'Should have thrown with an empty array').to.be.true 42 | 43 | try { 44 | await getProofs('not an array') 45 | } catch (e) { 46 | notArray = true 47 | } 48 | expect(notArray, 'Should have thrown with a non-array arg').to.be.true 49 | 50 | try { 51 | let largeDataSet = [...proofHandles] 52 | while (largeDataSet.length < 251) { 53 | largeDataSet.push(...proofHandles) 54 | } 55 | await getProofs(largeDataSet) 56 | } catch (e) { 57 | if (e.message.indexOf('<= 250') > -1) tooManyHandles = true 58 | } 59 | expect(tooManyHandles, 'Should have thrown with a data set larger than 250 items').to.be.true 60 | 61 | try { 62 | await getProofs([{ ...proofHandles[0], uri: undefined }]) 63 | } catch (e) { 64 | badURIs = true 65 | } 66 | expect(badURIs, 'Should have thrown with an invalid uri').to.be.true 67 | 68 | try { 69 | await getProofs([{ foo: 'bar' }]) 70 | } catch (e) { 71 | invalidHandle = true 72 | } 73 | expect(invalidHandle, 'Should have thrown with an invalid handle').to.be.true 74 | 75 | try { 76 | await getProofs([{ ...proofHandles[0], hashIdNode: '123456' }]) 77 | } catch (e) { 78 | badUUID = true 79 | } 80 | expect(badUUID, 'Should have thrown with an invalid hashIdNode').to.be.true 81 | }) 82 | 83 | describe('network responses', () => { 84 | let mockedReponses 85 | 86 | beforeEach(() => { 87 | // mocked response should be from test data 88 | mockedReponses = Object.keys(nodes).map((uri, index) => 89 | nock(uri) 90 | .get('/proofs') 91 | .reply(200, proofsResponse[index]) 92 | ) 93 | }) 94 | 95 | it('should make one get request to each node uri', async () => { 96 | await getProofs(proofHandles) 97 | mockedReponses.forEach(resp => expect(resp.isDone()).to.be.true) 98 | }) 99 | 100 | it('should return flattened proofs from nodes that can be normalized', async () => { 101 | let flattenedResponse = await getProofs(proofHandles) 102 | let evaluatedProofs = evaluateProofs(flattenedResponse) 103 | 104 | expect(evaluatedProofs).to.exist 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /tests/helpers-test.js: -------------------------------------------------------------------------------- 1 | import { helpers } from '../lib/utils' 2 | import { expect } from 'chai' 3 | import fs from 'bfile' 4 | import crypto from 'crypto' 5 | import path from 'path' 6 | import nock from 'nock' 7 | 8 | describe('helpers utilities', () => { 9 | let testPath 10 | before(async () => { 11 | testPath = '/tmp/test_hashes' 12 | await fs.mkdirp(testPath) 13 | }) 14 | 15 | after(async () => { 16 | await fs.remove(testPath) 17 | nock.cleanAll() 18 | }) 19 | describe('isHex', () => { 20 | it('should test for valid hexadecimal strings', () => { 21 | const buf = new Buffer.from('hello world', 'utf8') 22 | const hex = buf.toString('hex') 23 | expect(helpers.isHex(hex)).to.be.true 24 | expect(helpers.isHex('foo bar')).to.be.false 25 | }) 26 | }) 27 | 28 | describe('isValidUUID', () => { 29 | it('should validate v1 UUIDs', () => { 30 | let v1 = '23d57c30-afe7-11e4-ab7d-12e3f512a338' 31 | let v4 = '09bb1d8c-4965-4788-94f7-31b151eaba4e' 32 | 33 | expect(helpers.isValidUUID(v1)).to.be.true 34 | expect(helpers.isValidUUID(v4)).to.be.false 35 | }) 36 | }) 37 | 38 | describe('isSecureOrigin', () => { 39 | it('should only validate https locations', () => { 40 | global.window = { 41 | location: { 42 | protocol: 'https:' 43 | } 44 | } 45 | expect(helpers.isSecureOrigin()).to.be.true 46 | global.window.location.protocol = 'http:' 47 | expect(helpers.isSecureOrigin()).to.be.false 48 | }) 49 | }) 50 | 51 | describe('sha256FileByPath', () => { 52 | it('should create a sha256 hash of the contents of a file', async () => { 53 | let text = 'I am some test content' 54 | let hash = crypto 55 | .createHash('sha256') 56 | .update(text, 'utf8') 57 | .digest() 58 | let filePath = path.resolve(testPath, 'sha256FileByPath') 59 | await fs.writeFile(filePath, text) 60 | let file = await helpers.sha256FileByPath(filePath) 61 | 62 | // testing that the hash that is returned from the contents of the file 63 | // are the same as the hash we made of the content before adding to file 64 | expect(file.hash.toString('hex')).to.equal(hash.toString('hex')) 65 | }) 66 | }) 67 | 68 | describe('fetchEndpoints', () => { 69 | it('should return all responses from designated endpoints', async () => { 70 | let requests = [ 71 | { uri: 'http://test.com', method: 'GET', mockResponse: 'GET success!' }, 72 | { uri: 'http://test.com', method: 'POST', body: { foo: 'bar' }, mockResponse: 'POST success!' } 73 | ] 74 | requests.forEach(({ uri, method, body, mockResponse }) => 75 | nock(uri) 76 | .intercept('/', method, body) 77 | .reply(200, { data: mockResponse }) 78 | ) 79 | 80 | const fetchMap = await helpers.fetchEndpoints(requests) 81 | fetchMap.forEach((res, index) => expect(res.data).to.equal(requests[index].mockResponse)) 82 | }) 83 | }) 84 | 85 | describe('validateHashesArg', () => { 86 | it('should reject invalid arguments', () => { 87 | let { validateHashesArg } = helpers 88 | let testFns = [] 89 | let nonArray = () => validateHashesArg('not an array!') 90 | let emptyArray = () => validateHashesArg([]) 91 | let bigArray = () => validateHashesArg(Array(251)) 92 | let withValidator = () => validateHashesArg([1], item => item > 5) 93 | 94 | testFns.push(nonArray, emptyArray, bigArray, withValidator) 95 | testFns.forEach(test => 96 | expect(test, `invoking ${test.name} should have thrown but passed the validation`).to.throw() 97 | ) 98 | }) 99 | }) 100 | 101 | describe('validateUrisArg', () => { 102 | it('should reject invalid uri args', () => { 103 | let { validateUrisArg } = helpers 104 | let testFns = [] 105 | let nonArray = () => validateUrisArg('not an array!') 106 | let bigArray = () => validateUrisArg(Array(6)) 107 | testFns.push(nonArray, bigArray) 108 | testFns.forEach(test => 109 | expect(test, `invoking ${test.name} should have thrown but passed the validation`).to.throw() 110 | ) 111 | }) 112 | }) 113 | 114 | describe('getFileHashes', () => { 115 | let content1, content2, file1, file2 116 | before(async () => { 117 | content1 = 'I am some test content that will be added to the first file' 118 | content2 = 'I am some test content that will be added to the second file' 119 | file1 = path.resolve(testPath, 'file1') 120 | file2 = path.resolve(testPath, 'file2') 121 | await fs.writeFile(file1, content1) 122 | await fs.writeFile(file2, content2) 123 | }) 124 | 125 | after(async () => { 126 | await fs.remove(file1) 127 | await fs.remove(file2) 128 | }) 129 | it('should throw if paths arg is invalid', async () => { 130 | let { getFileHashes } = helpers 131 | let filePath = path.resolve(testPath, 'foobar.txt') 132 | let firstError = false 133 | let secondError = false 134 | 135 | // need to do this in try/catches because they are async functions 136 | try { 137 | await getFileHashes('foobar.txt') 138 | } catch (e) { 139 | if (e) firstError = true 140 | } 141 | 142 | try { 143 | await getFileHashes([filePath]) 144 | } catch (e) { 145 | if (e) secondError = true 146 | } 147 | expect(firstError).to.be.true 148 | expect(secondError).to.be.true 149 | }) 150 | it('should get hashes from the contents of multiple files', async () => { 151 | let hashObjs = await helpers.getFileHashes([file1, file2]) 152 | let hash1 = crypto 153 | .createHash('sha256') 154 | .update(content1, 'utf8') 155 | .digest() 156 | let hash2 = crypto 157 | .createHash('sha256') 158 | .update(content2, 'utf8') 159 | .digest() 160 | 161 | expect(hash1.toString('hex')).to.equal(hashObjs[0].hash.toString('hex')) 162 | expect(hash2.toString('hex')).to.equal(hashObjs[1].hash.toString('hex')) 163 | }) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | export function testArrayArg(fn) { 4 | let emptyArray = () => fn([]) 5 | let notArray = () => fn('not an array') 6 | expect(emptyArray, 'Did not throw when passed an empty array').to.throw() 7 | expect(notArray, 'Did not throw when passed a non-array').to.throw() 8 | } 9 | -------------------------------------------------------------------------------- /tests/network-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import nock from 'nock' 3 | 4 | import { network } from '../lib/utils' 5 | import nodes from './data/nodes' 6 | 7 | describe('network utilities', () => { 8 | describe('isValidNodeURI', () => { 9 | it('should only pass valid node URIs', () => { 10 | let validURIs = ['http://123.45.64.2', 'https://123.54.32.11'] 11 | let invalidURIs = [ 12 | 123, // should only accept strings 13 | '0.0.0.0', // blacklisted 14 | 'chainpoint.org', // must be IP address 15 | '123.45.66.3', // must have protocol 16 | 'ftp://123.45.66.3' // only accept http or https protocol 17 | ] 18 | validURIs.forEach(uri => expect(network.isValidNodeURI(uri), `expected ${uri} to be validated`).to.be.true) 19 | invalidURIs.forEach(uri => expect(network.isValidNodeURI(uri), `expected ${uri} to be validated`).to.be.false) 20 | }) 21 | }) 22 | 23 | describe('isValidCoreURI', () => { 24 | it('should only pass valid core URIs', () => { 25 | let validURIs = ['https://a.chainpoint.org'] 26 | let invalidURIs = [ 27 | 'http://satoshi.chainpoint.org', // only https is valid 28 | 'https://satoshi.chainpoint.org', // only single letter subdomains are valid 29 | 123, // should only accept strings 30 | '', // returns false on empty 31 | 'a.chainpoint.org' // must have protocol 32 | ] 33 | validURIs.forEach(uri => expect(network.isValidCoreURI(uri), `expected ${uri} to be validated`).to.be.true) 34 | invalidURIs.forEach(uri => expect(network.isValidCoreURI(uri), `expected ${uri} to be validated`).to.be.false) 35 | }) 36 | }) 37 | 38 | describe('getCores', () => { 39 | it('should return valid core URIs corresponding to the number requested', async () => { 40 | let count = 2 41 | let cores = await network.getCores(count) 42 | expect(cores).to.have.lengthOf.at.most(count) 43 | cores.forEach(core => expect(network.isValidCoreURI(core), `Invalid core URI returned: ${core}`).to.be.true) 44 | }) 45 | }) 46 | 47 | describe('getNodes', function() { 48 | this.timeout(8000) 49 | it('should return valid node URIs corresponding to the number requested', async () => { 50 | let count = 2 51 | let nodes = await network.getNodes(count) 52 | // because testing network is unstable, needs to be `at.most` because 53 | // some nodes will sometimes fail 54 | expect(nodes).to.have.lengthOf.at.most(count) 55 | nodes.forEach(node => expect(network.isValidNodeURI(node), `Invalid node URI returned: ${node}`).to.be.true) 56 | }) 57 | }) 58 | 59 | describe('testNodeEndpoints', () => { 60 | afterEach(() => { 61 | nock.cleanAll() 62 | }) 63 | 64 | it('should skip invalid node URIs and only return valid Nodes that respond to requests', async () => { 65 | nodes.forEach(node => { 66 | nock(node) 67 | .get('/') 68 | .delay(100) 69 | .reply(200) 70 | }) 71 | let badNodes = ['fail.com', 'http://0.0.0.3'] 72 | let failed = [] 73 | let tested = await network.testNodeEndpoints([...nodes, ...badNodes], failed) 74 | // clear failed endpoints from result 75 | tested = tested.filter(node => node) 76 | 77 | // should not have more than the known working nodes in result 78 | expect(failed).to.eql(badNodes) 79 | expect(tested).to.have.lengthOf(nodes.length) 80 | }) 81 | 82 | it("should reject endpoints that don't respond after specified timeout", async () => { 83 | let timeoutDelay = 100 84 | nodes.forEach(node => { 85 | nock(node) 86 | .get('/') 87 | .delay(timeoutDelay) 88 | .reply(200) 89 | }) 90 | 91 | let failed = [] 92 | let tested = await network.testNodeEndpoints(nodes, failed, timeoutDelay - 50) 93 | tested = tested.filter(node => node) 94 | expect(tested).to.have.lengthOf(0) 95 | expect(failed).to.have.lengthOf(nodes.length) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /tests/proof-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import { expect } from 'chai' 14 | import sinon from 'sinon' 15 | 16 | import submitHashes from './data/submit-hashes.json' 17 | import proofs from './data/proofs.json' 18 | import chp from 'chainpoint-binary' 19 | 20 | import { 21 | isValidProofHandle, 22 | mapSubmitHashesRespToProofHandles, 23 | parseProofs, 24 | flattenProofBranches, 25 | flattenProofs, 26 | // flattenBtcBranches, 27 | normalizeProofs 28 | } from '../lib/utils/proofs' 29 | import { testArrayArg } from './helpers' 30 | 31 | describe('proof utilities', () => { 32 | describe('isValidProofHandle', () => { 33 | it('should correctly validate proof handles', () => { 34 | let mockHandle = {} 35 | expect(isValidProofHandle(mockHandle), 'should not accept empty objects').to.be.false 36 | expect(isValidProofHandle('I am not an object!'), 'Should only accept objects').to.be.false 37 | mockHandle.uri = 'http://35.212.136.230' 38 | expect(isValidProofHandle(mockHandle), 'should fail without hashIdNode property').to.be.false 39 | delete mockHandle.uri 40 | mockHandle.hashIdNode = '4bd142c0-655d-11e9-8108-01842c6b2502' 41 | expect(isValidProofHandle(mockHandle), 'should fail without uri property').to.be.false 42 | mockHandle.uri = 'http://35.212.136.230' 43 | expect(isValidProofHandle(mockHandle), 'should pass with uri and hashIdNode properties').to.be.true 44 | }) 45 | }) 46 | 47 | describe('mapSubmitHashesRespToProofHandles', () => { 48 | it('should only accept non-empty array as argument', () => { 49 | testArrayArg(mapSubmitHashesRespToProofHandles) 50 | }) 51 | it('should return handles with expected json formatting', () => { 52 | let proofHandles = mapSubmitHashesRespToProofHandles(submitHashes) 53 | proofHandles.forEach(handle => { 54 | expect(handle).to.have.property('uri') 55 | expect(handle).to.have.property('hash') 56 | expect(handle).to.have.property('proofId') 57 | expect(handle).to.have.property('groupId') 58 | }) 59 | }) 60 | }) 61 | 62 | describe('parseProofs', () => { 63 | it('should only accept non-empty array as argument', () => { 64 | testArrayArg(parseProofs) 65 | }) 66 | 67 | it('should reject proofs of unknown format', () => { 68 | expect(() => parseProofs(['hello world!']), 'unserialized strings should not be accepted').to.throw() 69 | }) 70 | 71 | it('should parse an array of valid proofs', () => { 72 | let { proof } = proofs[0] 73 | proof = chp.binaryToObjectSync(proof) 74 | let proofBuff = chp.objectToBinarySync(proof) 75 | let proofHex = proofBuff.toString('hex') 76 | let proofBase64 = chp.objectToBase64Sync(proof) 77 | let proofJSON = JSON.stringify(proof) 78 | 79 | let proofsArr = [proof, proofBuff, proofHex, proofBase64, proofJSON] 80 | 81 | expect(() => parseProofs(proofsArr)).not.to.throw() 82 | }) 83 | }) 84 | 85 | describe('normalizeProofs', () => { 86 | beforeEach(() => { 87 | sinon.spy(console, 'error') 88 | }) 89 | afterEach(() => { 90 | console.error.restore() 91 | }) 92 | it('should skip incorrectly passed args and log errors', () => { 93 | testArrayArg(normalizeProofs) 94 | // empty array, null proof, or non-chainpoint type proofs should all fail gracefully 95 | let normalized = normalizeProofs([{}, { hashIdNode: 'i-am-an-id', proof: null }, { type: 'foobar' }]) 96 | expect(normalized, 'Array of normalized invalid proofs should have been empty').to.have.length(0) 97 | expect(console.error.calledThrice, 'Did not log errors for each incorrect proof object').to.be.true 98 | }) 99 | 100 | it('should normalize parsed proof objects', () => { 101 | let parsedProofs = proofs.map(proof => chp.binaryToObjectSync(proof.proof)) 102 | // test with already parsed proof objects with type `Chainpoint` 103 | let normalized = normalizeProofs(parsedProofs) 104 | expect(normalized[0]).to.equal(parsedProofs[0]) 105 | }) 106 | 107 | it('should normalize a raw proof binary or submit hashes response', () => { 108 | let testProofs = [...proofs] 109 | // raw proof string in Base64 110 | testProofs[0] = testProofs[0].proof 111 | let normalized = normalizeProofs(testProofs) 112 | 113 | expect(normalized[0]).to.equal(testProofs[0]) 114 | }) 115 | }) 116 | 117 | describe('flattenProofBranches', () => { 118 | it('should only accept non-empty array as argument', () => { 119 | testArrayArg(flattenProofBranches) 120 | }) 121 | 122 | it('should return an array of objects with all relevant data for each branch anchor submitted', () => { 123 | let parsedProofs = parseProofs(proofs.map(proof => chp.binaryToObjectSync(proof.proof))) 124 | let proofBranchArray = parsedProofs.map(proof => proof.branches[0]) 125 | let flattenedBranches = flattenProofBranches(proofBranchArray) 126 | expect(flattenedBranches).to.have.lengthOf.at.least(parsedProofs.length) 127 | 128 | flattenedBranches.forEach(branch => { 129 | // also checking that the property is a string to verify it is defined 130 | expect(branch) 131 | .to.have.property('branch') 132 | .that.is.a('string') 133 | expect(branch) 134 | .to.have.property('uri') 135 | .that.is.a('string') 136 | expect(branch) 137 | .to.have.property('type') 138 | .that.is.a('string') 139 | expect(branch) 140 | .to.have.property('anchor_id') 141 | .that.is.a('string') 142 | expect(branch) 143 | .to.have.property('expected_value') 144 | .that.is.a('string') 145 | expect(branch).to.not.have.property('branches') 146 | }) 147 | }) 148 | }) 149 | 150 | describe('flattenProofs', () => { 151 | it('should return an array of flattened proof anchor objects for each branch', () => { 152 | let parsedProofs = parseProofs(proofs.map(proof => chp.binaryToObjectSync(proof.proof))) 153 | let flattenedProofs = flattenProofs(parsedProofs) 154 | expect(flattenedProofs).to.have.lengthOf.at.least(parsedProofs.length) 155 | 156 | flattenedProofs.forEach(branch => { 157 | // also checking that the property is a string to verify it is defined 158 | expect(branch) 159 | .to.have.property('hash') 160 | .that.is.a('string') 161 | expect(branch) 162 | .to.have.property('proof_id') 163 | .that.is.a('string') 164 | expect(branch) 165 | .to.have.property('hash_received') 166 | .that.is.a('string') 167 | expect(branch) 168 | .to.have.property('branch') 169 | .that.is.a('string') 170 | expect(branch) 171 | .to.have.property('uri') 172 | .that.is.a('string') 173 | expect(branch) 174 | .to.have.property('type') 175 | .that.is.a('string') 176 | expect(branch) 177 | .to.have.property('anchor_id') 178 | .that.is.a('string') 179 | expect(branch) 180 | .to.have.property('expected_value') 181 | .that.is.a('string') 182 | expect(branch).to.not.have.property('branches') 183 | }) 184 | }) 185 | }) 186 | 187 | describe('flattenBtcBranches', () => { 188 | it('should return an array of objects with hash_id_node and raw btc tx') 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /tests/submit-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import { expect } from 'chai' 15 | import nock from 'nock' 16 | import sinon from 'sinon' 17 | import { isEqual } from 'lodash' 18 | 19 | import submitHashes from '../lib/submit' 20 | import { network, proofs } from '../lib/utils' 21 | import submitHashesResp from './data/submit-hashes' 22 | import hashes from './data/hashes' 23 | import nodes from './data/nodes' 24 | 25 | describe('submitHashes', () => { 26 | let mockResponses 27 | beforeEach(async () => { 28 | // steb getNodes so that we can be sure it was called 29 | // but can control what it returns and test expectations 30 | let stub = sinon.stub(network, 'getNodes') 31 | stub.returns(nodes) 32 | 33 | mockResponses = nodes.map((uri, index) => 34 | nock(uri) 35 | .persist() 36 | .post('/hashes') 37 | .reply(200, submitHashesResp[index]) 38 | ) 39 | }) 40 | 41 | afterEach(() => { 42 | // cleanup all mocked responses 43 | nock.cleanAll() 44 | sinon.restore() 45 | }) 46 | 47 | it('should reject invalid hashes arg', async () => { 48 | let emptyArray, notArray, notHex 49 | 50 | try { 51 | await submitHashes([]) 52 | } catch (e) { 53 | emptyArray = true 54 | } 55 | expect(emptyArray, 'Should have thrown with an empty array').to.be.true 56 | 57 | try { 58 | await submitHashes('not an array') 59 | } catch (e) { 60 | notArray = true 61 | } 62 | expect(notArray, 'Should have thrown with a non-array arg').to.be.true 63 | 64 | try { 65 | await submitHashes(['not a hash']) 66 | } catch (e) { 67 | notHex = true 68 | } 69 | expect(notHex, 'Should have thrown with a non-array arg').to.be.true 70 | }) 71 | 72 | it('should reject invalid uris arg', async () => { 73 | let bigArray, notArray, invalidUri 74 | 75 | try { 76 | await submitHashes(hashes, [1, 2, 3, 4, 5, 6]) 77 | } catch (e) { 78 | bigArray = e.message 79 | } 80 | expect(bigArray).to.have.string('5 elements', 'Should have thrown with a uris arg of more than 5 elements') 81 | 82 | try { 83 | await submitHashes(hashes, 'not an array') 84 | } catch (e) { 85 | notArray = true 86 | } 87 | expect(notArray, 'Should have thrown with a non-array uris arg').to.be.true 88 | 89 | try { 90 | await submitHashes(hashes, ['http://fail']) 91 | } catch (e) { 92 | invalidUri = e.message 93 | } 94 | expect(invalidUri).to.have.string('invalid URI', 'Should have thrown for an invalid node URI') 95 | }) 96 | 97 | it('should get node uris from a core if none are passed', async () => { 98 | await submitHashes(hashes) 99 | expect(network.getNodes.called).to.be.true 100 | }) 101 | 102 | it('should send POST request to nodes with hashes in the request body', async () => { 103 | // nock doesn't give us a good way to check the request bodies 104 | // so we need to add a check to each mocked request/response 105 | // by updating a reqBodies object that tracks if the req for each endpoint 106 | // has a body object with the hashes 107 | let reqBodies = {} 108 | nodes.forEach(uri => (reqBodies[uri] = false)) 109 | mockResponses = mockResponses.map(mock => 110 | mock.filteringRequestBody(body => { 111 | // remove the port ":80" at the end of the uri 112 | let uri = mock.basePath.slice(0, -3) 113 | body = JSON.parse(body) 114 | // check the body in the req for this uri has the hashes 115 | reqBodies[uri] = isEqual(body, { hashes }) 116 | }) 117 | ) 118 | 119 | await submitHashes(hashes) 120 | 121 | mockResponses.forEach(resp => expect(resp.isDone()).to.be.true) 122 | nodes.forEach(uri => expect(reqBodies[uri]).to.be.true) 123 | }) 124 | 125 | it('should return mapped proof handles after successful submission', async () => { 126 | sinon.spy(proofs, 'mapSubmitHashesRespToProofHandles') 127 | let testHandles = await submitHashes(hashes) 128 | 129 | expect(proofs.mapSubmitHashesRespToProofHandles.called, 'Did not map to proof handles').to.be.true 130 | 131 | // confirm all are valid proof handles 132 | testHandles.forEach(handle => expect(proofs.isValidProofHandle(handle)).to.be.true) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /tests/submitFile-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | 14 | import fs from 'bfile' 15 | import path from 'path' 16 | import crypto from 'crypto' 17 | import { expect } from 'chai' 18 | import sinon from 'sinon' 19 | 20 | import * as submit from '../lib/submit' 21 | import submitFileHashes from '../lib/submitFiles' 22 | import { helpers } from '../lib/utils' 23 | import nodes from './data/nodes' 24 | 25 | describe('submitFileHashes', () => { 26 | let testPath, file1, file2, content1, content2, hash1, hash2, spy, hashes, paths 27 | before(async () => { 28 | testPath = '/tmp/chainpoint_test' 29 | file1 = path.resolve(testPath, 'file1.txt') 30 | file2 = path.resolve(testPath, 'file2.txt') 31 | paths = [file1, file2] 32 | content1 = 'i am content in file1' 33 | content2 = 'i am content in file2' 34 | await fs.mkdirp(testPath) 35 | fs.writeFileSync(file1, content1) 36 | fs.writeFileSync(file2, content2) 37 | hash1 = crypto 38 | .createHash('sha256') 39 | .update(content1, 'utf8') 40 | .digest() 41 | .toString('hex') 42 | hash2 = crypto 43 | .createHash('sha256') 44 | .update(content2, 'utf8') 45 | .digest() 46 | .toString('hex') 47 | hashes = [hash1, hash2] 48 | }) 49 | 50 | after(async () => { 51 | await fs.remove(testPath) 52 | }) 53 | 54 | beforeEach(() => { 55 | spy = sinon.spy(submit, 'submitHashes') 56 | }) 57 | 58 | afterEach(() => { 59 | sinon.restore() 60 | }) 61 | 62 | it('should run submitHashes', async () => { 63 | await submitFileHashes([file1, file2], nodes) 64 | expect(submit.submitHashes.called).to.be.true 65 | }) 66 | 67 | it('should get hashes of contents of files at given paths', async () => { 68 | sinon.spy(helpers, 'getFileHashes') 69 | 70 | await submitFileHashes(paths, nodes) 71 | expect(spy.withArgs(hashes, nodes).called).to.be.true 72 | expect(helpers.getFileHashes.withArgs(paths).called).to.be.true 73 | }) 74 | it('should return proofHandles with path property matching the path of the submitted hash', async () => { 75 | let proofHandles = await submitFileHashes(paths, nodes) 76 | 77 | proofHandles.forEach(handle => { 78 | expect(handle).to.have.property('path') 79 | expect(handle.path).to.be.oneOf(paths) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /tests/verify-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Tierion 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, 9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | * See the License for the specific language governing permissions and 11 | * limitations under the License. 12 | */ 13 | import { expect } from 'chai' 14 | import sinon from 'sinon' 15 | import nock from 'nock' 16 | 17 | import { verifyProofs } from '../index' 18 | import { network } from '../lib/utils' 19 | import * as evaluate from '../lib/evaluate' 20 | import proofs from './data/proofs' 21 | import uris from './data/nodes' 22 | 23 | describe('verifyProofs', function() { 24 | this.timeout(4000) 25 | // verify just one proof to make calls faster 26 | let proof = proofs.slice(0, 1) 27 | let evaluatedProof = evaluate.evaluateProofs(proof)[0] 28 | let uri = uris[0] 29 | 30 | beforeEach(() => { 31 | nock.cleanAll() 32 | }) 33 | 34 | it('should evaluate proofs', async () => { 35 | sinon.spy(evaluate, 'evaluateProofs') 36 | await verifyProofs(proof) 37 | expect(evaluate.evaluateProofs.called).to.be.true 38 | }) 39 | 40 | it('should take a single node uri and reject invalid uris args', async () => { 41 | let notString, invalidUri 42 | 43 | try { 44 | await verifyProofs(proof, { foo: 'bar' }) 45 | } catch (e) { 46 | notString = true 47 | } 48 | 49 | expect(notString, 'Should have thrown error when passed a non-string as uri arg').to.be.true 50 | 51 | try { 52 | await verifyProofs(proof, 'foo://bar') 53 | } catch (e) { 54 | invalidUri = true 55 | } 56 | 57 | expect(invalidUri, 'Should have thrown error when passed invalid uri').to.be.true 58 | 59 | sinon.stub(network, 'getNodes').callsFake(() => uris) 60 | 61 | await verifyProofs(proof) 62 | 63 | expect(network.getNodes.called).to.be.true 64 | }) 65 | 66 | it('should verify all proofs against a single node at path /calendar/[ANCHOR_ID]/data', async () => { 67 | nock(uri) 68 | .get(`/calendar/${evaluatedProof['anchor_id']}/data`) 69 | .reply(200, [evaluatedProof['expected_value']]) 70 | 71 | await verifyProofs(proof, uri) 72 | expect(nock.isDone()).to.be.true 73 | }) 74 | 75 | it('should throw if no hashes found/returned', async () => { 76 | nock(uri) 77 | .get(`/calendar/${evaluatedProof['anchor_id']}/data`) 78 | .reply(200) 79 | 80 | let noHashFound 81 | try { 82 | await verifyProofs(proof, uri) 83 | } catch (e) { 84 | if (e.message.match('No hashes')) noHashFound = true 85 | } 86 | 87 | expect(noHashFound, 'should have thrown when no hashes found').to.be.true 88 | }) 89 | 90 | it('should return proofs with properties indicated if and when the hash was verified', async () => { 91 | nock(uri) 92 | .get(`/calendar/${evaluatedProof['anchor_id']}/data`) 93 | .reply(200, [evaluatedProof['expected_value']]) 94 | 95 | let verified = await verifyProofs(proof, uri) 96 | let now = new Date() 97 | expect(verified[0].verified).to.be.true 98 | expect(verified[0].verifiedAt).exist 99 | 100 | expect(new Date(verified[0].verifiedAt)).to.be.at.most(now) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const nodeExternals = require('webpack-node-externals') 4 | 5 | let base = { 6 | entry: ['@babel/polyfill', '@ungap/url-search-params', './index.js'], 7 | mode: 'production', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.m?js$/, 12 | exclude: /node_modules/, 13 | use: { 14 | loader: 'babel-loader', 15 | options: { 16 | presets: ['@babel/preset-env'], 17 | plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-transform-regenerator'] 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | 25 | let web = { 26 | entry: ['@babel/polyfill', './index.js'], 27 | target: 'web', 28 | node: { 29 | dgram: 'empty', 30 | fs: 'empty', 31 | net: 'empty', 32 | dns: 'empty', 33 | tls: 'empty' 34 | }, 35 | output: { 36 | path: path.resolve(__dirname, 'dist'), 37 | filename: 'bundle.web.js', 38 | library: 'chainpointClient', 39 | libraryTarget: 'umd', 40 | umdNamedDefine: true 41 | }, 42 | plugins: [ 43 | new webpack.ProvidePlugin({ 44 | chainpointClient: 'chainpointClient' 45 | }) 46 | ] 47 | } 48 | 49 | let node = { 50 | target: 'node', 51 | externals: [nodeExternals()], 52 | output: { 53 | path: path.resolve(__dirname, 'dist'), 54 | filename: 'bundle.js', 55 | library: 'chainpointClient', 56 | libraryTarget: 'umd', 57 | umdNamedDefine: true 58 | } 59 | } 60 | 61 | module.exports = [Object.assign({}, base, web), Object.assign({}, base, node)] 62 | --------------------------------------------------------------------------------