Repository: chainpoint/chainpoint-node Branch: master Commit: 87a35270802f Files: 56 Total size: 383.0 KB Directory structure: gitextract_zhinso_3/ ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── chainpoint-gateway-openapi-3.yaml ├── cloudbuild.yaml ├── docker-compose.yaml ├── init/ │ └── index.js ├── ip-blacklist.txt ├── lib/ │ ├── aggregator.js │ ├── analytics.js │ ├── api-server.js │ ├── cached-proofs.js │ ├── cores.js │ ├── endpoints/ │ │ ├── calendar.js │ │ ├── config.js │ │ ├── hashes.js │ │ ├── proofs.js │ │ └── verify.js │ ├── lightning.js │ ├── logger.js │ ├── models/ │ │ └── RocksDB.js │ ├── parse-env.js │ └── utils.js ├── package.json ├── scripts/ │ ├── install_deps.sh │ ├── prod_secrets_expand.sh │ └── run_prod.sh ├── server.js ├── swarm-compose.yaml └── tests/ ├── RocksDB.js ├── aggregator.js ├── api-server.js ├── cached-proofs.js ├── calendar.js ├── config.js ├── cores.js ├── hashes.js ├── parse-env.js ├── proofs.js ├── sample-data/ │ ├── btc-proof.chp.json │ ├── cal-proof-l.chp.json │ ├── cal-proof.chp.json │ ├── core-btc-proof.chp.json │ ├── core-cal-proof.chp.json │ ├── core-tbtc-proof.chp.json │ ├── core-tcal-proof.chp.json │ ├── lsat-data.json │ ├── tbtc-proof-l.chp.json │ ├── tbtc-proof.chp.json │ └── tcal-proof.chp.json ├── utils.js └── verify.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "env": { "es6": true, "node": true }, "extends": ["eslint:recommended", "plugin:prettier/recommended"], "parserOptions": { "ecmaVersion": 2018 }, "plugins": ["prettier"], "rules": { "prettier/prettier": "error", "linebreak-style": ["error", "unix"], "no-console": "off", "camelcase": [ "error", { "properties": "never", "ignoreDestructuring": true } ] } } ================================================ FILE: .gitignore ================================================ .env .DS_Store .data/ .vscode .vscode/ .nyc_output node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* test.js init/init.json ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "semi": false, "printWidth": 120 } ================================================ FILE: Dockerfile ================================================ # Node.js 8.x LTS on Debian Stretch Linux # see: https://github.com/nodejs/LTS # see: https://hub.docker.com/_/node/ FROM node:12.14.1-stretch LABEL MAINTAINER="Jacob Henderson " # The `node` user and its home dir is provided by # the base image. Create a subdir where app code lives. RUN mkdir /home/node/app WORKDIR /home/node/app COPY package.json yarn.lock server.js /home/node/app/ RUN yarn policies set-version 1.22.10 RUN yarn RUN mkdir -p /home/node/app/scripts COPY ./scripts/*.sh /home/node/app/scripts/ RUN mkdir -p /home/node/app/lib COPY ./lib/*.js /home/node/app/lib/ RUN mkdir -p /home/node/app/lib/endpoints COPY ./lib/endpoints/*.js /home/node/app/lib/endpoints/ RUN mkdir -p /home/node/app/lib/models COPY ./lib/models/*.js /home/node/app/lib/models/ RUN mkdir -p /root/.lnd RUN mkdir -p /root/.chainpoint/gateway/data/rocksdb RUN chmod -R 777 /root EXPOSE 80 CMD ["yarn", "start"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # First target in the Makefile is the default. all: help # without this 'source' won't work. SHELL := /bin/bash # Get the location of this makefile. ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) # Get home directory of current users GATEWAY_DATADIR := $(shell eval printf "~$$USER")/.chainpoint/gateway # Get home directory of current users HOMEDIR := $(shell eval printf "~$$USER") CORE_DATADIR := ${HOMEDIR}/.chainpoint/core UID := $(shell id -u $$USER) GID := $(shell id -g $$USER) .PHONY : help help : Makefile @sed -n 's/^##//p' $< ## logs : Tail Gateway logs .PHONY : logs logs: docker service logs -f chainpoint-gateway_chainpoint-gateway --raw ## up : Start Gateway in dev mode .PHONY : up up: build-config build build-rocksdb docker-compose up -d ## down : Shutdown Gateway .PHONY : down down: docker-compose down ## clean : Shutdown and **destroy** all local Gateway data .PHONY : clean clean: stop @rm -rf ${GATEWAY_DATADIR}/data/rocksdb/* @chmod 777 ${GATEWAY_DATADIR}/data/rocksdb ## burn : Shutdown and **destroy** all local Gateway data .PHONY : burn burn: clean @rm -rf ${HOMEDIR}/.chainpoint/gateway/.lnd @rm -rf init/init.json @docker swarm leave --force || echo "already left swarm" ## restart : Restart Gateway in dev mode .PHONY : restart restart: down up ## build : Build Gateway image .PHONY : build build: docker build -t chainpoint-gateway . docker tag chainpoint-gateway gcr.io/chainpoint-registry/github-chainpoint-chainpoint-gateway:latest docker container prune -f ## build-config : Copy the .env config from .env.sample .PHONY : build-config build-config: @[ ! -f ./.env ] && \ cp .env.sample .env && \ echo 'Copied config .env.sample to .env' || true ## build-rocksdb : Ensure the RocksDB data dir exists .PHONY : build-rocksdb build-rocksdb: @echo Setting up directories... @mkdir -p ${GATEWAY_DATADIR}/data/rocksdb && chmod 777 ${GATEWAY_DATADIR}/data/rocksdb ## pull : Pull Docker images .PHONY : pull pull: docker-compose pull ## git-pull : Git pull latest .PHONY : git-pull git-pull: @git pull --all @git submodule update --init --remote --recursive ## upgrade : Same as `make down && git pull && make up` .PHONY : upgrade upgrade: down git-pull up ## install-deps : Install system dependencies install-deps: scripts/install_deps.sh @echo Please login and logout to enable docker ## init : Bring up yarn, swarm, and generate secrets init: build-rocksdb init-yarn init-swarm ## init-yarn : Initialize dependencies init-yarn: @echo Installing packages... @yarn >/dev/null ## init-swarm : Initialize a docker swarm .PHONY : init-swarm init-swarm: @node ./init/index.js ## init-swarm-restart : Initialize a docker swarm, abandon current configuration .PHONY : init-swarm-restart init-swarm-restart: stop @rm -rf ~/.chainpoint/gateway/.lnd @rm -rf ./init/init.json @node ./init/index.js @docker swarm leave --force || echo "already left swarm" ## init-restart : Bring up yarn, swarm, and generate secrets, abondon current configuration init-restart: build-rocksdb init-yarn init-swarm-restart ## deploy : deploys a swarm stack deploy: set -a && source .env && set +a && export USERID=${UID} && export GROUPID=${GID} && docker stack deploy -c swarm-compose.yaml chainpoint-gateway ## optimize-network: increases number of sockets host can use optimize-network: @sudo sysctl net.core.somaxconn=1024 @sudo sysctl net.ipv4.tcp_fin_timeout=30 @sudo sysctl net.ipv4.tcp_tw_reuse=1 @sudo sysctl net.core.netdev_max_backlog=2000 @sudo sysctl net.ipv4.tcp_max_syn_backlog=2048 ## stop : removes a swarm stack stop: docker stack rm chainpoint-gateway rm -rf ${HOMEDIR}/.chainpoint/gateway/.lnd/tls.* ================================================ FILE: README.md ================================================ # Chainpoint Gateway [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) See [Chainpoint Start](https://github.com/chainpoint/chainpoint-start) for an overview of the Chainpoint Network. A Chainpoint Gateway is a dedicated server for generating many Chainpoint proofs with a single request to the Chainpoint Network. Each Gateway has an integrated Lightning Node running [LND](https://github.com/lightningnetwork/lnd). Gateways use [Lightning Service Authentication Tokens](https://www.npmjs.com/package/lsat-js) (LSATs) to pay Cores an `anchor fee` when submitting a Merkle root. The default anchor fee is 2 [satoshis](). Core operators can set their `anchor fee` to adapt to changing market conditions, and compete to receive transactions from Gateways Gateway setup takes 45 - 90 mins, due to activities that require the automated setup tools to interact with the Bitcoin Blockchain. - Lightning Node sync (10 - 15 minutes) - Funding the Lightning wallet and waiting for 3 confirmations (avg 30 mins) ## Installation ### Requirements The following software is required: - `*Nix-based OS (Ubuntu Linux and MacOS have been tested)` - `BASH` - `Git` - `Docker` A BASH script to install all other dependencies (make, openssl, nodejs, yarn) on Ubuntu and Mac can be run from `make install-deps`. Chainpoint Gateway has been tested with different hardware configurations. Minimum: - `4GB RAM` - `1 CPU Cores` - `128+ GB SSD` - `Public IPv4 address` Mid-Range: - `8GB RAM` - `2 CPU Cores` - `256+ GB SSD` - `Public IPv4 address` ### Deployment Run the following commands to initiate your Gateway: #### Install Dependencies ```bash $ sudo apt-get install make git $ git clone https://github.com/chainpoint/chainpoint-gateway.git $ cd chainpoint-gateway $ make install-deps Logout and login to allow your user to use Docker $ exit ``` #### Configure Gateway ``` $ ssh user@ $ cd chainpoint-gateway $ make init ██████╗██╗ ██╗ █████╗ ██╗███╗ ██╗██████╗ ██████╗ ██╗███╗ ██╗████████╗ ██████╗ █████╗ ████████╗███████╗██╗ ██╗ █████╗ ██╗ ██╗ ██╔════╝██║ ██║██╔══██╗██║████╗ ██║██╔══██╗██╔═══██╗██║████╗ ██║╚══██╔══╝ ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗╚██╗ ██╔╝ ██║ ███████║███████║██║██╔██╗ ██║██████╔╝██║ ██║██║██╔██╗ ██║ ██║ ██║ ███╗███████║ ██║ █████╗ ██║ █╗ ██║███████║ ╚████╔╝ ██║ ██╔══██║██╔══██║██║██║╚██╗██║██╔═══╝ ██║ ██║██║██║╚██╗██║ ██║ ██║ ██║██╔══██║ ██║ ██╔══╝ ██║███╗██║██╔══██║ ╚██╔╝ ╚██████╗██║ ██║██║ ██║██║██║ ╚████║██║ ╚██████╔╝██║██║ ╚████║ ██║ ╚██████╔╝██║ ██║ ██║ ███████╗╚███╔███╔╝██║ ██║ ██║ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ? Will this Gateway use Bitcoin mainnet or testnet? testnet ? Enter your Gateways's Public IP Address: 104.154.83.163 ``` #### Initialize Lightning ``` Initializing Lightning wallet... Create new address for wallet... Creating Docker secrets... **************************************************** Lightning initialization has completed successfully. **************************************************** Lightning Wallet Password: kPlIshurrduurSQoXa LND Wallet Seed: absorb behind drop safe like herp derp celery galaxy wait orient sign suit castle awake gadget pass pipe sudden ethics hill choose six orphan Lightning Wallet Address:tb1qglvlrlg0velrserjuuy7s4uhrsrhuzwgl8hvgm ****************************************************** You should back up this information in a secure place. ****************************************************** TODO: REMOVE Please fund the Lightning Wallet Address above with Bitcoin and wait for 6 confirmations before running 'make deploy' How many Cores would you like to connect to? (max 4) 2 Would you like to specify any Core IPs manually? No You have chosen to connect to 2 Core(s). You will now need to fund you wallet with a minimum amount of BTC to cover costs of the initial channel creation and future Core submissions. ? How many Satoshi to commit to each channel/Core? (min 120000) 500000 ? 500000 per channel will require 1000000 Satoshi total funding. Is this OK? (Y/n) y ************************************************************************************************************** Please send 1000000 Satoshi (0.01 BTC) to your wallet with address tb1qglvlrlg0velrserjuuy7s4uhrsrhuzwgl8hvgm ************************************************************************************************************** This initialization process will now wait until your Lightning node is fully synced and your wallet is funded with at least 400000 Satoshi. The init process should resume automatically. 2020-02-24T17:12:12.244Z> Syncing in progress... currently at block height 1576000 2020-02-24T17:12:42.259Z> Syncing in progress... currently at block height 1596000 2020-02-24T17:13:12.269Z> Syncing in progress... currently at block height 1608000 2020-02-24T17:13:42.279Z> Syncing in progress... currently at block height 1626000 2020-02-24T17:14:12.286Z> Syncing in progress... currently at block height 1650000 2020-02-24T17:14:42.297Z> Syncing in progress... currently at block height 1662000 ***************************************** Your Lightning node is fully synced. ***************************************** *********************************************** Your Lightning wallet is adequately funded. *********************************************** ********************************************************************************* Chainpoint Gateway and integrated Lightning node have been successfully initialized. ********************************************************************************* $ make deploy ``` After running `make deploy`, the Gateway will automatically peer and open Lightning channels with Cores. This may take up to 6 confirmations (~60 minutes). This will allow the Gateway to authenticate with Cores and pay for Anchor Fees. This process may take several minutes upon first run. ## Troubleshooting If your issue isn't addressed here, please [submit an issue](https://github.com/chainpoint/chainpoint-core/issues) to the Chainpoint Core repo. ### Init Problems If `make init` fails and the Lightning wallet hasn't yet been generated and funded, run `make init-restart`, then run `make init` again. If the Lightning wallet has already been generated and funded, you can usually just run `make init` again to continue the initialization process. ### Docker Secrets Gateway uses docker secrets to store sensitive credentials. If you receive a `secret not found` error for either `HOT_WALLET_PASS` or `HOT_WALLET_ADDRESS` while deploying, this can be remedied by using your saved lnd credentials to recreate the secrets: `printf | docker secret create HOT_WALLET_PASS -` or `printf | docker secret create HOT_WALLET_ADDRESS -`. ## Gateway Public API Every Gateway provides a public HTTP API. This is documented in greater detail on the [Gateway HTTP API wiki](https://github.com/chainpoint/chainpoint-gateway/wiki/Gateway-HTTP-API) Additionally, lightning node information for your Gateway can be found at `http:///config`. ## License [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0) ```text Copyright (C) 2017-2020 Tierion Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: chainpoint-gateway-openapi-3.yaml ================================================ openapi: 3.0.0 info: title: 'Chainpoint Node' description: 'Documentation for the Chainpoint Node API' version: '2.0.0' license: name: 'Apache 2.0' url: 'https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)' servers: - url: 'http://35.231.41.69' description: 'Development server (produces testnet proofs)' paths: '/hashes': post: summary: 'Submit one or more hashes for anchoring' operationId: 'submitHashes' requestBody: description: 'An array of hex string hashes to be anchored' required: true content: application/json: schema: $ref: '#/components/schemas/PostHashesRequest' responses: '200': description: 'An array of hash object and supporting meta information for that array' content: 'application/json': schema: $ref: '#/components/schemas/PostHashesResponse' '409': description: 'There was an invalid argument in the request' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' tags: - 'Hashes' '/proofs/{proof_id}': get: summary: 'Retrieves a proof by proof_id' operationId: 'getProof' parameters: - name: 'proof_id' in: 'path' required: true description: 'The proof_id of the proof to retrieve' schema: type: 'string' format: 'uuid' responses: '200': description: 'The requested proof object' content: 'application/json': schema: $ref: '#/components/schemas/GetProofsBase64Response' 'application/vnd.chainpoint.ld+json': schema: $ref: '#/components/schemas/GetProofsJSONResponse' 'application/vnd.chainpoint.json+base64': schema: $ref: '#/components/schemas/GetProofsBase64Response' '409': description: 'There was an invalid argument in the request' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' 'application/vnd.chainpoint.ld+json': schema: $ref: '#/components/schemas/ErrorResponse' 'application/vnd.chainpoint.json+base64': schema: $ref: '#/components/schemas/ErrorResponse' tags: - 'Proofs' '/proofs': get: summary: 'Retrieves one or more proofs by proofids supplied in header' operationId: 'getProofs' parameters: - name: 'proofids' in: 'header' required: true description: 'Comma separated proof_id list of the proofs to retrieve' schema: type: 'string' responses: '200': description: 'An array of the requested proof objects' content: 'application/json': schema: type: 'array' items: $ref: '#/components/schemas/GetProofsBase64Response' 'application/vnd.chainpoint.ld+json': schema: type: 'array' items: $ref: '#/components/schemas/GetProofsJSONResponse' 'application/vnd.chainpoint.json+base64': schema: type: 'array' items: $ref: '#/components/schemas/GetProofsBase64Response' '409': description: 'There was an invalid argument in the request' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' 'application/vnd.chainpoint.ld+json': schema: $ref: '#/components/schemas/ErrorResponse' 'application/vnd.chainpoint.json+base64': schema: $ref: '#/components/schemas/ErrorResponse' tags: - 'Proofs' '/verify': post: summary: 'Submit one or more proofs for verification' operationId: 'verifyProofs' requestBody: description: 'Array of one or more proofs to be verified' required: true content: application/json: schema: $ref: '#/components/schemas/PostVerifyRequest' responses: '200': description: 'An array of the verification results' content: 'application/json': schema: type: array items: $ref: '#/components/schemas/PostVerifyResponse' '409': description: 'There was an invalid argument in the request' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' tags: - 'Verify' '/calendar/{tx_id}/data': get: summary: 'Retrieves the the data embedded in a calendar transaction' operationId: 'getCalendarTxData' parameters: - name: 'tx_id' in: 'path' required: true description: 'The calendar transaction id from which to retrieve the embedded data' schema: type: 'string' responses: '200': description: 'The data value embedded within the calendar transaction' content: 'application/json': schema: type: 'string' example: 'f18bf0968b224f73528d99cc83ca9e79d467f34875e85f36e2c1f074ff2dc657' '404': description: 'The requested transaction was not found' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' '409': description: 'There was an invalid argument in the request' content: 'application/json': schema: $ref: '#/components/schemas/ErrorResponse' tags: - 'Calendar' '/config': get: summary: 'Retrieves some basic information for the Node' operationId: 'getNodeConfig' responses: '200': description: 'Basic information about the Node and it''s environment' content: 'application/json': schema: $ref: '#/components/schemas/GetConfigResponse' tags: - 'Config' components: schemas: PostHashesRequest: type: 'object' properties: hashes: type: 'array' items: type: 'string' example: '1957db7fe23e4be1740ddeb941ddda7ae0a6b782e536a9e00b5aa82db1e84547' pattern: '^([a-fA-F0-9]{2}){20,64}$' minLength: 40 maxLength: 128 PostHashesResponse: type: 'object' properties: meta: type: 'object' properties: submitted_at: type: 'string' format: 'date-time' example: '2017-05-02T15:16:44Z' processing_hints: type: 'object' properties: cal: type: 'string' format: 'date-time' example: '2017-05-02T15:17:44Z' btc: type: 'string' format: 'date-time' example: '2017-05-02T16:17:44Z' hashes: type: 'array' items: type: 'object' properties: proof_id: type: 'string' example: '5a001650-2f4a-11e7-ad22-37b426116bc4' hash: type: 'string' example: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89' GetProofsJSONResponse: type: 'object' properties: proof_id: type: 'string' example: '577c6c90-78d5-11e9-9c57-010a193d9f8c' proof: type: 'object' example: '@context': 'https://w3id.org/chainpoint/v3' type: 'Chainpoint' hash: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89' proof_id: '577c6c90-78d5-11e9-9c57-010a193d9f8c' hash_received: '2019-05-17T18:55:30Z' branches: - label: 'cal_anchor_branch' ops: - l: 'node_id:577c6c90-78d5-11e9-9c57-010a193d9f8c' - op: 'sha-256' - l: 'core_id:5a22fb80-78d5-11e9-8186-01d1f712eccc' - op: 'sha-256' - l: 'nistv2:1558119240000:eb591780782f746fda5e7ac8011064fda657ae451bd1ae6b71e2f5d7e24e9d49bdc25db6d901ccf8736bbf135c451d1edc9c6065b577d69f3fd9be6a1a8d0763' - op: 'sha-256' - l: '1766c5a6c10cf8ae5cce76c6d89cb9bc8696a2acf8e7ed4dbe05a71802cae38a' - op: 'sha-256' - anchors: - type: 'cal' anchor_id: 'b220c0443b5f8b1394a38a102892590b4c21d8ad1382cd9e4d59b9834f6a769f' uris: - 'http://35.245.9.90/calendar/b220c0443b5f8b1394a38a102892590b4c21d8ad1382cd9e4d59b9834f6a769f/data' anchors_complete: type: 'array' items: type: 'string' example: 'cal' GetProofsBase64Response: type: 'object' properties: proof_id: type: 'string' example: '577c6c90-78d5-11e9-9c57-010a193d9f8c' proof: type: 'string' example: 'eJyNk79u1EAQh1+Gksvt7P91dRKvQJXmNDszy1k67JPtBCgDDW0KOpqQoAREg4QoeY97G+y7S4AAUrbzrr5vfyP/9u3NgtpmkJfDj9UwbPpqPn9haj5qu2dzWmHdbNq6Gean5mp4tZHPT+62rlbYr7YLAOKIJiqJ7AobJBsISozKJGUssFUZwEYnESVZz7GgYet90aZITF8mzbLmZdOybB+5EMhTUrMw6mYAkmaJXJgpUAjJcCqRvu+Q/iQ/r4dB9uQSh29aQZqpkQpPIVbOVUYd3+mp7SY9al1y/F0fIfpRz1ACaCH6Sz+R/9bb45vcYUMr6c9ff1xjlvVXwvVy2mq75f7sst30788u1tvHu5w1Vw+Z8exDu7nuVzjTzu/gXYoJfsAE9+F3Td0Pp7oC5yJA0laNq5LsEoSoQtQlWF8YnQSkqACUt+OndwHFOsgMKD4HEF0cB9FWEtuUmbTj7DkpICoxGJ9zAeNoZBiEKZFX3uVxXvapmMIpi0fAyCp4cz/lAoL35NATqFGH4ogkeBorkyinTNEnjxrHMwnClrMohwGi0oRiIt4XJqO10irezm1MqKC6bXk++lXvqe3V+OeqA3F20W0XQRBJMhoxbABRUzBG5zFkZBNztihZNEDBWGL23hvtQhFO1hSXAvwR53rfif78ze4dXY6XfTrUpObrw7VXJ13dn2+P/hdxPlLSMHbzAzCfqvoTAAQ82A==' anchors_complete: type: 'array' items: type: 'string' example: 'cal' PostVerifyRequest: type: 'object' properties: proofs: type: 'array' items: type: 'string' example: 'eJyNk79u1EAQh1+Gksvt7P91dRKvQJXmNDszy1k67JPtBCgDDW0KOpqQoAREg4QoeY97G+y7S4AAUrbzrr5vfyP/9u3NgtpmkJfDj9UwbPpqPn9haj5qu2dzWmHdbNq6Gean5mp4tZHPT+62rlbYr7YLAOKIJiqJ7AobJBsISozKJGUssFUZwEYnESVZz7GgYet90aZITF8mzbLmZdOybB+5EMhTUrMw6mYAkmaJXJgpUAjJcCqRvu+Q/iQ/r4dB9uQSh29aQZqpkQpPIVbOVUYd3+mp7SY9al1y/F0fIfpRz1ACaCH6Sz+R/9bb45vcYUMr6c9ff1xjlvVXwvVy2mq75f7sst30788u1tvHu5w1Vw+Z8exDu7nuVzjTzu/gXYoJfsAE9+F3Td0Pp7oC5yJA0laNq5LsEoSoQtQlWF8YnQSkqACUt+OndwHFOsgMKD4HEF0cB9FWEtuUmbTj7DkpICoxGJ9zAeNoZBiEKZFX3uVxXvapmMIpi0fAyCp4cz/lAoL35NATqFGH4ogkeBorkyinTNEnjxrHMwnClrMohwGi0oRiIt4XJqO10irezm1MqKC6bXk++lXvqe3V+OeqA3F20W0XQRBJMhoxbABRUzBG5zFkZBNztihZNEDBWGL23hvtQhFO1hSXAvwR53rfif78ze4dXY6XfTrUpObrw7VXJ13dn2+P/hdxPlLSMHbzAzCfqvoTAAQ82A==' minItems: 1 maxItems: 1000 PostVerifyResponse: type: 'object' properties: proof_index: type: 'integer' example: 0 hash: type: 'string' example: '11cd8a380e8d5fd3ac47c1f880390341d40b11485e8ae946d8fa3d466f23fe89' proof_id: type: 'string' example: '577c6c90-78d5-11e9-9c57-010a193d9f8c' hash_received: type: 'string' format: 'date-time' example: '2019-05-17T18:55:30Z' anchors: type: 'array' items: type: 'object' properties: branch: type: 'string' example: 'cal_anchor_branch' type: type: 'string' example: 'cal' valid: type: 'boolean' example: true status: type: 'string' example: 'verified' GetConfigResponse: type: 'object' properties: version: type: 'string' example: '2.0.0' time: type: 'string' format: 'date-time' example: '2019-05-17T19:53:49.140Z' ErrorResponse: type: 'object' properties: code: type: 'string' message: type: 'string' tags: - name: 'Hashes' description: 'Your hashes to be anchored' - name: 'Proofs' description: 'Your Chainpoint proofs created for each of your hashes' - name: 'Verify' description: 'Verification process for your proofs' - name: 'Calendar' description: 'Chainpoint calendar transaction data' - name: 'Config' description: 'Configuration information about the Node' externalDocs: description: 'Find out more about Chainpoint' url: 'https://chainpoint.org' ================================================ FILE: cloudbuild.yaml ================================================ steps: - name: 'gcr.io/cloud-builders/git' args: ['submodule', 'update', '--init', '--recursive'] - name: 'gcr.io/cloud-builders/docker' args: [ 'build', '-f', 'Dockerfile', '-t', 'gcr.io/chainpoint-registry/$REPO_NAME:$COMMIT_SHA', '-t', 'gcr.io/chainpoint-registry/$REPO_NAME:latest', '.' ] id: 'chainpoint-gateway' timeout: 1000s images: - 'gcr.io/chainpoint-registry/$REPO_NAME:latest' - 'gcr.io/chainpoint-registry/$REPO_NAME:$COMMIT_SHA' options: machineType: 'N1_HIGHCPU_8' ================================================ FILE: docker-compose.yaml ================================================ version: '3.4' networks: chainpoint-gateway: driver: bridge services: chainpoint-gateway: restart: on-failure volumes: - ./ip-blacklist.txt:/home/node/app/ip-blacklist.txt:ro - ~/.chainpoint/gateway/data/rocksdb:/root/.chainpoint/gateway/data/rocksdb - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro build: . container_name: chainpoint-gateway ports: # - '${PORT}:${CHAINPOINT_NODE_PORT}' - '80:8080' networks: - chainpoint-gateway environment: HOME: /root HOT_WALLET_PASS: ${HOT_WALLET_PASS} HOT_WALLET_ADDRESS: ${HOT_WALLET_ADDRESS} LND_SOCKET: ${LND_SOCKET} LND_MACAROON: ${LND_MACAROON} LND_TLS_CERT: ${LND_TLS_CERT} CHAINPOINT_CORE_CONNECT_IP_LIST: '${CHAINPOINT_CORE_CONNECT_IP_LIST}' PORT: '${PORT:-80}' AGGREGATION_INTERVAL_SECONDS: '${AGGREGATION_INTERVAL_SECONDS}' PROOF_EXPIRE_MINUTES: '${PROOF_EXPIRE_MINUTES}' CHAINPOINT_NODE_PORT: '${CHAINPOINT_NODE_PORT:-9090}' POST_HASHES_MAX: '${POST_HASHES_MAX}' POST_VERIFY_PROOFS_MAX: '${POST_VERIFY_PROOFS_MAX}' GET_PROOFS_MAX: '${GET_PROOFS_MAX}' MAX_SATOSHI_PER_HASH: '${MAX_SATOSHI_PER_HASH}' NETWORK: ${NETWORK} NODE_ENV: ${NODE_ENV} CHANNEL_AMOUNT: ${CHANNEL_AMOUNT} FUND_AMOUNT: ${FUND_AMOUNT} NO_LSAT_CORE_WHITELIST: ${NO_LSAT_CORE_WHITELIST} GOOGLE_UA_ID: '' PUBLIC_IP: ${LND_PUBLIC_IP} tty: true # Lightning node lnd: image: tierion/lnd:${NETWORK:-testnet}-0.9.2 user: ${USERID}:${GROUPID} entrypoint: './start-lnd.sh' container_name: lnd-node ports: - target: 8080 published: 8080 protocol: tcp mode: host - target: 9735 published: 9735 protocol: tcp mode: host - target: 10009 published: 10009 protocol: tcp mode: host restart: always environment: - PUBLICIP=${LND_PUBLIC_IP} - RPCUSER - RPCPASS - NETWORK=${NETWORK:-testnet} - CHAIN - DEBUG=info - BACKEND=neutrino - NEUTRINO=faucet.lightning.community:18333 - LND_REST_PORT - LND_RPC_PORT - TLSPATH - TLSEXTRADOMAIN=lnd volumes: - ~/.chainpoint/gateway/.lnd:/root/.lnd:z networks: - chainpoint-gateway # ln-accounting # Returns accounting reports in harmony format for lnd node ln-accounting: image: tierion/ln-accounting ports: - '9000' environment: NETWORK: ${NETWORK} LND_DIR: /root/.lnd LND_SOCKET: ${LND_SOCKET} ACCOUNTING_PORT: ${ACCOUNTING_PORT:-9000} volumes: - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro networks: - chainpoint-gateway ================================================ FILE: init/index.js ================================================ /* Copyright (C) 2019 Tierion * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ const inquirer = require('inquirer') const validator = require('validator') const chalk = require('chalk') const exec = require('executive') const generator = require('generate-password') const lightning = require('../lib/lightning') const home = require('os').homedir() const fs = require('fs') const path = require('path') const utils = require('../lib/utils.js') const _ = require('lodash') const rp = require('request-promise-native') const retry = require('async-retry') const QUIET_OUTPUT = true const LND_SOCKET = '127.0.0.1:10009' const MIN_CHANNEL_SATOSHI = 100000 const CHANNEL_OPEN_OVERHEAD_SAFE = 20000 const CORE_SEED_IPS_MAINNET = ['18.220.31.138'] const CORE_SEED_IPS_TESTNET = ['3.133.119.65', '52.14.49.31', '3.135.54.225'] const initQuestionConfig = [ { type: 'list', name: 'NETWORK', message: 'Will this Gateway use Bitcoin mainnet or testnet?', choices: [ { name: 'Mainnet', value: 'mainnet' }, { name: 'Testnet', value: 'testnet' } ], default: 'mainnet' }, { type: 'input', name: 'LND_PUBLIC_IP', message: "Enter your Node's Public IP Address:", validate: input => { if (input) { return validator.isIP(input, 4) } else { return true } } } ] function displayTitleScreen() { const txt = ` ██████╗██╗ ██╗ █████╗ ██╗███╗ ██╗██████╗ ██████╗ ██╗███╗ ██╗████████╗ ██╔════╝██║ ██║██╔══██╗██║████╗ ██║██╔══██╗██╔═══██╗██║████╗ ██║╚══██╔══╝ ██║ ███████║███████║██║██╔██╗ ██║██████╔╝██║ ██║██║██╔██╗ ██║ ██║ ██║ ██╔══██║██╔══██║██║██║╚██╗██║██╔═══╝ ██║ ██║██║██║╚██╗██║ ██║ ╚██████╗██║ ██║██║ ██║██║██║ ╚████║██║ ╚██████╔╝██║██║ ╚████║ ██║ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ` const gateway = ` ██████╗ █████╗ ████████╗███████╗██╗ ██╗ █████╗ ██╗ ██╗ ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗╚██╗ ██╔╝ ██║ ███╗███████║ ██║ █████╗ ██║ █╗ ██║███████║ ╚████╔╝ ██║ ██║██╔══██║ ██║ ██╔══╝ ██║███╗██║██╔══██║ ╚██╔╝ ╚██████╔╝██║ ██║ ██║ ███████╗╚███╔███╔╝██║ ██║ ██║ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ` console.log('\n') console.log(chalk.dim.magenta(txt)) console.log(chalk.dim.magenta(gateway)) console.log('\n') } async function startLndNodeAsync(initAnswers) { try { let uid = (await exec.quiet('id -u $USER')).stdout.trim() let gid = (await exec.quiet('id -g $USER')).stdout.trim() console.log(chalk.yellow(`Starting Lightning node...`)) await exec.quiet([ `docker-compose pull lnd && mkdir -p ${home}/.chainpoint/gateway/.lnd && export USERID=${uid} && export GROUPID=${gid} && export NETWORK=${initAnswers.NETWORK} && export PUBLICIP=${initAnswers.LND_PUBLIC_IP} && docker-compose run -d --service-ports lnd` ]) } catch (error) { throw new Error(`Could not start Lightning node : ${error.message}`) } await utils.sleepAsync(20000) } async function initializeLndNodeAsync(initAnswers) { await startLndNodeAsync(initAnswers) let walletSecret = generator.generate({ length: 20, numbers: false }) try { console.log(chalk.yellow(`Initializing Lightning wallet...`)) let lnd = new lightning(LND_SOCKET, initAnswers.NETWORK, true, true) let seed = await lnd.callMethodRawAsync('unlocker', 'genSeedAsync', {}, true) await lnd.callMethodRawAsync('unlocker', 'initWalletAsync', { wallet_password: walletSecret, cipher_seed_mnemonic: seed.cipher_seed_mnemonic }) await utils.sleepAsync(10000) console.log(chalk.yellow(`Create new address for wallet...`)) lnd = new lightning(LND_SOCKET, initAnswers.NETWORK, false, true) let newAddress = await lnd.callMethodAsync('lightning', 'newAddressAsync', { type: 0 }, walletSecret) return { cipherSeedMnemonic: seed.cipher_seed_mnemonic, newAddress: newAddress.address, walletSecret: walletSecret } } catch (error) { throw new Error(`Could not initialize Lightning wallet : ${error.message}`) } } async function createDockerSecretsAsync(initAnswers, walletInfo) { try { console.log(chalk.yellow('Creating Docker secrets...')) await exec.quiet([`docker swarm init --advertise-addr=${initAnswers.LND_PUBLIC_IP}`]) await utils.sleepAsync(2000) // wait for swarm to initialize await exec.quiet([ `printf ${walletInfo.walletSecret} | docker secret create HOT_WALLET_PASS -`, `printf '${walletInfo.cipherSeedMnemonic.join(' ')}' | docker secret create HOT_WALLET_SEED -`, `printf ${walletInfo.newAddress} | docker secret create HOT_WALLET_ADDRESS -` ]) } catch (error) { throw new Error(`Could not create Docker secrets : ${error.message}`) } } function displayInitResults(walletInfo) { console.log(chalk.green(`\n****************************************************`)) console.log(chalk.green(`Lightning initialization has completed successfully.`)) console.log(chalk.green(`****************************************************\n`)) console.log(chalk.yellow(`Lightning Wallet Password: `) + walletInfo.walletSecret) console.log(chalk.yellow(`Lightning Wallet Seed: `) + walletInfo.cipherSeedMnemonic.join(' ')) console.log(chalk.yellow(`Lightning Wallet Address:`) + walletInfo.newAddress) console.log(chalk.magenta(`\n******************************************************`)) console.log(chalk.magenta(`You should back up this information in a secure place.`)) console.log(chalk.magenta(`******************************************************\n\n`)) console.log( chalk.green( `\nPlease fund the Lightning Wallet Address above with Bitcoin and wait for 6 confirmation before running 'make deploy'\n` ) ) } async function setENVValuesAsync(newENVData) { // check for existence of .env file let envFileExists = fs.existsSync(path.resolve(__dirname, '../', '.env')) // load .env file if it exists, otherwise load the .env.sample file let envContents = fs.readFileSync(path.resolve(__dirname, '../', `.env${!envFileExists ? '.sample' : ''}`)).toString() let updatedEnvContents = Object.keys(newENVData).reduce((contents, key) => { let regexMatch = new RegExp(`^${key}=.*`, 'gim') if (!contents.match(regexMatch)) return contents + `\n${key}=${newENVData[key]}` return contents.replace(regexMatch, `${key}=${newENVData[key]}`) }, envContents) fs.writeFileSync(path.resolve(__dirname, '../', '.env'), updatedEnvContents) } async function getCorePeerListAsync(seedIPs) { seedIPs = _.shuffle(seedIPs) let peersReceived = false while (!peersReceived && seedIPs.length > 0) { let targetIP = seedIPs.pop() let options = { uri: `http://${targetIP}/peers`, method: 'GET', json: true, gzip: true, resolveWithFullResponse: true } try { let response = await retry(async () => await rp(options), { retries: 3 }) return response.body.concat([targetIP]) } catch (error) { console.log(`Core IP ${targetIP} not repsonding to peers requests`) } } throw new Error('Unable to retrieve Core peer list') } async function askCoreConnectQuestionsAsync(progress) { let peerList = _.shuffle( await getCorePeerListAsync(progress.network === 'maininet' ? CORE_SEED_IPS_MAINNET : CORE_SEED_IPS_TESTNET) ) let peerCount = peerList.length const coreConnectQuestion = [ { type: 'number', name: 'CORE_COUNT', message: `Connecting with more Cores improves the reliability but is more expensive (2 is recommended).\nHow many Cores would you like to connect to? (max ${peerCount})`, validate: input => input > 0 && input <= peerCount }, { type: 'confirm', name: 'MANUAL_IP', message: 'Would you like to specify any Core IPs manually?', default: false } ] let coreConnectAnswers = await inquirer.prompt(coreConnectQuestion) let coreConnectIPs = [] if (coreConnectAnswers.MANUAL_IP) { let manualCount = await inquirer.prompt({ type: 'number', name: 'TOTAL', message: `How many Core IPs would you like to specify manually? (max ${coreConnectAnswers.CORE_COUNT})`, validate: input => input > 0 && input <= coreConnectAnswers.CORE_COUNT }) for (let x = 0; x < manualCount.TOTAL; x++) { let manualInput = await inquirer.prompt({ type: 'input', name: 'IP', message: `Enter Core IP manual entry #${x + 1}:`, validate: input => peerList.includes(input) && !coreConnectIPs.includes(input) }) coreConnectIPs.push(manualInput.IP) } } let randomCoreIPCount = coreConnectAnswers.CORE_COUNT - coreConnectIPs.length let unusedPeers = peerList.filter(ip => !coreConnectIPs.includes(ip)) for (let x = 0; x < randomCoreIPCount; x++) coreConnectIPs.push(unusedPeers.pop()) // Update ENV file with core IP list await setENVValuesAsync({ CHAINPOINT_CORE_CONNECT_IP_LIST: coreConnectIPs.join(',') }) let coreLNDUris = [] for (let coreIP of coreConnectIPs) { let options = { uri: `http://${coreIP}/status`, method: 'GET', json: true, gzip: true, resolveWithFullResponse: true } try { let response = await retry(async () => await rp(options), { retries: 3 }) coreLNDUris.push(response.body.uris[0]) } catch (error) { throw new Error(`Unable to retrive status of Core at ${coreIP}`) } } progress.coreLNDUris = coreLNDUris writeInitProgress(progress) return coreLNDUris } async function askFundAmountAsync(progress) { const coreConnectCount = progress.coreLNDUris.length console.log(chalk.yellow(`\nYou have chosen to connect to ${coreConnectCount} Core(s).`)) console.log( chalk.yellow( 'You will now need to fund your wallet with a minimum amount of BTC to cover costs of the initial Lightning channel(s) creation and future Core submissions.\nThe init process will wait for your funding to confirm with the Bitcoin Network.' ) ) const minAmount = MIN_CHANNEL_SATOSHI + CHANNEL_OPEN_OVERHEAD_SAFE let finalFundAmount = null let finalChannelAmount = null while (finalFundAmount === null) { const fundQuestion1 = [ { type: 'number', name: 'AMOUNT', message: `How many Satoshi to commit to each channel/Core? (min ${minAmount})`, validate: input => input >= minAmount } ] let fundAnswer1 = await inquirer.prompt(fundQuestion1) const totalFundsNeeded = fundAnswer1.AMOUNT * coreConnectCount const fundQuestion2 = [ { type: 'confirm', name: 'AGREE', message: `${fundAnswer1.AMOUNT} per channel will require ${totalFundsNeeded} Satoshi total funding. Is this OK?`, default: true } ] let fundAnswer2 = await inquirer.prompt(fundQuestion2) if (fundAnswer2.AGREE) { finalChannelAmount = fundAnswer1.AMOUNT finalFundAmount = totalFundsNeeded } } console.log( chalk.magenta( `\n**************************************************************************************************************` ) ) console.log( chalk.magenta( `Please send ${finalFundAmount} Satoshi (${finalFundAmount / 10 ** 8} BTC) to your wallet with address ${ progress.walletAddress }` ) ) console.log( chalk.magenta( `**************************************************************************************************************\n` ) ) progress.finalFundAmount = finalFundAmount progress.finalChannelAmount = finalChannelAmount writeInitProgress(progress) return progress } async function waitForSyncAndFundingAsync(progress) { console.log( chalk.yellow( `This initialization process will now wait until your Lightning node is fully synced and your wallet is funded with at least ${progress.finalFundAmount} Satoshi. The init process should resume automatically. \n` ) ) let isSynced = false let isFunded = false let lnd = new lightning(LND_SOCKET, progress.network, false, true) while (!isSynced) { try { let info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, progress.walletSecret) if (info.synced_to_chain) { console.log(chalk.green('\n*****************************************')) console.log(chalk.green('Your lightning node is fully synced.')) console.log(chalk.green('*****************************************')) isSynced = true } else { console.log( chalk.magenta( `${new Date().toISOString()}> Syncing in progress... currently at block height ${info.block_height}` ) ) } } catch (error) { console.log(chalk.red(`An error occurred while checking node state : ${error.message}`)) } finally { if (!isSynced) await utils.sleepAsync(30000) } } while (!isFunded) { try { let balance = await lnd.callMethodAsync('lightning', 'walletBalanceAsync', null, progress.walletSecret) if (balance.confirmed_balance >= progress.finalFundAmount) { console.log(chalk.green('\n***********************************************')) console.log(chalk.green('Your lightning wallet is adequately funded.')) console.log(chalk.green('***********************************************\n')) console.log( chalk.yellow( 'Your wallet may require up to 5 more confirmations (~60 Minutes) before your gateway can open payment channels to submit hashes\n' ) ) isFunded = true } else { console.log( chalk.magenta( `${new Date().toISOString()}> Awaiting funds for wallet... wallet has a current balance of ${ balance.confirmed_balance }` ) ) } } catch (error) { console.log(chalk.red(`An error occurred while checking wallet balance : ${error.message}`)) } finally { if (!isFunded) await utils.sleepAsync(30000) } } } function displayFinalConnectionSummary() { console.log(chalk.green('\n*********************************************************************************')) console.log(chalk.green('Chainpoint Gateway and supporting Lighning node have been successfully initialized.')) console.log(chalk.green('*********************************************************************************\n')) } function readInitProgress() { // check for existence of .init file let initFileExists = fs.existsSync(path.resolve(__dirname, './init.json')) // load .init file if it exists if (initFileExists) return JSON.parse(fs.readFileSync(path.resolve(__dirname, './init.json')).toString()) return {} } function writeInitProgress(progress) { fs.writeFileSync(path.resolve(__dirname, './init.json'), JSON.stringify(progress, null, 2)) } function setInitProgressCompleteAsync() { fs.writeFileSync(path.resolve(__dirname, './init.json'), JSON.stringify({ complete: true }, null, 2)) } async function start() { try { // Check if init has already recorded some progress // This allows recovery from last known step in process // Exit of initialization has already completed successfully let progress = await readInitProgress() if (progress.complete) { console.log(chalk.green('Initialization has already been completed successfully.')) return } // Display the title screen displayTitleScreen() if (!progress.walletAddress) { // Ask initialization questions let initAnswers = await inquirer.prompt(initQuestionConfig) // Initialize the LND wallet and create a new address let walletInfo = await initializeLndNodeAsync(initAnswers) // Store relevant values as Docker secrets await createDockerSecretsAsync(initAnswers, walletInfo) // Update the .env file with generated data await setENVValuesAsync(initAnswers) // Display the generated wallet information to the user displayInitResults(walletInfo) progress.network = initAnswers.NETWORK progress.walletAddress = walletInfo.newAddress progress.walletSecret = walletInfo.walletSecret writeInitProgress(progress) } else { await startLndNodeAsync({ NETWORK: progress.network }) } if (!progress.coreLNDUris) { // Determine which Core(s) to connect to let coreLNDUris = await askCoreConnectQuestionsAsync(progress) progress.coreLNDUris = coreLNDUris } // Ask funding questions if (!progress.finalChannelAmount > 0) { progress = await askFundAmountAsync(progress) } // Wait for sync and wallet funding await waitForSyncAndFundingAsync(progress) await setENVValuesAsync({ CHANNEL_AMOUNT: progress.finalChannelAmount, FUND_AMOUNT: progress.finalFundAmount }) await displayFinalConnectionSummary() // Initialization complete, mark progress file as such setInitProgressCompleteAsync() } catch (error) { console.error(chalk.red(`An unexpected error has occurred : ${error.message}. Please run 'make init' again.`)) } finally { try { console.log(chalk.yellow(`Shutting down Lightning node...`)) await exec([`docker-compose down`], { quiet: QUIET_OUTPUT }) console.error(chalk.yellow(`Shutdown complete`)) } catch (error) { console.error(chalk.red(`Unable to shut down Lightning node : ${error.message}`)) } } } start() ================================================ FILE: ip-blacklist.txt ================================================ # ip-blacklist.txt # # Add a single IPv4 address per line that you'd # like to block from connecting to this Node. # # Lines beginning with '#' are ignored. ================================================ FILE: lib/aggregator.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const MerkleTools = require('merkle-tools') const uuidTime = require('uuid-time') let cores = require('./cores.js') const BLAKE2s = require('blake2s-js') let rocksDB = require('./models/RocksDB.js') const logger = require('./logger.js') const env = require('./parse-env.js').env const utils = require('./utils.js') const { UlidMonotonic } = require('id128') // a boolean value indicating whether or not aggregateSubmitAndPersistAsync is currently running let AGG_IN_PROCESS = false // The merkle tools object for building trees and generating proof paths const merkleTools = new MerkleTools() async function getSubmittedHashData() { let submittedHashData = [[], []] try { submittedHashData = await rocksDB.getIncomingHashesUpToAsync(Date.now()) } catch (error) { logger.error('Could not read submitted hash data') return [[], []] } return submittedHashData } // Build a merkle tree from HashData queued in RocksDB, submit the root to Core, persist resulting state data async function aggregateSubmitAndPersistAsync() { // Return if the previous call to this function has not yet completed if (AGG_IN_PROCESS) { return } else { AGG_IN_PROCESS = true } let [hashesForTree, hashDataForTreeDeleteOps] = await getSubmittedHashData() let aggregationRoot = null if (hashesForTree.length > 0) { // clear the merkleTools instance to prepare for a new tree merkleTools.resetTree() // concatenate and hash the hash ids and hash values into new array let leaves = hashesForTree.map(hashObj => { return Buffer.from(hashObj.hash, 'hex') }) // Add every hash in hashesForTree to new Merkle tree merkleTools.addLeaves(leaves) merkleTools.makeTree() let nodeProofDataItems = [] aggregationRoot = merkleTools.getMerkleRoot().toString('hex') let treeSize = merkleTools.getLeafCount() for (let x = 0; x < treeSize; x++) { // push the hash_id and corresponding proof onto the array let nodeProofDataItem = {} nodeProofDataItem.proofId = hashesForTree[x].proof_id nodeProofDataItem.hash = hashesForTree[x].hash nodeProofDataItem.proofState = merkleTools.getProof(x, true) nodeProofDataItems.push(nodeProofDataItem) } // submit merkle root to Core try { let submitResults = await submitHashToCoresAsync(aggregationRoot) if (submitResults.length === 0) throw new Error(`Unable to submit hash to Core : No Cores responded`) submitResults = submitResults.filter(submitResult => { // Log all proofId values returned by Cores logger.info( `Aggregator : Core IP : ${submitResult.coreIP} : proofId : ${submitResult.proofId} : ${JSON.stringify( submitResult )} : ${hashesForTree.length}` ) if (utils.isULID(submitResult.proofId)) { try { UlidMonotonic.fromCanonical(submitResult.proofId) return true } catch (error) { logger.error(`unable to validate ProofID ulid ${error.message}`) } } else if (utils.isUUID(submitResult.proofId)) { // validate BLAKE2s let hashTimestampMS = parseInt(uuidTime.v1(submitResult.proofId)) let h = new BLAKE2s(32, { personalization: Buffer.from('CHAINPNT') }) let hashStr = [ hashTimestampMS.toString(), hashTimestampMS.toString().length, submitResult.hash, submitResult.hash.length ].join(':') h.update(Buffer.from(hashStr)) let expectedData = Buffer.concat([Buffer.from([0x01]), h.digest().slice(27)]).toString('hex') let embeddedData = submitResult.proofId.slice(24) if (embeddedData == expectedData) { return true } else { logger.error( `Aggregator : Submit : ProofID UUID from Core refused : Cannot validate embedded BLAKE2s data : ${embeddedData} != ${expectedData}` ) logger.error(`hashStr was ${hashStr}`) } } return false }) if (submitResults.length === 0) throw new Error(`Unable to submit hash to Core : No Cores responded with valid HashID`) // add the submission info including core IP and proofId values from Cores for each item in proofDataItems let submitId = UlidMonotonic.generate().toCanonical() // the identifier for all hashes submitted in this batch let coreInfo = submitResults.map(submitResult => { return { ip: submitResult.coreIP, proofId: submitResult.proofId } }) nodeProofDataItems = nodeProofDataItems.map(nodeProofDataItem => { nodeProofDataItem.submission = { submitId: submitId, cores: coreInfo } return nodeProofDataItem }) // persist these proofDataItems to storage try { await rocksDB.saveProofStatesBatchAsync(nodeProofDataItems) } catch (error) { throw new Error(`Unable to persist proof state data to disk : ${error.message}`) } try { // Submission to Core was successful, purge hashes that were delivered from RocksDB await rocksDB.deleteBatchAsync(hashDataForTreeDeleteOps) } catch (error) { logger.warn(`Aggregator : Submit to Core : Could not purge submitted hashes : ${error.message}`) } let submittedIPs = submitResults.map(item => item.coreIP).toString() logger.info(`Aggregator : ${hashesForTree.length} hash(es) : Core IPs : ${submittedIPs} `) } catch (error) { logger.error(`Aggregator : Submit : ${error.message}`) if (error.stack) { logger.error(`Stacktrace: ${error.stack}`) } } } AGG_IN_PROCESS = false return aggregationRoot } async function submitHashToCoresAsync(hash) { let response = await cores.submitHashAsync(hash) if (response.length == 0) { throw new Error('No Response from any Core') } return response.map(item => { return { coreIP: item.ip, proofId: item.response.proof_id, hash: item.response.hash } }) } function startAggInterval() { return setInterval(aggregateSubmitAndPersistAsync, env.AGGREGATION_INTERVAL_SECONDS * 1000) } module.exports = { startAggInterval: startAggInterval, // additional functions for testing purposes aggregateSubmitAndPersistAsync: aggregateSubmitAndPersistAsync, setRocksDB: db => { rocksDB = db }, setCores: c => { cores = c } } ================================================ FILE: lib/analytics.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables let env = require('./parse-env.js').env let ua = require('universal-analytics') const logger = require('./logger.js') let visitor if (env.GOOGLE_UA_ID) { visitor = ua(env.GOOGLE_UA_ID, env.PUBLIC_IP, { strictCidFormat: false }) logger.info(`Setup analytics for analytics id ${env.GOOGLE_UA_ID}`) } function setClientID(clientID) { if (env.GOOGLE_UA_ID) { visitor = ua(env.GOOGLE_UA_ID, clientID, { strictCidFormat: false }) } } function sendEvent(params) { if (params && visitor) { logger.info(`Sending event ${params.ea}`) visitor.event(params).send() } } module.exports = { setClientID: setClientID, sendEvent: sendEvent } ================================================ FILE: lib/api-server.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const restify = require('restify') const errors = require('restify-errors') const corsMiddleware = require('restify-cors-middleware') let rp = require('request-promise-native') let fs = require('fs') const _ = require('lodash') const validator = require('validator') let rocksDB = require('./models/RocksDB.js') const logger = require('./logger.js') const { version } = require('../package.json') const apiHashes = require('./endpoints/hashes.js') const apiCalendar = require('./endpoints/calendar.js') const apiProofs = require('./endpoints/proofs.js') const apiVerify = require('./endpoints/verify.js') const apiConfig = require('./endpoints/config.js') // RESTIFY SETUP // 'version' : all routes will default to this version const httpOptions = { name: 'chainpoint-node', version: '2.0.0', formatters: { 'application/json': restify.formatters['application/json; q=0.4'], 'application/javascript': restify.formatters['application/json; q=0.4'], 'text/html': restify.formatters['application/json; q=0.4'], 'application/octet-stream': restify.formatters['application/json; q=0.4'] } } const TOR_IPS_KEY = 'blacklist:tor:ips' const CHAINPOINT_NODE_HTTP_PORT = process.env.CHAINPOINT_NODE_PORT || 8080 // state indicating if the Node is ready to accept new hashes for processing let acceptingHashes = true let registrationPassed = true // the list of IP to refuse connections from let IPBlacklist = [] // middleware to ensure the Node is accepting hashes function ensureAcceptingHashes(req, res, next) { if (!acceptingHashes || !registrationPassed) { return next(new errors.ServiceUnavailableError('Service is not currently accepting hashes')) } return next() } async function refreshIPBlacklistAsync() { let torExitIPs = await getTorExitIPAsync() let localIPBlacklist = await getLocalIPBlacklistAsync() let mergedIPBlacklist = torExitIPs.concat(localIPBlacklist) return _.uniq(mergedIPBlacklist) } async function getTorExitIPAsync() { let options = { headers: { 'User-Agent': `chainpoint-node/${version}` }, method: 'GET', uri: 'https://check.torproject.org/exit-addresses', gzip: true, timeout: 10000 } // Retrieve latest exit IP list let extractedTorExitIPs = null try { let response = await rp(options) extractedTorExitIPs = parseTorExitIPs(response) } catch (error) { logger.error('Firewall : Unable to refresh Tor exit IP list from check.torproject.org') } // Save IPs to cache and return if retrieval succeeded if (extractedTorExitIPs !== null) { let compactTorExitIPs = _.compact(extractedTorExitIPs) let uniqueTorExitIPs = _.uniq(compactTorExitIPs) try { await rocksDB.setAsync(TOR_IPS_KEY, uniqueTorExitIPs) } catch (error) { logger.error('Firewall : Unable to save Tor exit IP list to cache') } return uniqueTorExitIPs } else { // otherwise, read existing from cache and return try { let cachedTorExitIPs = await rocksDB.getAsync(TOR_IPS_KEY) return cachedTorExitIPs.split(',') } catch (error) { logger.error('Firewall : Unable to load Tor exit IP list from cache') return [] } } } function parseTorExitIPs(response) { let exitIPs = [] if (!_.isString(response)) { return exitIPs } let respArr = response.split('\n') if (!_.isArray(respArr)) { return exitIPs } _.forEach(respArr, value => { if (/^ExitAddress/.test(value)) { // The second segment of the ExitAddress line is the IP let ip = value.split(' ')[1] // Confirm its an IPv4 address if (validator.isIP(ip.toString(), 4)) { exitIPs.push(ip) } } }) return exitIPs } async function getLocalIPBlacklistAsync() { try { if (fs.existsSync('./ip-blacklist.txt')) { let blacklist = fs.readFileSync('./ip-blacklist.txt', 'utf-8') let splitBlacklist = blacklist.split('\n') let compactBlacklist = _.compact(splitBlacklist) let uniqBlacklist = _.uniq(compactBlacklist) let ipList = [] _.forEach(uniqBlacklist, ip => { // any line that doesn't start with '#' comment if (/^[^#]/.test(ip)) { // Confirm its an IPv4 or IPv6 address // IPv6 allowed is to handle the macOS/Docker // situation where the IP is like: ::ffff:172.18.0.1 // See : https://stackoverflow.com/a/33790357/3902629 if (validator.isIP(ip.toString())) { ipList.push(ip) } } }) return ipList } else { return [] } } catch (error) { logger.warn('Firewall : Unable to parse local IP blacklist (ip-blacklist.txt) ') return [] } } function ipFilter(req, res, next) { var reqIPs = [] if (req.headers['x-forwarded-for']) { let fwdIPs = req.headers['x-forwarded-for'].split(',') reqIPs.push(fwdIPs[0]) } reqIPs.push(req.connection.remoteAddress || '') reqIPs = reqIPs .filter(ip => validator.isIP(ip)) .reduce((ips, ip) => { ips.push(ip, ip.replace(/^.*:/, '')) return ips }, []) for (let ip of reqIPs) { if (IPBlacklist.includes(ip)) return next(new errors.ForbiddenError()) } return next() } // Put any routing, response, etc. logic here. function setupCommonRestifyConfigAndRoutes(server) { // limit responses to only requests for acceptable types server.pre( restify.plugins.acceptParser([ 'application/json', 'application/javascript', 'text/html', 'application/octet-stream', 'application/vnd.chainpoint.ld+json', 'application/vnd.chainpoint.json+base64' ]) ) // Clean up sloppy paths like //todo//////1// server.pre(restify.pre.sanitizePath()) // Checks whether the user agent is curl. If it is, it sets the // Connection header to "close" and removes the "Content-Length" header // See : http://restify.com/#server-api server.pre(restify.pre.userAgentConnection()) // CORS // See : https://github.com/TabDigital/restify-cors-middleware // See : https://github.com/restify/node-restify/issues/1151#issuecomment-271402858 // // Test w/ // // curl \ // --verbose \ // --request OPTIONS \ // http://127.0.0.1:9090/hashes \ // --header 'Origin: http://localhost:9292' \ // --header 'Access-Control-Request-Headers: Origin, Accept, Content-Type' \ // --header 'Access-Control-Request-Method: POST' \ // --header 'proofids: da5b6c70-d628-11e7-a676-0102636501e0' // let cors = corsMiddleware({ preflightMaxAge: 600, origins: ['*'], allowHeaders: ['proofids,auth'], exposeHeaders: ['proofids'] }) server.pre(cors.preflight) server.use(cors.actual) server.use(restify.plugins.gzipResponse()) server.use( restify.plugins.queryParser({ mapParams: true }) ) server.use( restify.plugins.bodyParser({ mapParams: true }) ) // DROP all requests from blacklisted IP addresses server.use(ipFilter) const applyMiddleware = (middlewares = []) => { if (process.env.NODE_ENV === 'development' || process.env.NETWORK === 'testnet') { return [] } else { return middlewares } } let throttle = (burst, rate, opts = { ip: true }) => { return restify.plugins.throttle(Object.assign({}, { burst, rate }, opts)) } // API RESOURCES // IMPORTANT : These routes MUST come after the firewall initialization! // submit hash(es) server.post( { path: '/hashes', version: '2.0.0' }, ...applyMiddleware([throttle(50, 25)]), ensureAcceptingHashes, apiHashes.postHashesAsync ) // get a data value from a calendar transaction server.get( { path: '/calendar/:tx_id/data', version: '2.0.0' }, ...applyMiddleware([throttle(15, 5)]), apiCalendar.getDataValueByIDAsync ) // get a single proof with a single hash_id server.get( { path: '/proofs/:proof_id', version: '2.0.0' }, ...applyMiddleware([throttle(15, 5)]), apiProofs.getProofsByIDAsync ) // get multiple proofs with 'proofids' header param server.get({ path: '/proofs', version: '2.0.0' }, ...applyMiddleware([throttle(15, 5)]), apiProofs.getProofsByIDAsync) // verify one or more proofs server.post( { path: '/verify', version: '2.0.0' }, ...applyMiddleware([throttle(15, 5)]), apiVerify.postProofsForVerificationAsync ) // get configuration information for this Node server.get({ path: '/config', version: '2.0.0' }, ...applyMiddleware([throttle(1, 1)]), apiConfig.getConfigInfoAsync) server.get({ path: '/login', version: '2.0.0' }, function(req, res, next) { res.redirect('/', next) }) server.get({ path: '/about', version: '2.0.0' }, function(req, res, next) { res.redirect('/', next) }) } // HTTP Server async function startInsecureRestifyServerAsync() { let restifyServer = restify.createServer(httpOptions) setupCommonRestifyConfigAndRoutes(restifyServer) // Begin listening for requests return new Promise((resolve, reject) => { restifyServer.listen(CHAINPOINT_NODE_HTTP_PORT, err => { if (err) return reject(err) logger.info(`App : Chainpoint Node listening on port ${CHAINPOINT_NODE_HTTP_PORT}`) return resolve(restifyServer) }) }) } function startIPBlacklistRefreshInterval() { return setInterval(async () => { IPBlacklist = await refreshIPBlacklistAsync() }, 24 * 60 * 60 * 1000) // refresh IPBlacklist every 24 hours } async function startAsync(lnd) { try { apiConfig.setLnd(lnd) IPBlacklist = await refreshIPBlacklistAsync() await startInsecureRestifyServerAsync() } catch (error) { logger.error(`Startup : ${error.message}`) } } module.exports = { startAsync: startAsync, startInsecureRestifyServerAsync: startInsecureRestifyServerAsync, startIPBlacklistRefreshInterval: startIPBlacklistRefreshInterval, // additional functions for testing purposes refreshIPBlacklistAsync: refreshIPBlacklistAsync, setAcceptingHashes: isAccepting => { acceptingHashes = isAccepting }, setRocksDB: db => { rocksDB = db }, setRP: RP => { rp = RP }, setFS: FS => { fs = FS } } ================================================ FILE: lib/cached-proofs.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let cores = require('./cores.js') const utils = require('./utils.js') const _ = require('lodash') const logger = require('./logger.js') let env = require('./parse-env.js').env // The object containing the cached Core proof objects let CORE_PROOF_CACHE = {} const PRUNE_EXPIRED_INTERVAL_SECONDS = 10 async function getCachedCoreProofsAsync(coreSubmissions) { // determine max core submission count for these submissions... the largest cores array of all submissions // this will be constant under normal usage, only varying if the master submission count is updated let maxCoreSubmissionCount = coreSubmissions.reduce((result, item) => { if (item.cores.length > result) result = item.cores.length return result }, 0) // create `proofId` to `submitId` lookup object, keyed by `proofId` // and simultaneously create `coreSubmissionsLookup` object keyed by the coreSubmissions `submitId` let [submitIdForHashIdCoreLookup, coreSubmissionsLookup] = coreSubmissions.reduce( (result, item) => { for (let core of item.cores) { result[0][core.proofId] = item.submitId } result[1][item.submitId] = { cores: item.cores, proof: undefined } return [result[0], result[1]] }, [{}, {}] ) // Attempt to read proofs from the cache. // For all proofs that are not found (not cached), keep `proof` value as undefined for (let submitId in coreSubmissionsLookup) { coreSubmissionsLookup[submitId].proof = (function() { if (CORE_PROOF_CACHE[submitId] && _.isNil(CORE_PROOF_CACHE[submitId].coreProof)) return null return CORE_PROOF_CACHE[submitId] ? _.get(CORE_PROOF_CACHE, `${submitId}.coreProof`, undefined) : undefined })() } // loop through the 1st, 2nd, ... cores array object for each submission until all proofs have been returned // under normal operation, with all Cores operating as expected, only one iteration will be performed // if a Core is offline, the second iterations will attempt to request proofs from the second IP/HashIdCore pair // iterations will continue until all values have been retrieved, or all IP/HashIdCore pairs have been attempted // // use `newProofSubmitIds` to keep track of the submitIds that have new proof data returned from Core // this information is used later to determine what new data needs to be cached let newProofSubmitIds = [] for (let index = 0; index < maxCoreSubmissionCount; index++) { // find core submissions that have `undefined` proofs, they will be requested from Core at cores index `index` let undefinedProofSubmissions = Object.keys(coreSubmissionsLookup).reduce((result, submitId) => { let coreInfo = coreSubmissionsLookup[submitId].cores[index] // if the proof is undefined, and Core info exists in this submission for this `index`, add to results if (coreSubmissionsLookup[submitId].proof === undefined && coreInfo) { result.push({ submitId: submitId, ip: coreInfo.ip, proofId: coreInfo.proofId }) } else if ( coreSubmissionsLookup[submitId].proof !== undefined && coreSubmissionsLookup[submitId].proof != null && coreSubmissionsLookup[submitId].proof.hash_received !== undefined && coreInfo ) { let timer = Date.parse(coreSubmissionsLookup[submitId].proof.hash_received) let btcDue = Date.now() - timer > 7200000 // 120 min have passed // if 90 minutes have passed and we have a cal anchor and not a btc anchor // then we add the proof to undefinedProofSubmissions to ensure re-retrieval let containsRelevantAnchors = coreSubmissionsLookup[submitId].anchorsComplete !== undefined && (!coreSubmissionsLookup[submitId].anchorsComplete.includes('btc') || !coreSubmissionsLookup[submitId].anchorsComplete.includes('tbtc')) if (btcDue && containsRelevantAnchors) { logger.info(`time to check for btc proof ${coreInfo.proofId} from ${coreInfo.ip}`) result.push({ submitId: submitId, ip: coreInfo.ip, proofId: coreInfo.proofId }) } } return result }, []) // if none were found, then we have received proof data for all requested submissions, exit loop if (undefinedProofSubmissions.length === 0) break // split `undefinedProofSubmissions` into distinct array by unique Core IP // this is needed so that we may request proofs from Core in batches grouped by Core IP // start by determining all the unique IPs in play in undefinedProofSubmissions let uniqueIPs = undefinedProofSubmissions.reduce((result, item) => { // using unshift to build list in reverse for efficiency because we must iterate in reverse later if (!result.includes(item.ip)) result.unshift(item.ip) return result }, []) // build an array of submissions for each unique Core IP found let submissionsGroupByIPs = [] for (let ip of uniqueIPs) { let result = [] for (let x = undefinedProofSubmissions.length - 1; x >= 0; x--) { if (undefinedProofSubmissions[x].ip === ip) result.push(...undefinedProofSubmissions.splice(x, 1)) } submissionsGroupByIPs.push(result) } // for each resulting submission group array, request the proofs from Core for (let submissionsGroup of submissionsGroupByIPs) { // flatten the submission data for use in the getProofsAsync call let flattenedSubmission = submissionsGroup.reduce( (result, item) => { result.ip = item.ip // need to make the setting only once, but will be the same for every item result.proofIds.push(item.proofId) return result }, { ip: '', proofIds: [] } ) // attempt to retrieve proofs from Core let getProofsFromCoreResults = [] try { getProofsFromCoreResults = await cores.getProofsAsync(flattenedSubmission.ip, flattenedSubmission.proofIds) } catch (err) { logger.error( `getCachedCoreProofsAsync : Core ${flattenedSubmission.ip} : ProofID Count ${ flattenedSubmission.proofIds.length } (${JSON.stringify(flattenedSubmission.proofIds)}) : ${err.message}` ) // Cache as `null` Proof to prevent subsequent retries for 1min CORE_PROOF_CACHE[submissionsGroup.submitId] = { coreProof: null, expiresAt: Date.now() + 1 * 60 * 1000 // 1min } } // assign the returned proof values back to the `coreSubmissions` object, // for each item in the results, using the `submitIdForHashIdCoreLookup` object for (let result of getProofsFromCoreResults) { let submitIdForResult = submitIdForHashIdCoreLookup[result.proof_id] // be sure that the `submitIdForResult` is known in `coreSubmissions` // assuming it is, as it always should be unless error, assign the proof to that key if (coreSubmissionsLookup.hasOwnProperty(submitIdForResult)) { coreSubmissionsLookup[submitIdForResult].proof = result.proof coreSubmissionsLookup[submitIdForResult].anchorsComplete = _.isNil( coreSubmissionsLookup[submitIdForResult].proof ) ? [] : utils.parseAnchorsComplete(coreSubmissionsLookup[submitIdForResult].proof, env.NETWORK) // track this submitId for caching later newProofSubmitIds.push(submitIdForResult) } } } } // cache any new results returned from Core if (newProofSubmitIds.length > 0) { // for all new proofs received from Core, create an proofType lookup object for use in determining // proper cache TTL for the proof let proofTypeLookup = newProofSubmitIds.reduce((result, submitId) => { if (!_.isNil(coreSubmissionsLookup[submitId].proof)) { if (env.NETWORK === 'mainnet') { result[submitId] = coreSubmissionsLookup[submitId].anchorsComplete.includes('btc') ? 'btc' : 'cal' } else { result[submitId] = coreSubmissionsLookup[submitId].anchorsComplete.includes('tbtc') ? 'tbtc' : 'tcal' } } return result }, {}) // Store the non-null AND null proofs from Core in the local cache for subsequent requests // First, create the array of objects to be written to the cache let uncachedCoreProofObjects = newProofSubmitIds.reduce((result, submitId) => { // `null` cached proofs expire after 1 minute // `(t)cal` cached proofs expire after 15 minutes // `(t)btc` cached proofs expire after 25 hours let expMinutes = 1 if (coreSubmissionsLookup[submitId].proof !== null) { expMinutes = ['btc', 'tbtc'].includes(proofTypeLookup[submitId]) ? 25 * 60 : 15 } result.push({ submitId: submitId, coreProof: coreSubmissionsLookup[submitId].proof, expiresAt: Date.now() + expMinutes * 60 * 1000 }) return result }, []) // Next,write proofs to cache for (let coreProofObject of uncachedCoreProofObjects) { CORE_PROOF_CACHE[coreProofObject.submitId] = { coreProof: coreProofObject.coreProof, expiresAt: coreProofObject.expiresAt } } } // format `coreSubmissions` into the proper result object to return from this function let finalProofResults = Object.keys(coreSubmissionsLookup).map(submitId => { if (_.isNil(coreSubmissionsLookup[submitId].proof)) { return { submitId: submitId, proof: null } } // A proof exists and has been found for this submitId // Identify the anchors completed in this proof and append that information return { submitId: submitId, proof: coreSubmissionsLookup[submitId].proof, anchorsComplete: coreSubmissionsLookup[submitId].anchorsComplete } }) // Finally, return the proof items array return finalProofResults } function pruneExpiredItems() { let now = Date.now() for (let key in CORE_PROOF_CACHE) { if (CORE_PROOF_CACHE[key].expiresAt <= now) { delete CORE_PROOF_CACHE[key] } } } function startPruneExpiredItemsInterval() { return setInterval(pruneExpiredItems, PRUNE_EXPIRED_INTERVAL_SECONDS * 1000) } module.exports = { getCachedCoreProofsAsync: getCachedCoreProofsAsync, startPruneExpiredItemsInterval: startPruneExpiredItemsInterval, // additional functions for testing purposes pruneExpiredItems: pruneExpiredItems, getPruneExpiredIntervalSeconds: () => PRUNE_EXPIRED_INTERVAL_SECONDS, getCoreProofCache: () => CORE_PROOF_CACHE, setCoreProofCache: obj => { CORE_PROOF_CACHE = obj }, setCores: c => { cores = c }, setENV: obj => { env = obj } } ================================================ FILE: lib/cores.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables let env = require('./parse-env.js').env let rp = require('request-promise-native') const { version } = require('../package.json') const retry = require('async-retry') const { Lsat } = require('lsat-js') const _ = require('lodash') const logger = require('./logger.js') const utils = require('./utils.js') const chalk = require('chalk') const lightning = require('./lightning') const PRUNE_EXPIRED_INTERVAL_SECONDS = 10 let CONNECTED_CORE_IPS = [] let CONNECTED_CORE_LN_URIS = [] let coreConnectionCount = 1 // In some cases we may want a list of all Core Nodes (whether or not the Chainpoint Node is connected to it or not) let ALL_CORE_IPS = [] // This is the local in-memory cache of calendar transactions // CORE_TX_CACHE is an object keyed by txId, storing the transaction object let CORE_TX_CACHE = {} // Initialize the Lightning grpc object let lnd = new lightning(env.LND_SOCKET, env.NETWORK) async function connectAsync() { // Retrieve the list of Core IPs we can work with let coreIPList = [] if (!_.isEmpty(env.CHAINPOINT_CORE_CONNECT_IP_LIST)) { // Core IPs have been supplied in CHAINPOINT_CORE_CONNECT_IP_LIST, use those IPs only coreIPList = env.CHAINPOINT_CORE_CONNECT_IP_LIST } //coreConnectionCount = coreIPList.length logger.info(`Connecting to Cores...`) // Select and establish connection to Core(s) let connectedCoreIPResult = await getConnectedCoreIPsAsync(coreIPList, coreConnectionCount) // warn users about env mismatch if (connectedCoreIPResult.networkMismatch) logger.warn(`Unable to connect to Cores with a different network setting. This Node is set to '${env.NETWORK}'`) // ensure we have successfully communicated with `coreConnectionCount` Cores // if we have not, the Node cannot continue, log error and exit if (!connectedCoreIPResult.connected) { throw new Error(`Unable to connect to ${coreConnectionCount} Core(s) as required`) } CONNECTED_CORE_IPS = connectedCoreIPResult.ips CONNECTED_CORE_LN_URIS = connectedCoreIPResult.lnUris // if we're not paying cores, then skip lightning // TODO: ability to pay some cores but not others if (env.NO_LSAT_CORE_WHITELIST.length > 0) { return } let done = false while (!done) { try { try { logger.info(`unlocking lightning...`) await lnd.handleUnlock(null, env.HOT_WALLET_PASS) } catch (error) { utils.sleepAsync(2000) } logger.info(`syncing lightning...`) await waitForSync() logger.info(`peering with lightning nodes...`) await createCoreLNDPeerConnectionsAsync(CONNECTED_CORE_LN_URIS) utils.sleepAsync(10000) // takes a few seconds for peers to connect logger.info(`creating lightning channels...`) await createCoreLNDChannelsAsync(CONNECTED_CORE_LN_URIS) done = true logger.info(`opening lightning channels to Core complete`) } catch (error) { console.log(`open peer or channel failed: ${error.message}`) done = false utils.sleepAsync(2000) } } logger.info(`App : Core IPs : ${CONNECTED_CORE_IPS}`) } async function waitForSync() { let isSynced = false while (!isSynced) { try { let info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, env.HOT_WALLET_PASS) if (info.synced_to_chain) { console.log(chalk.green('\n*****************************************')) console.log(chalk.green('Your lightning node is fully synced.')) console.log(chalk.green('*****************************************')) isSynced = true } else { console.log( chalk.magenta( `${new Date().toISOString()}> Syncing in progress... currently at block height ${info.block_height}` ) ) } } catch (error) { if ( !( error.message.includes('failed to connect to all addresses') || error.message.includes('unknown service lnrpc.Lightning') ) ) { console.log(chalk.red(`An error occurred while checking lnd state : ${error.message}`)) } } finally { if (!isSynced) await utils.sleepAsync(5000) } } } async function createCoreLNDPeerConnectionsAsync(lnUris) { let peerPubKeys = [] try { let peerList = await lnd.callMethodAsync('lightning', 'listPeersAsync', null, env.HOT_WALLET_PASS) for (let peer of peerList.peers) { peerPubKeys.push(peer.pub_key) } } catch (error) { throw new Error('Could not retrieve LND peer list') } for (let lndUri of lnUris) { let [pubkey, host] = lndUri.split('@') if (peerPubKeys.includes(pubkey)) continue // already peered to this node, skip try { await lnd.callMethodAsync( 'lightning', 'connectPeerAsync', { addr: { pubkey, host }, perm: true }, env.HOT_WALLET_PASS ) console.log(chalk.yellow(`Peer connection established with ${lndUri}`)) } catch (error) { throw new Error(`Unable to establish a peer connection with ${lndUri} : ${error.message}`) } } } async function getConnectedCoreIPsAsync(coreIPList, coreConnectionCount) { let getStatusOptions = buildRequestOptions(null, 'GET', '/status') let connectedCoreIPs = [] let lnUris = [] let networkMismatch = false coreIPList = _.shuffle(coreIPList) for (let coreIP of coreIPList) { logger.info(`Connecting to core ${coreIP}`) try { let coreResponse = await coreRequestAsync(getStatusOptions, coreIP, 0) let networkMatch = coreResponse.network === env.NETWORK let isCoreSynced = coreResponse.sync_info.catching_up === false if (!networkMatch) networkMismatch = true if (networkMatch && isCoreSynced) connectedCoreIPs.push(coreIP) if (coreResponse.uris.length > 0) lnUris.push(coreResponse.uris[0]) } catch (error) { console.log(`unable to contact core ${coreIP}: ${error.message}`) } // if we've made enough connections, break out of loop and return IPs if (connectedCoreIPs.length >= coreConnectionCount) break } return { connected: connectedCoreIPs.length >= coreConnectionCount, ips: connectedCoreIPs, lnUris: lnUris, networkMismatch } } async function createCoreLNDChannelsAsync(lnUris) { let channelPubKeys = {} try { let channelList = await lnd.callMethodAsync('lightning', 'listChannelsAsync', {}, env.HOT_WALLET_PASS) for (let channel of channelList.channels) { channelPubKeys[channel.remote_pubkey] = channel } } catch (error) { let msg = `Could not retrieve LND channel list: ${error.message}` console.log(chalk.red(msg)) throw new Error(msg) } try { let channelList = await lnd.callMethodAsync('lightning', 'pendingChannelsAsync', {}, env.HOT_WALLET_PASS) for (let pendingChannel of channelList.pending_open_channels) { channelPubKeys[pendingChannel.channel.remote_node_pub] = pendingChannel console.log( chalk.green( `Channel to ${pendingChannel.channel.remote_node_pub} is pending and may require up to 6 confirmations (~60 minutes) to fully open` ) ) } } catch (error) { let msg = `Could not retrieve pending LND channel list: ${error.message}` console.log(chalk.red(msg)) throw new Error(msg) } for (let lndUri of lnUris) { let pubkey = lndUri.split('@')[0] if (channelPubKeys.hasOwnProperty(pubkey)) { let chan = channelPubKeys[pubkey] // close channel if all local funds are used up if ( chan.hasOwnProperty('total_satoshis_sent') && chan.total_satoshis_sent > 0 && chan.local_balance <= env.MAX_SATOSHI_PER_HASH ) { try { let fee = await lnd.callMethodAsync( 'lightning', 'estimateFeeAsync', { target_confirmations: 6 }, env.HOT_WALLET_PASS ) let closeChanReq = { channel_point: { funding_txid_bytes: Buffer.from(chan.transaction_id, 'hex').reverse(), output_index: chan.transaction_vout }, delivery_address: env.HOT_WALLET_ADDRESS, force: false, sat_per_byte: fee.tokens_per_vbyte, target_conf: 6 } let close = await lnd.callMethodAsync('lightning', 'closeChannelAsync', closeChanReq, env.HOT_WALLET_PASS) console.log(`channel close txid: ${close.close_pending.txid.reverse().toString('hex')}`) } catch (error) { console.log(`unable to close channel: ${error.message}`) } } else { continue } } try { let channelTxInfo = await lnd.callMethodAsync( 'lightning', 'openChannelSyncAsync', { node_pubkey_string: pubkey, local_funding_amount: env.CHANNEL_AMOUNT, push_sat: 0 }, env.HOT_WALLET_PASS ) console.log( `Channel created with ${lndUri} with the following transaction Id: ${Buffer.from( channelTxInfo.funding_txid_bytes.data ).toString('hex')}` ) } catch (error) { let msg = `Unable to create a channel with ${lndUri} : ${error.message}` console.log(chalk.red(msg)) throw new Error(msg) } } } async function coreRequestAsync(options, coreIP, retryCount = 3, timeout = 500) { options.headers['X-Node-Version'] = version options.uri = `http://${coreIP}${options.uriPath}` let response if (retryCount <= 0) { try { response = await rp(options) } catch (error) { if (error.statusCode === 402 || error.status === 402) return error.response throw error } } else { await retry( async bail => { try { response = await rp(options) } catch (error) { // If no response was received or there is a status code >= 500 or payment is still pending/held, // then we should retry the call, throw an error if (!error.statusCode || error.statusCode >= 500 || error.statusCode === 402) throw error // errors like 409 Conflict or 400 Bad Request are not retried because the request is bad and will never succeed bail(error) } }, { retries: retryCount, // The maximum amount of times to retry the operation. Default is 3 factor: 1, // The exponential factor to use. Default is 2 minTimeout: timeout, // The number of milliseconds before starting the first retry. Default is 200 randomize: true, onRetry: error => { if (error.statusCode === 402) logger.warn(`Core request : 402: Payment Required. Core ${coreIP} : Request ${options.uri}. Retrying`) else logger.warn( `Core request : ${error.statusCode || 'no response'} : Core ${coreIP} : Request ${ options.uri } - ${JSON.stringify(options, null, 2)} : ${error.message} : retrying` ) } } ) } return response.body } async function submitHashAsync(hash) { let responses = [] for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) { try { // send initial request without LSAT. Expecting an LSAT w/ invoice in response let postHashOptions = buildRequestOptions(null, 'POST', '/hash', { hash }) let submitResponse = await coreRequestAsync(postHashOptions, coreIP, 0) if (!env.NO_LSAT_CORE_WHITELIST.includes(coreIP)) { logger.info(`Aggregator : Response received from Core ${coreIP} : response : ${JSON.stringify(submitResponse)}`) // get invoice for hash submission from LSAT challenge let lsat = parse402Response(submitResponse) let submitHashInvoiceId = lsat.paymentHash let invoiceAmount = lsat.invoiceAmount let decodedPaymentRequest = lsat.invoice logger.info( `Aggregator : Invoice received from Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : Invoice Amount : ${invoiceAmount} Satoshis` ) // ensure that the invoice amount does not exceed max payment amount if (invoiceAmount > env.MAX_SATOSHI_PER_HASH) throw new Error( `Aggregator : Invoice amount exceeds max setting of ${env.MAX_SATOSHI_PER_HASH} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}` ) // pay the invoice // since this is a hodl invoice, the request will stall until it is settled // so we don't want to await the response but rather continue trying submission until complete payInvoiceAsync(decodedPaymentRequest, submitHashInvoiceId) .then(() => { logger.info( `Aggregator : Invoice paid to Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}` ) }) .catch(e => { logger.error(e.message) }) // submit hash with paid invoice id let headers = { Authorization: lsat.toToken() } postHashOptions = buildRequestOptions(headers, 'POST', '/hash', { hash }) // setting retries to 5 since we can't await invoice payment // and don't know exactly when the invoice is paid and held which is when the hash can be submitted submitResponse = await coreRequestAsync(postHashOptions, coreIP, 5, 1000) logger.info( `Aggregator : Hash submitted to Core ${coreIP} : invoiceId : ${submitHashInvoiceId} : ${invoiceAmount}` ) } else { logger.info(`Aggregator : Hash submitted to Core ${coreIP}`) } responses.push({ ip: coreIP, response: submitResponse }) } catch (error) { // Ignore and try next coreIP logger.warn(`submitHashAsync : Unable to submit to Core ${coreIP} : Hash = ${hash} : ${error.message}`) } } return responses } async function payInvoiceAsync(invoice, submitHashInvoiceId) { return new Promise(async (resolve, reject) => { var call = await lnd.callMethodAsync('lightning', 'sendPayment', {}) call.on('data', function(response) { // A response was received from the server. call.end() if (response.payment_error) return reject( new Error(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${response.payment_error}`) ) return resolve(response) }) call.on('error', err => { console.log(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${err.message}`) ;(async () => await lnd.handleUnlock(err))() return reject(new Error(`Error paying invoice : SubmitHashInvoiceId = ${submitHashInvoiceId} : ${err.message}`)) }) call.write({ payment_request: invoice }) }) } async function getProofsAsync(coreIP, proofIds) { let getProofsOptions = buildRequestOptions( { proofids: proofIds.join(',') }, 'GET', '/proofs', null, 20000 ) try { let coreResponse = await coreRequestAsync(getProofsOptions, coreIP, 0) return coreResponse } catch (error) { if (error.statusCode) throw new Error(`Invalid response on GET proof : ${error.statusCode} : ${error.message}`) throw new Error(`Invalid response received on GET proof : ${error.message || error}`) } } async function getLatestCalBlockInfoAsync() { let getStatusOptions = buildRequestOptions(null, 'GET', '/status') let lastError = null for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) { try { let coreResponse = await coreRequestAsync(getStatusOptions, coreIP, 0) // if the Core is catching up, we cannot use its status to retrieve the latest cal block hash if (coreResponse.sync_info.catching_up) throw 'Core not fully synced' return coreResponse.sync_info } catch (error) { // Record most recent error, ignore, and try next coreIP lastError = error } } if (lastError.statusCode) throw new Error(`Invalid response on GET status : ${lastError.statusCode}`) throw new Error('Invalid response received on GET status') } async function getCachedTransactionAsync(txID) { // if the transaction already exists in the cache, return it if (CORE_TX_CACHE[txID]) return CORE_TX_CACHE[txID].transaction // otherwise, get the tranasction from Core let getTxOptions = buildRequestOptions(null, 'GET', `/calendar/${txID}/data`) for (let coreIP of env.CHAINPOINT_CORE_CONNECT_IP_LIST) { try { let transaction = await coreRequestAsync(getTxOptions, coreIP) if (transaction) { // cache the result and return the transaction CORE_TX_CACHE[txID] = { transaction: transaction, expiresAt: Date.now() + 120 * 60 * 1000 // in 2 hours } return transaction } } catch (error) { console.log(chalk.red(error.message)) } } return null } function buildRequestOptions(headerValues, method, uriPath, body, timeout = 3000) { return { headers: headerValues || {}, method: method, uriPath: uriPath, body: body || undefined, json: true, gzip: true, timeout: timeout, resolveWithFullResponse: true, agent: false, forever: true } } function pruneExpiredItems() { let now = Date.now() for (let key in CORE_TX_CACHE) { if (CORE_TX_CACHE[key].expiresAt <= now) { delete CORE_TX_CACHE[key] } } } function startPruneExpiredItemsInterval() { return setInterval(pruneExpiredItems, PRUNE_EXPIRED_INTERVAL_SECONDS * 1000) } function startConnectionMonitoringInterval() { return setInterval(async () => { await connectAsync() }, 3600000) } function parse402Response(response) { if (response.statusCode !== 402) throw new Error('Expected a 402 response') if (!response.headers['www-authenticate']) throw new Error('Missing www-authenticate header. Cannot parse LSAT challenge') try { const lsat = Lsat.fromChallenge(response.headers['www-authenticate']) return lsat } catch (e) { logger.error(`Could not generate LSAT from challenge: ${e.message}`) throw new Error('Problem processing www-authenticate header challenge for LSAT') } } module.exports = { connectAsync: connectAsync, coreRequestAsync: coreRequestAsync, submitHashAsync: submitHashAsync, getProofsAsync: getProofsAsync, getLatestCalBlockInfoAsync: getLatestCalBlockInfoAsync, getCachedTransactionAsync: getCachedTransactionAsync, startPruneExpiredItemsInterval: startPruneExpiredItemsInterval, parse402Response: parse402Response, getAllCoreIPs: () => ALL_CORE_IPS, // additional functions for testing purposes setCoreConnectionCount: c => (coreConnectionCount = c), getCoreConnectedIPs: () => CONNECTED_CORE_IPS, startConnectionMonitoringInterval: startConnectionMonitoringInterval, pruneExpiredItems: pruneExpiredItems, getPruneExpiredIntervalSeconds: () => PRUNE_EXPIRED_INTERVAL_SECONDS, getCoreTxCache: () => CORE_TX_CACHE, setCoreTxCache: obj => { CORE_TX_CACHE = obj }, setENV: obj => { env = obj }, setRP: RP => { rp = RP }, setLN: LN => { lnd = LN }, getLn: () => { return lnd } } ================================================ FILE: lib/endpoints/calendar.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const errors = require('restify-errors') let cores = require('../cores.js') async function getDataValueByIDAsync(req, res, next) { res.contentType = 'application/json' let txId = req.params.tx_id // validate TM txId is well formed let containsValidTxId = /^([a-fA-F0-9]{2}){32}$/.test(txId) if (!containsValidTxId) { return next(new errors.InvalidArgumentError('invalid JSON body, invalid txId present')) } // check Core for the transaction let txInfo = await cores.getCachedTransactionAsync(txId) if (txInfo === null) { return next(new errors.NotFoundError()) } res.contentType = 'text/plain' res.send(txInfo.toLowerCase()) return next() } module.exports = { getDataValueByIDAsync: getDataValueByIDAsync, // additional functions for testing purposes setCores: c => { cores = c } } ================================================ FILE: lib/endpoints/config.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const { version } = require('../../package.json') let env = require('../parse-env.js').env /** * GET /config handler * * Returns a configuration information object */ let lnd async function getConfigInfoAsync(req, res, next) { res.contentType = 'application/json' let info try { info = await lnd.callMethodAsync('lightning', 'getInfoAsync', null, env.HOT_WALLET_PASS) } catch (error) { info = { error: error.message } } let balance try { balance = await lnd.callMethodAsync('lightning', 'walletBalanceAsync', null, env.HOT_WALLET_PASS) } catch (error) { balance = { error: error.message } } res.send({ lndInfo: info, lightning_balance: balance, version: version, time: new Date().toISOString() }) return next() } module.exports = { getConfigInfoAsync: getConfigInfoAsync, setLnd: lndObj => { lnd = lndObj } } ================================================ FILE: lib/endpoints/hashes.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables let env = require('../parse-env.js').env const errors = require('restify-errors') const _ = require('lodash') const utils = require('../utils.js') let rocksDB = require('../models/RocksDB.js') const logger = require('../logger.js') const analytics = require('../analytics.js') const { UlidMonotonic } = require('id128') /** * Generate the values for the 'meta' property in a POST /hashes response. * * Returns an Object with metadata about a POST /hashes request * including a 'timestamp', and hints for estimated time to completion * for various operations. * * @returns {Object} */ function generatePostHashesResponseMetadata() { let metaDataObj = {} let timestamp = new Date() metaDataObj.hash_received = utils.formatDateISO8601NoMs(timestamp) metaDataObj.processing_hints = generateProcessingHints(timestamp) return metaDataObj } /** * Generate the expected proof ready times for each proof stage * * @param {Date} timestampDate - The hash submission timestamp * @returns {Object} An Object with 'cal' and 'btc' properties * */ function generateProcessingHints(timestampDate) { // cal proof aggregation occurs at :30 seconds past each minute // allow and extra 30 seconds for processing let maxLocalAggregationFromTimestamp = utils.addSeconds(timestampDate, env.AGGREGATION_INTERVAL_SECONDS) let maxSeconds = maxLocalAggregationFromTimestamp.getSeconds() let secondsUntil30Past = maxSeconds < 30 ? 30 - maxSeconds : 90 - maxSeconds let calHint = utils.formatDateISO8601NoMs(utils.addSeconds(maxLocalAggregationFromTimestamp, secondsUntil30Past + 30)) let twoHoursFromTimestamp = utils.addMinutes(timestampDate, 120) let btcHint = utils.formatDateISO8601NoMs(twoHoursFromTimestamp) return { cal: calHint, btc: btcHint } } /** * Converts an array of hash strings to a object suitable to * return to HTTP clients. * * @param {string[]} hashes - An array of string hashes to process * @returns {Object} An Object with 'meta' and 'hashes' properties */ function generatePostHashesResponse(ip, hashes) { let lcHashes = utils.lowerCaseHashes(hashes) let hashObjects = lcHashes.map(hash => { let proofId try { proofId = UlidMonotonic.generate().toCanonical() } catch (error) { UlidMonotonic.reset() proofId = UlidMonotonic.generate().toCanonical() } let hashObj = {} hashObj.proof_id = proofId hashObj.hash = hash logger.info(`Created proof_id ${proofId}`) //send event to google UA var hashEvent = { ec: env.GATEWAY_NAME, ea: 'CreateProof', el: proofId, cd1: hash, cd2: utils.formatDateISO8601NoMs(new Date()), cd3: env.PUBLIC_IP, cd4: ip, dp: '/hash' } analytics.setClientID(proofId) analytics.sendEvent(hashEvent) return hashObj }) return { meta: generatePostHashesResponseMetadata(hashObjects), hashes: hashObjects } } /** * POST /hashes handler * * Expects a JSON body with the form: * {"hashes": ["hash1", "hash2", "hashN"]} * * The `hashes` key must reference a JSON Array * of strings representing each hash to anchor. * * Each hash must be: * - in Hexadecimal form [a-fA-F0-9] * - minimum 40 chars long (e.g. 20 byte SHA1) * - maximum 128 chars long (e.g. 64 byte SHA512) * - an even length string */ async function postHashesAsync(req, res, next) { res.contentType = 'application/json' // validate content-type sent was 'application/json' if (req.contentType() !== 'application/json') { return next(new errors.InvalidArgumentError('invalid content type')) } // validate params has parse a 'hashes' key if (!req.params.hasOwnProperty('hashes')) { return next(new errors.InvalidArgumentError('invalid JSON body, missing hashes')) } // validate hashes param is an Array if (!_.isArray(req.params.hashes)) { return next(new errors.InvalidArgumentError('invalid JSON body, hashes is not an Array')) } // validate hashes param Array has at least one hash if (_.size(req.params.hashes) < 1) { return next(new errors.InvalidArgumentError('invalid JSON body, hashes Array is empty')) } // validate hashes param Array is not larger than allowed max length if (_.size(req.params.hashes) > env.POST_HASHES_MAX) { return next( new errors.InvalidArgumentError(`invalid JSON body, hashes Array max size of ${env.POST_HASHES_MAX} exceeded`) ) } // validate hashes are individually well formed let containsValidHashes = _.every(req.params.hashes, hash => { return /^([a-fA-F0-9]{2}){20,64}$/.test(hash) }) if (!containsValidHashes) { return next(new errors.InvalidArgumentError('invalid JSON body, invalid hashes present')) } let ip = utils.getClientIP(req) logger.info(`Incoming hash from ${ip}`) let responseObj = generatePostHashesResponse(ip, req.params.hashes) // store hash data for later aggregation try { await rocksDB.queueIncomingHashObjectsAsync(responseObj.hashes) } catch (error) { return next(new errors.InternalServerError('Could not save hash data')) } res.send(responseObj) return next() } module.exports = { postHashesAsync: postHashesAsync, generatePostHashesResponse: generatePostHashesResponse, // additional functions for testing purposes setRocksDB: db => { rocksDB = db }, setENV: obj => { env = obj } } ================================================ FILE: lib/endpoints/proofs.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let env = require('../parse-env.js').env const errors = require('restify-errors') const utils = require('../utils.js') const uuidValidate = require('uuid-validate') const uuidTime = require('uuid-time') const chpBinary = require('chainpoint-binary') const _ = require('lodash') let cachedProofs = require('../cached-proofs.js') let rocksDB = require('../models/RocksDB.js') const logger = require('../logger.js') const analytics = require('../analytics.js') const { UlidMonotonic } = require('id128') // The custom MIME type for JSON proof array results containing Base64 encoded proof data const BASE64_MIME_TYPE = 'application/vnd.chainpoint.json+base64' // The custom MIME type for JSON proof array results containing Base64 encoded proof data const JSONLD_MIME_TYPE = 'application/vnd.chainpoint.ld+json' /** * Converts proof path array output from the merkle-tools package * to a Chainpoint v3 ops array * * @param {proof object array} proof - The proof array generated by merkle-tools * @param {string} op - The hash type performed throughout merkle tree construction (sha-256, sha-512, sha-256-x2, etc.) * @returns {ops object array} */ function formatAsChainpointV3Ops(proof, op) { let ChainpointV3Ops = proof.reduce((result, item) => { if (item.left) { item = { l: item.left } } else { item = { r: item.right } } result.push(item, { op: op }) return result }, []) return ChainpointV3Ops } /** * GET /proofs/:proof_id handler * * Expects a path parameter 'proof_id' in the form of a Version 1 UUID * * Returns a chainpoint proof for the requested Proof ID */ async function getProofsByIDAsync(req, res, next) { res.contentType = 'application/json' let ip = utils.getClientIP(req) let proofIds = [] // check if proof_id parameter was included if (req.params && req.params.proof_id) { // a proof_id was specified in the url, so use that proof_id only proofIds.push(req.params.proof_id) } else if (req.headers && req.headers.proofids) { // no proof_id was specified in url, read from headers.proofids proofIds = req.headers.proofids.split(',').map(_.trim) } // ensure at least one proof_id was submitted if (proofIds.length === 0) { return next(new errors.InvalidArgumentError('invalid request, at least one proof id required')) } // ensure that the request count does not exceed the maximum setting if (proofIds.length > env.GET_PROOFS_MAX) { return next(new errors.InvalidArgumentError('invalid request, too many proof ids (' + env.GET_PROOFS_MAX + ' max)')) } // ensure all proof_ids are valid, send event for (let proofId of proofIds) { if (!(uuidValidate(proofId, 1) || utils.isULID(proofId))) { return next(new errors.InvalidArgumentError('invalid request, bad proof_id')) } // Proof endpoint (always logged regardless of proof retrieval success) var startProofEvent = { ec: env.GATEWAY_NAME, ea: 'GetProof', el: proofId, cd1: 'Start', cd2: utils.formatDateISO8601NoMs(new Date()), cd3: env.PUBLIC_IP, cd4: ip, dp: '/proof' } analytics.setClientID(proofId) analytics.sendEvent(startProofEvent) } let requestedType = req.accepts(JSONLD_MIME_TYPE) && !req.accepts(BASE64_MIME_TYPE) ? JSONLD_MIME_TYPE : BASE64_MIME_TYPE // retrieve all the nodeProofDataItems for the requested proofIds let nodeProofDataItems = [] try { nodeProofDataItems = await rocksDB.getProofStatesBatchByProofIdsAsync(proofIds) } catch (error) { return next(new errors.InternalError('error retrieving node proof data items')) } // convert all proof state value to chpv3 nodeProofDataItems = nodeProofDataItems.map(nodeProofDataItem => { if (nodeProofDataItem.proofState === null) return nodeProofDataItem nodeProofDataItem.proofState = formatAsChainpointV3Ops(nodeProofDataItem.proofState, 'sha-256') return nodeProofDataItem }) // get an array of all unique core submission objects from the proof data items by submitId let knownSubmitIds = [] let uniqueCoreSubmissions = nodeProofDataItems.reduce((result, val) => { // if the node data item was not found for that proofId, skip this item if (_.isNil(val.submission)) return result // add this submission object to the results if it is unique let submitId = val.submission.submitId if (!knownSubmitIds.includes(submitId)) { knownSubmitIds.push(submitId) result.push(val.submission) } return result }, []) // get proofs for each unique submitId let coreProofDataItems try { coreProofDataItems = await cachedProofs.getCachedCoreProofsAsync(uniqueCoreSubmissions) } catch (error) { logger.error(`Could not get proofs from Core : ${error.message}`) return next(new errors.InternalError('error retrieving proofs from Core')) } // assemble all Core proofs received into an object keyed by submitId coreProofDataItems = coreProofDataItems.reduce((results, coreProofDataItem) => { results[coreProofDataItem.submitId] = coreProofDataItem return results }, {}) // build the resulting proofs from the collected data for each proof_id let results = [] for (let nodeProofDataItem of nodeProofDataItems) { let submitId = nodeProofDataItem.submission ? nodeProofDataItem.submission.submitId : null let coreProof = coreProofDataItems[submitId] ? coreProofDataItems[submitId].proof : null let fullProof = null if (coreProof) { fullProof = buildFullProof(coreProof, nodeProofDataItem) } let proofResult = fullProof if (requestedType === BASE64_MIME_TYPE && fullProof) { try { proofResult = chpBinary.objectToBase64Sync(fullProof) } catch (error) { if (nodeProofDataItem) { logger.error(`Could not convert binary proof for proof id : ${nodeProofDataItem.proofId}`) logger.error(`Binary Conversion Error : ${error.message}`) if (fullProof) { logger.error(JSON.stringify(fullProof)) } } return next(error) } } let result = { proof_id: nodeProofDataItem.proofId, proof: proofResult, anchors_complete: coreProofDataItems[submitId] ? coreProofDataItems[submitId].anchorsComplete : [] } results.push(result) //send events var event = { ec: env.GATEWAY_NAME, el: result.proof_id, cd1: result.proof ? result.proof.hash : 'null', cd2: utils.formatDateISO8601NoMs(new Date()), cd3: env.PUBLIC_IP, cd4: ip, dp: '/proof' } analytics.setClientID(result.proof_id) if (fullProof) { try { let proofString = JSON.stringify(fullProof) if (proofString.includes('btc') || proofString.includes('tbtc')) { // eslint-disable-line event.ea = 'GetProofSuccessBtc' analytics.sendEvent(event) } else if (proofString.includes('cal') || proofString.includes('tcal')) { // eslint-disable-line event.ea = 'GetProofSuccessCal' analytics.sendEvent(event) } } catch (error) { logger.error(`could not get proof size of proof ${result.proof_id}`) } } else { event.ea = 'GetProofFail' analytics.sendEvent(event) } } res.send(results) return next() } function buildFullProof(coreProof, nodeProofDataItem) { if (!coreProof || !nodeProofDataItem) return null let fullProof = _.cloneDeep(coreProof) let coreBranches = _.cloneDeep(fullProof.branches) fullProof.proof_id = nodeProofDataItem.proofId fullProof.hash = nodeProofDataItem.hash if (utils.isULID(nodeProofDataItem.proofId)) { fullProof.hash_received = utils.formatDateISO8601NoMs(UlidMonotonic.fromCanonical(nodeProofDataItem.proofId).time) } else { fullProof.hash_received = utils.formatDateISO8601NoMs(new Date(parseInt(uuidTime.v1(nodeProofDataItem.proofId)))) } fullProof.branches[0].label = 'aggregator' fullProof.branches[0].ops = nodeProofDataItem.proofState fullProof.branches[0].branches = coreBranches fullProof = utils.jsonTransform( fullProof, // eslint-disable-next-line no-unused-vars (key, value) => key.includes('uris'), (key, value) => { value = value.replace('https://', 'http://') if (!value.includes('http://')) { value = 'http://' + value } return value } ) return fullProof } module.exports = { getProofsByIDAsync: getProofsByIDAsync, // additional functions for testing purposes setRocksDB: db => { rocksDB = db }, setCachedProofs: cp => { cachedProofs = cp }, setENV: obj => { env = obj } } ================================================ FILE: lib/endpoints/verify.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let env = require('../parse-env.js').env const _ = require('lodash') const chpParse = require('chainpoint-parse') const errors = require('restify-errors') let cores = require('../cores.js') const parallel = require('async-await-parallel') const logger = require('../logger.js') async function ProcessVerifyTasksAsync(verifyTasks) { let processedTasks = [] for (let x = 0; x < verifyTasks.length; x++) { let verifyTask = verifyTasks[x] // check for malformatted proofs let status = verifyTask.status if (status === 'malformed') { processedTasks.push({ proof_index: verifyTask.proof_index, status: status }) continue } // check for anchors on unsupported network let supportedAnchorTypes = [] // if this Node is running in mainnet mode, only accept mainnet anchors if (env.NETWORK === 'mainnet') supportedAnchorTypes = ['cal', 'btc'] // if this Node is running in testnet mode, only accept testnet anchors if (env.NETWORK === 'testnet') supportedAnchorTypes = ['tcal', 'tbtc'] // if there is a network mismatch, do not attempt to verify proof let mismatchFound = false for (let x = 0; x < verifyTask.anchors.length; x++) { if (!supportedAnchorTypes.includes(verifyTask.anchors[x].anchor.type)) { processedTasks.push({ proof_index: verifyTask.proof_index, status: `This is a '${env.NETWORK}' Node supporting '${supportedAnchorTypes.join( "' and '" )}' anchor types. Cannot verify '${verifyTask.anchors[x].anchor.type}' anchors.` }) mismatchFound = true break } } if (mismatchFound) continue let totalCount = 0 let validCount = 0 let anchorResults = [] let confirmTasks = [] for (let x = 0; x < verifyTask.anchors.length; x++) { confirmTasks.push(async () => { return confirmExpectedValueAsync(verifyTask.anchors[x].anchor) }) } let confirmResults = [] if (confirmTasks.length > 0) { try { confirmResults = await parallel(confirmTasks, 20) } catch (error) { logger.error(`Could not confirm proof data ${error.message}`) throw new Error('error confirming proof data') } } for (let x = 0; x < verifyTask.anchors.length; x++) { try { let anchor = verifyTask.anchors[x] let confirmResult = confirmResults[x] let anchorResult = { branch: anchor.branch || null, type: anchor.anchor.type, valid: confirmResult } totalCount++ validCount = validCount + (anchorResult.valid === true ? 1 : 0) anchorResults.push(anchorResult) } catch (error) { logger.error('Verification error') } } if (validCount === 0) { status = 'invalid' } else if (validCount === totalCount) { status = 'verified' } else { status = 'mixed' } let result = { proof_index: verifyTask.proof_index, hash: verifyTask.hash, proof_id: verifyTask.proof_id, hash_received: verifyTask.hash_received, anchors: anchorResults, status: status } processedTasks.push(result) } return processedTasks } function BuildVerifyTaskList(proofs) { let results = [] let proofIndex = 0 // extract id, time, anchors, and calculate expected values _.forEach(proofs, proof => { try { let parseObj = chpParse.parse(proof) results.push(buildResultObject(parseObj, proofIndex++)) } catch (error) { // continue regardless of error results.push(buildResultObject(null, proofIndex++)) } }) return results } function buildResultObject(parseObj, proofIndex) { let hash = parseObj !== null ? parseObj.hash : undefined let proofId = parseObj !== null ? parseObj.proof_id : undefined let hashReceived = parseObj !== null ? parseObj.hash_received : undefined let expectedValues = parseObj !== null ? flattenExpectedValues(parseObj.branches) : undefined return { proof_index: proofIndex, hash: hash, proof_id: proofId, hash_received: hashReceived, anchors: expectedValues, status: parseObj === null ? 'malformed' : '' } } async function confirmExpectedValueAsync(anchorInfo) { let anchorUri = anchorInfo.uris[0] let anchorTxId = _.takeRight(anchorUri.split('/'), 2)[0] let expectedValue = anchorInfo.expected_value // check Core for the transaction let txInfo = await cores.getCachedTransactionAsync(anchorTxId) if (txInfo === null) { throw new Error('Unable to retrieve transaction from Core to the confirm the anchor value') } return txInfo === expectedValue } function flattenExpectedValues(branchArray) { let results = [] for (let b = 0; b < branchArray.length; b++) { let anchors = branchArray[b].anchors if (anchors.length > 0) { for (let a = 0; a < anchors.length; a++) { results.push({ branch: branchArray[b].label || undefined, anchor: anchors[a] }) } } if (branchArray[b].branches) { results = results.concat(flattenExpectedValues(branchArray[b].branches)) } return results } } /** * POST /verify handler * * Expects a JSON body with the form: * {"proofs": [ {proofJSON1}, {proofJSON2}, {proofJSON3} ]} * or * {"proofs": [ "proof binary 1", "proof binary 2", "proof binary 3" ]} * * The `proofs` key must reference a JSON Array of chainpoint proofs. * Proofs may be in either JSON form or base64 encoded binary form. * */ async function postProofsForVerificationAsync(req, res, next) { res.contentType = 'application/json' // validate content-type sent was 'application/json' if (req.contentType() !== 'application/json') { return next(new errors.InvalidArgumentError('Invalid content type')) } // validate params has parse a 'proofs' key if (!req.params.hasOwnProperty('proofs')) { return next(new errors.InvalidArgumentError('Invalid JSON body, missing proofs')) } // validate proofs param is an Array if (!_.isArray(req.params.proofs)) { return next(new errors.InvalidArgumentError('Invalid JSON body, proofs is not an Array')) } // validate proofs param Array has at least one hash if (_.size(req.params.proofs) < 1) { return next(new errors.InvalidArgumentError('Invalid JSON body, proofs Array is empty')) } // validate proofs param Array is not larger than allowed max length if (_.size(req.params.proofs) > env.POST_VERIFY_PROOFS_MAX) { return next( new errors.InvalidArgumentError( `Invalid JSON body, proofs Array max size of ${env.POST_VERIFY_PROOFS_MAX} exceeded` ) ) } let verifyTasks try { verifyTasks = BuildVerifyTaskList(req.params.proofs) } catch (error) { logger.error(`Could not build verify list: ${error.message}`) return next(new errors.InternalError('Node internal error verifying proof(s)')) } let verifyResults try { verifyResults = await ProcessVerifyTasksAsync(verifyTasks) } catch (error) { return next(new errors.InternalError('Node internal error verifying proof(s)')) } res.send(verifyResults) return next() } module.exports = { postProofsForVerificationAsync: postProofsForVerificationAsync, // additional functions for testing purposes setCores: c => { cores = c }, setENV: obj => { env = obj } } ================================================ FILE: lib/lightning.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables let env = require('./parse-env.js').env const lnRPCNodeClient = require('lnrpc-node-client') const retry = require('async-retry') const dotenv = require('dotenv') // track if unlock is in process to prevent multiple simultaneous unlock errors let IS_UNLOCKING = false let LND_DIR let LND_SOCKET let LND_TLS_CERT let LND_MACAROON dotenv.config() let lnd = function(socket, network, unlockOnly = false, inHostContext = false) { if (!['mainnet', 'testnet'].includes(network)) throw new Error('Invalid network value') LND_DIR = process.env.LND_DIR || `${process.env.HOME}/${inHostContext ? '.chainpoint/gateway/' : ''}.lnd` LND_SOCKET = socket LND_TLS_CERT = process.env.LND_TLS_CERT || `${LND_DIR}/tls.cert` LND_MACAROON = process.env.LND_MACAROON || `${LND_DIR}/data/chain/bitcoin/${network}/admin.macaroon` if (unlockOnly) { lnRPCNodeClient.setTls(LND_SOCKET, LND_TLS_CERT) } else { lnRPCNodeClient.setCredentials(LND_SOCKET, LND_MACAROON, LND_TLS_CERT) } // Call the service method with the given parameter // If a locked wallet is detected, wallet unlock is attempted and call is retried this.callMethodAsync = async (service, method, params, hotWalletPass = null) => { let lndService = lnRPCNodeClient[service]() return await retry(async () => await lndService[method](params), { retries: 30, factor: 1, minTimeout: 1000, onRetry: async error => { if (!error.message.includes('failed to connect to all addresses')) { if (!error.message.includes('unknown service lnrpc.Lightning')) { console.log(error) } await this.handleUnlock(error, hotWalletPass) } } }) } this.handleUnlock = async (err, hotWalletPass) => { if (err == null || err.code === 2 || err.code === 12) { // error code 12 indicates wallet may be locked if (!IS_UNLOCKING) { IS_UNLOCKING = true try { await lnRPCNodeClient .unlocker() .unlockWalletAsync({ wallet_password: hotWalletPass || env.HOT_WALLET_PASS, recovery_window: 10000 }) } catch (error) { throw new Error(`Unable to unlock LND wallet : ${error.message}`) } finally { IS_UNLOCKING = false } } } } // Call the service method with the given parameter // No unlocking or retrying is performed with this method this.callMethodRawAsync = async (service, method, params) => await lnRPCNodeClient[service]()[method](params) } module.exports = lnd ================================================ FILE: lib/logger.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables let env = require('./parse-env.js').env const winston = require('winston') const myFormat = winston.format.printf(({ level, message, timestamp }) => `${timestamp} [${level}] ${message}`) let consoleOpts = { level: 'info', stderrLevels: ['error'], format: winston.format.combine(winston.format.colorize({ all: true }), winston.format.timestamp(), myFormat) } if (env.NODE_ENV === 'test') consoleOpts.silent = true const logger = winston.createLogger({ transports: [new winston.transports.Console(consoleOpts)] }) module.exports = logger ================================================ FILE: lib/models/RocksDB.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let env = require('../parse-env.js').env const level = require('level-rocksdb') const crypto = require('crypto') const path = require('path') const JSBinaryType = require('js-binary').Type const logger = require('../logger.js') const utils = require('../utils.js') const { Ulid } = require('id128') // See Options: https://github.com/level/leveldown#options // Setup with options, all default except: // cacheSize : which was increased from 8MB to 32MB let options = { createIfMissing: true, errorIfExists: false, compression: true, cacheSize: 32 * 1024 * 1024, writeBufferSize: 4 * 1024 * 1024, blockSize: 4096, maxOpenFiles: 1000, blockRestartInterval: 16, maxFileSize: 2 * 1024 * 1024, keyEncoding: 'binary', valueEncoding: 'binary' } const prefixBuffers = { PROOF_STATE_INDEX: Buffer.from('b1a1', 'hex'), PROOF_STATE_VALUE: Buffer.from('b1a2', 'hex'), INCOMING_HASH_OBJECTS: Buffer.from('b1b1', 'hex'), REP_ITEM_VALUE: Buffer.from('b1c1', 'hex'), REP_ITEM_ID_INDEX: Buffer.from('b1c2', 'hex'), REP_ITEM_PROOF_VALUE: Buffer.from('b1c3', 'hex') } const PRUNE_BATCH_SIZE = 1000 const PRUNE_INTERVAL_SECONDS = 10 let PRUNE_IN_PROGRESS = false let db = null async function openConnectionAsync(dir = `${process.env.HOME}/.chainpoint/gateway/data/rocksdb`) { return new Promise(resolve => { level(path.resolve(dir), options, (err, conn) => { if (err) { logger.error(`Unable to open database : ${err.message}`) process.exit(0) } else { db = conn resolve(db) } }) }) } /**************************************************************************************************** * DEFINE SCHEMAS ****************************************************************************************************/ // #region SCHEMAS const nodeProofDataItemSchema = new JSBinaryType({ proofId: 'Buffer', hash: 'Buffer', proofState: ['Buffer'], submission: { submitId: 'Buffer', cores: [{ ip: 'string', proofId: 'Buffer' }] } }) const incomingHashObjectsSchema = new JSBinaryType([ { proof_id: 'Buffer', hash: 'Buffer' } ]) // #endregion SCHEMAS /**************************************************************************************************** * PROOF STATE FUNCTIONS ****************************************************************************************************/ // #region PROOF STATE FUNCTIONS function encodeBinaryProofStateId(proofIdNode) { let idBuffer let idType if (utils.isUUID(proofIdNode)) { idBuffer = Buffer.from(proofIdNode.replace(/-/g, ''), 'hex') idType = 'uuid' } else if (utils.isULID(proofIdNode)) { idBuffer = Buffer.from(Ulid.fromCanonicalTrusted(proofIdNode).bytes) idType = 'ulid' } return { idType: idType, idBuffer: idBuffer } } function decodeBinaryProofStateId(idType, proofId) { return idType == 'uuid' ? utils.hexToUUIDv1(proofId.toString('hex')) : Ulid.construct(Uint8Array.from(proofId)).toCanonical() } function encodeBinaryProofStateValueKey(proofIdNode) { let idBuffer = encodeBinaryProofStateId(proofIdNode).idBuffer return Buffer.concat([prefixBuffers.PROOF_STATE_VALUE, idBuffer]) } function createBinaryProofStateTimeIndexKey() { // generate a new key for the current time let timestampBuffer = Buffer.alloc(8) timestampBuffer.writeDoubleBE(Date.now()) let rndBuffer = crypto.randomBytes(16) return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, timestampBuffer, rndBuffer]) } function createBinaryProofStateTimeIndexMin() { // generate the minimum key value for range query let minBoundsBuffer = Buffer.alloc(24, 0) return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, minBoundsBuffer]) } function createBinaryProofStateTimeIndexMax(timestamp) { // generate the maximum key value for range query up to given timestamp let timestampBuffer = Buffer.alloc(8, 0) timestampBuffer.writeDoubleBE(timestamp) let rndBuffer = Buffer.alloc(16, 'ff', 'hex') return Buffer.concat([prefixBuffers.PROOF_STATE_INDEX, timestampBuffer, rndBuffer]) } function encodeProofStateValue(nodeProofDataItem) { let stateObj = { proofId: encodeBinaryProofStateId(nodeProofDataItem.proofId).idBuffer, hash: Buffer.from(nodeProofDataItem.hash, 'hex'), proofState: nodeProofDataItem.proofState, submission: { submitId: encodeBinaryProofStateId(nodeProofDataItem.submission.submitId).idBuffer, cores: nodeProofDataItem.submission.cores.map(core => { return { ip: core.ip, proofId: encodeBinaryProofStateId(core.proofId).idBuffer } }) } } return nodeProofDataItemSchema.encode(stateObj) } function decodeProofStateValue(proofStateValue, idType) { let leftCode = Buffer.from('\x00') let rightCode = Buffer.from('\x01') let stateObj = nodeProofDataItemSchema.decode(proofStateValue) let nodeProofDataItem = { proofId: decodeBinaryProofStateId(idType, stateObj.proofId), hash: stateObj.hash.toString('hex'), proofState: stateObj.proofState.reduce((result, op, index, proofState) => { if (op.equals(leftCode)) result.push({ left: proofState[index + 1].toString('hex') }) if (op.equals(rightCode)) result.push({ right: proofState[index + 1].toString('hex') }) return result }, []), submission: { submitId: decodeBinaryProofStateId(idType, stateObj.submission.submitId), cores: stateObj.submission.cores.map(core => { return { ip: core.ip, proofId: decodeBinaryProofStateId(idType, core.proofId) } }) } } return nodeProofDataItem } async function getProofStatesBatchByProofIdsAsync(proofIds) { let results = [] for (let proofId of proofIds) { try { let proofIdType = encodeBinaryProofStateId(proofId).idType let proofStateValueKey = encodeBinaryProofStateValueKey(proofId) let proofStateValue = await db.get(proofStateValueKey) let nodeProofDataItem = decodeProofStateValue(proofStateValue, proofIdType) results.push(nodeProofDataItem) } catch (error) { if (error.notFound) { results.push({ proofId: proofId, hash: null, proofState: null, submission: null }) } else { let err = `Unable to read proof state for hash with proofId = ${proofId} : ${error.message}` throw err } } } return results } async function saveProofStatesBatchAsync(nodeProofDataItems) { let ops = [] for (let nodeProofDataItem of nodeProofDataItems) { let proofStateValueKey = encodeBinaryProofStateValueKey(nodeProofDataItem.proofId) let proofStateTimeIndexKey = createBinaryProofStateTimeIndexKey() let proofStateValue = encodeProofStateValue(nodeProofDataItem) ops.push({ type: 'put', key: proofStateValueKey, value: proofStateValue }) ops.push({ type: 'put', key: proofStateTimeIndexKey, value: proofStateValueKey }) } try { await db.batch(ops) } catch (error) { let err = `Unable to write proof state : ${error.message}` throw err } } async function pruneProofStateDataSince(timestampMS) { return new Promise((resolve, reject) => { let delOps = [] let minKey = createBinaryProofStateTimeIndexMin() let maxKey = createBinaryProofStateTimeIndexMax(timestampMS) db.createReadStream({ gte: minKey, lte: maxKey }) .on('data', async data => { delOps.push({ type: 'del', key: data.key }) delOps.push({ type: 'del', key: data.value }) // Execute in batches of PRUNE_BATCH_SIZE if (delOps.length >= PRUNE_BATCH_SIZE) { try { let delOpsBatch = delOps.splice(0) await db.batch(delOpsBatch) } catch (error) { let err = `Error during proof state batch delete : ${error.message}` return reject(err) } } }) .on('error', error => { let err = `Error reading proof state keys for pruning : ${error.message}` return reject(err) }) .on('end', async () => { try { await db.batch(delOps) } catch (error) { return reject(error.message) } return resolve() }) }) } async function pruneOldProofStateDataAsync() { let pruneTime = Date.now() - env.PROOF_EXPIRE_MINUTES * 60 * 1000 try { await pruneProofStateDataSince(pruneTime) } catch (error) { logger.warn(`An error occurred during proof state pruning : ${error.message}`) } } // #endregion PROOF STATE FUNCTIONS /**************************************************************************************************** * INCOMING HASH QUEUE FUNCTIONS ****************************************************************************************************/ // #region INCOMING HASH QUEUE FUNCTIONS function createBinaryIncomingHashObjectsTimeIndexKey() { // generate a new key for the current time let timestampBuffer = Buffer.alloc(8) timestampBuffer.writeDoubleBE(Date.now()) let rndBuffer = crypto.randomBytes(16) return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, timestampBuffer, rndBuffer]) } function createBinaryIncomingHashObjectsTimeIndexMin() { // generate the minimum key value for range query let minBoundsBuffer = Buffer.alloc(24, 0) return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, minBoundsBuffer]) } function createBinaryIncomingHashObjectsTimeIndexMax(timestamp) { // generate the maximum key value for range query up to given timestamp let timestampBuffer = Buffer.alloc(8, 0) timestampBuffer.writeDoubleBE(timestamp) let rndBuffer = Buffer.alloc(16, 'ff', 'hex') return Buffer.concat([prefixBuffers.INCOMING_HASH_OBJECTS, timestampBuffer, rndBuffer]) } function encodeIncomingHashObjectsValue(hashObjects) { hashObjects = hashObjects.map(hashObject => { return { proof_id: encodeBinaryProofStateId(hashObject.proof_id).idBuffer, hash: Buffer.from(hashObject.hash, 'hex') } }) return incomingHashObjectsSchema.encode(hashObjects) } function decodeIncomingHashObjectsValue(hashObjectsBinary) { if (hashObjectsBinary.length === 0) return [] let hashObjects = incomingHashObjectsSchema.decode(hashObjectsBinary) hashObjects = hashObjects.map(hashObject => { return { proof_id: Ulid.construct(Uint8Array.from(hashObject.proof_id)).toCanonical(), hash: hashObject.hash.toString('hex') } }) return hashObjects } async function queueIncomingHashObjectsAsync(hashObjects) { try { let incomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexKey() let incomingHashObjectsValue = encodeIncomingHashObjectsValue(hashObjects) await db.put(incomingHashObjectsBinaryKey, incomingHashObjectsValue) } catch (error) { let err = `Unable to write incoming hash data : ${error.message}` throw err } } async function getIncomingHashesUpToAsync(maxTimestamp) { return new Promise((resolve, reject) => { let hashesObjects = [] let delOps = [] let minIncomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexMin() let maxIncomingHashObjectsBinaryKey = createBinaryIncomingHashObjectsTimeIndexMax(maxTimestamp) db.createReadStream({ gte: minIncomingHashObjectsBinaryKey, lte: maxIncomingHashObjectsBinaryKey }) .on('data', async data => { hashesObjects.push(...decodeIncomingHashObjectsValue(data.value)) delOps.push({ type: 'del', key: data.key }) }) .on('error', error => { let err = `Error reading incoming hashes : ${error.message}` return reject(err) }) .on('end', async () => { return resolve([hashesObjects, delOps]) }) }) } // #endregion INCOMING HASH QUEUE FUNCTIONS /**************************************************************************************************** * GENERAL KEY - VALUE FUNCTIONS ****************************************************************************************************/ // #region GENERAL KEY - VALUE FUNCTIONS async function setAsync(key, value) { try { await db.put(Buffer.from(`custom_key:${key}`), Buffer.from(value.toString())) } catch (error) { let err = `Unable to write key : ${error.message}` throw new Error(err) } } async function getAsync(key) { try { let result = await db.get(Buffer.from(`custom_key:${key}`)) return result.toString() } catch (error) { if (error.notFound) { return null } else { let err = `Unable to read key : ${error.message}` throw new Error(err) } } } async function deleteBatchAsync(delOps) { return db.batch(delOps) } // #endregion GENERAL KEY - VALUE FUNCTIONS /**************************************************************************************************** * SUPPORT FUNCTIONS ****************************************************************************************************/ // #region SUPPORT FUNCTIONS // #endregion SUPPORT FUNCTIONS /**************************************************************************************************** * SET AUTOMATIC PRUNING INTERVALS ****************************************************************************************************/ // #region SET AUTOMATIC PRUNING INTERVALS function startPruningInterval() { return setInterval(async () => { if (!PRUNE_IN_PROGRESS) { PRUNE_IN_PROGRESS = true await pruneOldProofStateDataAsync() PRUNE_IN_PROGRESS = false } }, PRUNE_INTERVAL_SECONDS * 1000) } // #endregion SET AUTOMATIC PRUNING INTERVALS module.exports = { openConnectionAsync: openConnectionAsync, getProofStatesBatchByProofIdsAsync: getProofStatesBatchByProofIdsAsync, saveProofStatesBatchAsync: saveProofStatesBatchAsync, queueIncomingHashObjectsAsync: queueIncomingHashObjectsAsync, getIncomingHashesUpToAsync: getIncomingHashesUpToAsync, setAsync: setAsync, getAsync: getAsync, deleteBatchAsync: deleteBatchAsync, startPruningInterval: startPruningInterval, pruneOldProofStateDataAsync: pruneOldProofStateDataAsync, // additional functions for testing purposes setENV: obj => { env = obj } } ================================================ FILE: lib/parse-env.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const envalid = require('envalid') const ip = require('ip') function valCoreIPList(list) { // If IP list supplied, ensure it is valid, or continue with empty string if (list === '') return '' let IPs = list.split(',') for (let val of IPs) { if ((!ip.isV4Format(val) && !ip.isV6Format(val)) || val === '') throw new Error('The Core IP list contains an invalid entry') } // ensure each IP is unique let ipSet = new Set(IPs) if (ipSet.size !== IPs.length) throw new Error('The Core IP list cannot contain duplicates') return IPs } function valNetwork(name) { if (name === '' || name === 'mainnet') return 'mainnet' if (name === 'testnet') return 'testnet' throw new Error('The NETWORK value is invalid') } const validateCoreIPList = envalid.makeValidator(valCoreIPList) const validateNetwork = envalid.makeValidator(valNetwork) let envDefinitions = { // Chainpoint Node environment related variables NODE_ENV: envalid.str({ default: 'production', desc: 'The type of environment in which the service is running' }), NETWORK: validateNetwork({ default: 'mainnet', desc: `The network to use, 'mainnet' or 'testnet'` }), LND_SOCKET: envalid.str({ default: 'lnd:10009', desc: 'Lightning GRPC host and port' }), PUBLIC_IP: envalid.str({ default: '127.0.0.1', desc: 'IP host and port' }), GATEWAY_NAME: envalid.str({ default: 'UNNAMED', desc: 'A, B, or C' }), GOOGLE_UA_ID: envalid.str({ default: '', desc: 'Google Universal Analytics ID' }), // Chainpoint Core CHAINPOINT_CORE_CONNECT_IP_LIST: validateCoreIPList({ default: '', desc: 'A comma separated list of specific Core IPs to connect to (instead of using Core discovery)' }), AGGREGATION_INTERVAL_SECONDS: envalid.num({ default: 60, desc: `The aggregation and Core submission frequency, in seconds` }), MAX_SATOSHI_PER_HASH: envalid.num({ default: 10, desc: `The maximum amount you are willing to spend for each hash submission to Core, in Satoshi` }), PROOF_EXPIRE_MINUTES: envalid.num({ default: 1440, desc: `The length of time proofs as stored on the node for retrieval, in minutes` }), POST_HASHES_MAX: envalid.num({ default: 1000, desc: `The maximum number of hashes accepted in a single submit request` }), POST_VERIFY_PROOFS_MAX: envalid.num({ default: 1000, desc: `The maximum number of proofs accepted in a single verification request` }), GET_PROOFS_MAX: envalid.num({ default: 250, desc: `The maximum number of proofs to be returned in a single request` }), CHANNEL_AMOUNT: envalid.num({ default: 120000, desc: `The amount to fund a channel with` }), FUND_AMOUNT: envalid.num({ default: 360000, desc: `The total wallet funding required` }), NO_LSAT_CORE_WHITELIST: validateCoreIPList({ default: '', desc: 'A comma separated list of specific Core IPs to skip LSAT auth' }) } module.exports = { env: envalid.cleanEnv(process.env, envDefinitions, { strict: false }), // additional functions for testing purposes valCoreIPList: valCoreIPList, valNetwork: valNetwork } ================================================ FILE: lib/utils.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const jmespath = require('jmespath') const _ = require('lodash') const flattenKeys = (obj, path = []) => !_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}) // wait for a specified number of milliseconds to elapse function sleepAsync(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } /** * Add specified seconds to a Date object * * @param {Date} date - The starting date * @param {number} seconds - The seconds of seconds to add to the date * @returns {Date} */ function addSeconds(date, seconds) { return new Date(date.getTime() + seconds * 1000) } /** * Add specified minutes to a Date object * * @param {Date} date - The starting date * @param {number} minutes - The number of minutes to add to the date * @returns {Date} */ function addMinutes(date, minutes) { return new Date(date.getTime() + minutes * 60000) } /** * Convert Date to ISO8601 string, stripping milliseconds * '2017-03-19T23:24:32Z' * * @param {Date} date - The date to convert * @returns {string} An ISO8601 formatted time string */ function formatDateISO8601NoMs(date) { return date.toISOString().slice(0, 19) + 'Z' } /** * Convert strings in an Array of hashes to lower case * * @param {string[]} hashes - An array of string hashes to convert to lower case * @returns {string[]} An array of lowercase hash strings */ function lowerCaseHashes(hashes) { return hashes.map(hash => { return hash.toLowerCase() }) } function parseAnchorsComplete(proofObject, network) { // Because the minimum proof will contain a cal anchor, always start with cal let anchorsComplete = [network === 'mainnet' ? 'cal' : 'tcal'].concat( jmespath.search(proofObject, '[branches[].branches[].ops[].anchors[].type] | [0]') ) return anchorsComplete } /** * Checks if value is a hexadecimal string * * @param {string} value - The value to check * @returns {bool} true if value is a hexadecimal string, otherwise false */ function isHex(value) { var hexRegex = /^[0-9a-f]{2,}$/i var isHex = hexRegex.test(value) && !(value.length % 2) return isHex } /** * Checks if value is a uuid string * * @param {string} value - The value to check * @returns {bool} true if value is a hexadecimal string, otherwise false */ function isUUID(value) { var uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i return uuidRegex.test(value) } /** * Checks if value is a ulid string * * @param {string} value - The value to check * @returns {bool} true if value is a hexadecimal string, otherwise false */ function isULID(value) { var ulidRegex = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/i return ulidRegex.test(value) } /** * converts a hex value to a uuid * * @param {string} value - The value to convert * @returns {string} the segmented uuid string */ function hexToUUIDv1(hexString) { if (hexString.length < 32) return null let segment1 = hexString.substring(0, 8) let segment2 = hexString.substring(8, 12) let segment3 = hexString.substring(12, 16) let segment4 = hexString.substring(16, 20) let segment5 = hexString.substring(20, 32) return `${segment1}-${segment2}-${segment3}-${segment4}-${segment5}` } /** * Returns a random Integer between min and max * * @param {Integer} min - The min value to be returned * @param {Integer} max - The max value to be returned * @returns {Integer} The selected random Integer between min and max */ function randomIntFromInterval(min, max) { return Math.floor(Math.random() * (max - min + 1) + min) } function nodeUIPasswordBooleanCheck(pw = '') { if (_.isBoolean(pw) && pw === false) { return false } else { let password = pw.toLowerCase() if (password === 'false') return false } return pw } /** * Extracts the IP address from a Restify request object * * @param {req} value - The Restify request object * @returns {string} - The IP address, or null if it cannot be determined */ function getClientIP(req) { let xff, rcr, rsa try { xff = req.headers['x-forwarded-for'] } catch (error) { xff = null } try { rcr = req.connection.remoteAddress } catch (error) { rcr = null } try { rsa = req.socket.remoteAddress } catch (error) { rsa = null } let result = xff || rcr || rsa if (result) result = result.replace('::ffff:', '') return result || null } function jsonTransform(json, conditionFn, modifyFn) { // transform { responses: { category: 'first' } } to { 'responses.category': 'first' } const flattenedKeys = Object.keys(flattenKeys(json)) // Easily iterate over the flat json for (let i = 0; i < flattenedKeys.length; i++) { const key = flattenedKeys[i] const value = _.get(json, key) // Did the condition match the one we passed? if (conditionFn(key, value)) { // Replace the value to the new one _.set(json, key, modifyFn(key, value)) } } return json } module.exports = { sleepAsync: sleepAsync, addMinutes: addMinutes, addSeconds: addSeconds, formatDateISO8601NoMs: formatDateISO8601NoMs, lowerCaseHashes: lowerCaseHashes, parseAnchorsComplete: parseAnchorsComplete, isHex: isHex, isUUID: isUUID, isULID: isULID, hexToUUIDv1: hexToUUIDv1, randomIntFromInterval: randomIntFromInterval, nodeUIPasswordBooleanCheck: nodeUIPasswordBooleanCheck, getClientIP: getClientIP, jsonTransform: jsonTransform } ================================================ FILE: package.json ================================================ { "name": "chainpoint-gateway", "description": "A Chainpoint Network Gateway is a key part of a scalable solution for anchoring data to public blockchains.", "version": "1.2.0", "main": "server.js", "scripts": { "start": "node server.js", "eslint-check": "eslint --print-config . | eslint-config-prettier-check", "test": "mocha tests/*.js" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "linters": { "*.js": [ "eslint --fix", "git add" ], "*.{json,css,md}": [ "prettier --write", "git add" ] } }, "keywords": [ "Chainpoint", "bitcoin", "lightning", "Tierion", "node", "hash", "blockchain", "crypto", "cryptography", "sha256" ], "author": "Jason Bukowski (https://tierion.com)", "license": "Apache-2.0", "devDependencies": { "chai": "^4.2.0", "eslint": "^5.4.0", "eslint-config-prettier": "^3.0.1", "eslint-plugin-prettier": "^2.6.2", "eslint-plugin-react": "^7.11.1", "husky": "^1.3.1", "lint-staged": "^8.1.0", "mocha": "^5.2.0", "prettier": "^1.14.2", "rimraf": "^2.6.3", "supertest": "^3.4.2" }, "dependencies": { "async-await-parallel": "^1.0.0", "async-retry": "^1.2.3", "blake2s-js": "^1.3.0", "bluebird": "^3.5.5", "chainpoint-binary": "^5.1.1", "chainpoint-parse": "^5.0.1", "chalk": "^2.4.2", "dotenv": "^8.2.0", "envalid": "^4.2.0", "executive": "^1.6.3", "generate-password": "^1.4.2", "id128": "^1.6.6", "ip": "^1.1.5", "jmespath": "^0.15.0", "js-binary": "^1.2.0", "level-rocksdb": "^4.0.0", "lnrpc-node-client": "^1.1.2", "lodash": "^4.17.11", "lsat-js": "^2.0.0", "merkle-tools": "^1.4.0", "request": "^2.88.0", "request-promise-native": "^1.0.7", "restify": "^8.3.3", "restify-cors-middleware": "^1.1.1", "restify-errors": "^6.1.1", "universal-analytics": "^0.4.23", "uuid": "^3.3.2", "uuid-time": "^1.0.0", "uuid-validate": "^0.0.3", "validator": "^10.11.0", "winston": "^3.2.1", "winston-papertrail": "^1.0.5" } } ================================================ FILE: scripts/install_deps.sh ================================================ #!/bin/bash if [ -x "$(command -v docker)" ]; then echo "Docker already installed" else echo "Install docker" curl -fsSL https://get.docker.com -o get-docker.sh bash get-docker.sh sudo usermod -aG docker $USER fi if [[ "$OSTYPE" == "linux-gnu" ]]; then sudo apt-get -qq update -y sudo apt-get -qq install -y apt-utils sudo apt-get -qq install -y git make jq openssl curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt-get update && sudo apt-get install yarn sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose sudo ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose || echo Binary is not at usual location or is already linked elif [[ "$OSTYPE" == "darwin"* ]]; then ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install caskroom/cask/brew-cask brew cask install docker-toolbox brew install jq brew install homebrew/core/make brew install git brew install node brew install yarn brew install openssl fi yarn ================================================ FILE: scripts/prod_secrets_expand.sh ================================================ #!/bin/sh : ${ENV_SECRETS_DIR:=/run/secrets} function env_secret_debug() { if [ ! -z "$ENV_SECRETS_DEBUG" ]; then echo -e "\033[1m$@\033[0m" fi } # usage: env_secret_expand VAR # ie: env_secret_expand 'XYZ_DB_PASSWORD' # (will check for "$XYZ_DB_PASSWORD" variable value for a placeholder that defines the # name of the docker secret to use instead of the original value. For example: # XYZ_DB_PASSWORD=DOCKER-SECRET->my-db.secret env_secret_expand() { var="$1" eval val=\$$var if secret_name=$(expr match "$val" "DOCKER-SECRET->\([^}]\+\)$"); then secret="${ENV_SECRETS_DIR}/${secret_name}" env_secret_debug "Secret file for $var: $secret" if [ -f "$secret" ]; then val=$(cat "${secret}") export "$var"="$val" env_secret_debug "Expanded variable: $var=$val" else env_secret_debug "Secret file does not exist! $secret" fi fi } env_secrets_expand() { for env_var in $(printenv | cut -f1 -d"=") do env_secret_expand $env_var done if [ ! -z "$ENV_SECRETS_DEBUG" ]; then echo -e "\n\033[1mExpanded environment variables\033[0m" printenv fi } env_secrets_expand ================================================ FILE: scripts/run_prod.sh ================================================ #!/bin/bash cd $(dirname $0) source ./prod_secrets_expand.sh yarn start ================================================ FILE: server.js ================================================ /** * Copyright 2019 Tierion * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // load environment variables const env = require('./lib/parse-env.js').env const apiServer = require('./lib/api-server.js') const aggregator = require('./lib/aggregator.js') const { version } = require('./package.json') const rocksDB = require('./lib/models/RocksDB.js') const cachedProofs = require('./lib/cached-proofs.js') const cores = require('./lib/cores.js') const logger = require('./lib/logger.js') // establish a connection with the database async function openStorageConnectionAsync() { await rocksDB.openConnectionAsync() } // process all steps need to start the application async function startAsync() { try { logger.info(`App : Startup : Version ${version}`) // display NETWORK value logger.info(`App : Startup : Network : ${env.NETWORK}`) // establish a connection with the database await openStorageConnectionAsync() logger.info(`App : Startup : Storage Connection Opened`) // connect to the Cores listed in .env and check/open lightning connections await cores.connectAsync() logger.info(`App : Startup : Cores Connected`) // start API server await apiServer.startAsync(cores.getLn()) logger.info(`App : Startup : API Started`) // start the interval processes for refreshing the IP blocklist apiServer.startIPBlacklistRefreshInterval() // start the interval processes for aggregating and submitting hashes to Core aggregator.startAggInterval() // start the interval processes for pruning expired proof state data from RocksDB rocksDB.startPruningInterval() // start the interval processes for pruning cached proof data from memory cachedProofs.startPruneExpiredItemsInterval() // start the interval processes for pruning cached transaction data from memory cores.startPruneExpiredItemsInterval() // start monitoring health of peer/channel connections cores.startConnectionMonitoringInterval() logger.info(`App : Startup : Complete`) } catch (err) { logger.error(`App : Startup : ${err.message}`) // Unrecoverable Error : Exit cleanly (!), so Docker Compose `on-failure` policy // won't force a restart since this situation will not resolve itself. process.exit(0) } } // get the whole show started startAsync() ================================================ FILE: swarm-compose.yaml ================================================ version: "3.7" networks: chainpoint-gateway: secrets: HOT_WALLET_PASS: external: true HOT_WALLET_ADDRESS: external: true services: chainpoint-gateway: restart: on-failure entrypoint: /home/node/app/scripts/run_prod.sh volumes: - ./ip-blacklist.txt:/home/node/app/ip-blacklist.txt:ro - ~/.chainpoint/gateway/data/rocksdb:/root/.chainpoint/gateway/data/rocksdb - ./.env:/home/node/app/.env - ~/.chainpoint/gateway/.lnd:/root/.lnd:ro image: gcr.io/chainpoint-registry/github_chainpoint_chainpoint-gateway:${DOCKER_TAG:-latest} user: ${USERID}:${GROUPID} build: . deploy: mode: global placement: constraints: [node.role==manager] restart_policy: condition: any delay: 5s max_attempts: 15 window: 90s depends_on: - lnd ports: - target: 8080 published: 80 protocol: tcp mode: host networks: - chainpoint-gateway secrets: - HOT_WALLET_PASS - HOT_WALLET_ADDRESS environment: HOME: /root HOT_WALLET_PASS: DOCKER-SECRET->HOT_WALLET_PASS HOT_WALLET_ADDRESS: DOCKER-SECRET->HOT_WALLET_ADDRESS LND_SOCKET: ${LND_SOCKET} CHAINPOINT_CORE_CONNECT_IP_LIST: "${CHAINPOINT_CORE_CONNECT_IP_LIST}" AGGREGATION_INTERVAL_SECONDS: "${AGGREGATION_INTERVAL_SECONDS}" PROOF_EXPIRE_MINUTES: "${PROOF_EXPIRE_MINUTES}" POST_HASHES_MAX: "${POST_HASHES_MAX}" POST_VERIFY_PROOFS_MAX: "${POST_VERIFY_PROOFS_MAX}" GET_PROOFS_MAX: "${GET_PROOFS_MAX}" MAX_SATOSHI_PER_HASH: "${MAX_SATOSHI_PER_HASH}" NETWORK: ${NETWORK} NODE_ENV: ${NODE_ENV} CHANNEL_AMOUNT: ${CHANNEL_AMOUNT} FUND_AMOUNT: ${FUND_AMOUNT} NO_LSAT_CORE_WHITELIST: ${NO_LSAT_CORE_WHITELIST} GOOGLE_UA_ID: ${GOOGLE_UA_ID} PUBLIC_IP: ${LND_PUBLIC_IP} tty: true logging: driver: 'json-file' options: max-size: '1g' max-file: '5' # Lightning node lnd: image: tierion/lnd:${NETWORK:-testnet}-v0.14.1 user: ${USERID}:${GROUPID} entrypoint: "./start-lnd.sh" ports: - target: 8080 published: 8080 protocol: tcp mode: host - target: 9735 published: 9735 protocol: tcp mode: host - target: 10009 published: 10009 protocol: tcp mode: host deploy: restart_policy: condition: any delay: 5s max_attempts: 15 window: 90s endpoint_mode: dnsrr environment: - PUBLICIP=${LND_PUBLIC_IP} - RPCUSER - RPCPASS - NETWORK=${NETWORK:-testnet} - CHAIN - DEBUG=info - BACKEND=neutrino - NEUTRINO=faucet.lightning.community:18333 - LND_REST_PORT - LND_RPC_PORT - TLSPATH - TLSEXTRADOMAIN=lnd volumes: - ~/.chainpoint/gateway/.lnd:/root/.lnd:z networks: - chainpoint-gateway logging: driver: 'json-file' options: max-size: '1g' max-file: '5' ================================================ FILE: tests/RocksDB.js ================================================ /* global describe, it, before, after */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const rocksDB = require('../lib/models/RocksDB.js') const rmrf = require('rimraf') const uuidv1 = require('uuid/v1') const crypto = require('crypto') const TEST_ROCKS_DIR = './test_db' let insertedProofStateHashIdNodes = null describe('RocksDB Methods', () => { let db = null before(async () => { db = await rocksDB.openConnectionAsync(TEST_ROCKS_DIR) expect(db).to.be.a('object') }) after(() => { db.close(() => { rmrf.sync(TEST_ROCKS_DIR) }) }) describe('Proof State Functions', () => { it('should return the same data that was inserted', async () => { let sampleData = generateSampleProofStateData(100) await rocksDB.saveProofStatesBatchAsync(sampleData.state) let queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(sampleData.proofIdNodes) insertedProofStateHashIdNodes = sampleData.proofIdNodes queriedState = convertStateBackToBinaryForm(queriedState) expect(queriedState).to.deep.equal(sampleData.state) }) }) describe('Incoming Hash Functions', () => { let delOps = [] it('should return the same data that was inserted', async () => { let sampleData = generateSampleHashObjects(100) await rocksDB.queueIncomingHashObjectsAsync(sampleData) let getResults = await rocksDB.getIncomingHashesUpToAsync(Date.now) let queriedHashes = getResults[0] delOps = getResults[1] expect(queriedHashes).to.deep.equal(sampleData) }) after(async () => { await db.batch(delOps) }) }) describe('Generic key/value Functions', () => { it('should return the same value that was inserted', async () => { let keys = [] let values = [] for (let x = 0; x < 100; x++) { keys.push(`testKey${x}${crypto.randomBytes(8).toString('hex')}`) values.push(crypto.randomBytes(8).toString('hex')) } for (let x = 0; x < 100; x++) { await rocksDB.setAsync(keys[x], values[x]) } let getValues = [] for (let x = 0; x < 100; x++) { getValues.push(await rocksDB.getAsync(keys[x])) } expect(getValues).to.deep.equal(values) let delOps = [] for (let key of keys) { delOps.push({ type: 'del', key: key }) } rocksDB.deleteBatchAsync(delOps).then(async () => { let getValues = [] for (let x = 0; x < 100; x++) { getValues.push(await rocksDB.getAsync(keys[x])) } expect(getValues).to.deep.equal(values) }) }) }) describe('Delete/Prune Functions', () => { before(() => { rocksDB.setENV({ PROOF_EXPIRE_MINUTES: 0 }) }) it('should batch delete as expected', async () => { let keys = [] let values = [] for (let x = 0; x < 100; x++) { keys.push(`testKey${x}${crypto.randomBytes(8).toString('hex')}`) values.push(crypto.randomBytes(8).toString('hex')) } for (let x = 0; x < 100; x++) { await rocksDB.setAsync(keys[x], values[x]) } let getValues = [] for (let x = 0; x < 100; x++) { getValues.push(await rocksDB.getAsync(keys[x])) } expect(getValues).to.deep.equal(values) let delOps = [] for (let key of keys) { delOps.push({ type: 'del', key: `custom_key:${key}` }) } await rocksDB.deleteBatchAsync(delOps) getValues = [] for (let x = 0; x < 100; x++) { let getResult = await rocksDB.getAsync(keys[x]) expect(getResult).to.equal(null) } }) it('should initiate prune interval as expected', async () => { let interval = rocksDB.startPruningInterval() expect(interval).to.be.a('object') clearInterval(interval) }) it('should prune proof state data as expected', async () => { // retrieve inserted proof state, confirm it still exists let queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(insertedProofStateHashIdNodes) expect(queriedState).to.be.a('array') expect(queriedState.length).to.be.greaterThan(0) for (let x = 0; x < queriedState.length; x++) { expect(queriedState[x]).to.have.property('hash') expect(queriedState[x].hash).to.be.a('string') } // prune all proof state data (0 minute expiration) await rocksDB.pruneOldProofStateDataAsync() // retrieve inserted proof state, confirm it has all beed pruned queriedState = await rocksDB.getProofStatesBatchByProofIdsAsync(insertedProofStateHashIdNodes) expect(queriedState).to.be.a('array') expect(queriedState.length).to.be.greaterThan(0) for (let x = 0; x < queriedState.length; x++) { expect(queriedState[x]).to.have.property('hash') expect(queriedState[x].hash).to.equal(null) } }) }) describe('Other Functions', () => { it('hexToUUIDv1 should return null with invalid hex value', done => { let result = rocksDB.hexToUUIDv1('deadbeefcafe') expect(result).to.equal(null) done() }) it('hexToUUIDv1 should return the expected result with proper hex value', done => { let result = rocksDB.hexToUUIDv1('ed60c311ede60102689f66a9e98feab6') expect(result) .to.be.a('string') .and.to.equal('ed60c311-ede6-0102-689f-66a9e98feab6') done() }) }) }) // support functions function generateSampleProofStateData(batchSize) { let results = {} results.state = [] results.proofIdNodes = [] for (let x = 0; x < batchSize; x++) { let newHashIdNode = uuidv1() let submitId = uuidv1() results.state.push({ proofId: newHashIdNode, hash: crypto.randomBytes(32).toString('hex'), proofState: [Buffer.from(Math.round(Math.random()) ? '00' : '01', 'hex'), crypto.randomBytes(32)], submission: { submitId: submitId, cores: [ { ip: '65.1.12.122', proofId: uuidv1() }, { ip: '65.1.12.123', proofId: uuidv1() }, { ip: '65.1.12.124', proofId: uuidv1() } ] } }) results.proofIdNodes.push(newHashIdNode) } return results } function convertStateBackToBinaryForm(queriedState) { for (let stateItem of queriedState) { let binState for (let psItem of stateItem.proofState) { binState = [] if (psItem.left) { binState.push(Buffer.from('00', 'hex')) binState.push(Buffer.from(psItem.left, 'hex')) } else { binState.push(Buffer.from('01', 'hex')) binState.push(Buffer.from(psItem.right, 'hex')) } } stateItem.proofState = binState } return queriedState } function generateSampleHashObjects(batchSize) { let results = [] for (let x = 0; x < batchSize; x++) { results.push({ proof_id: uuidv1(), hash: crypto.randomBytes(32).toString('hex') }) } return results } ================================================ FILE: tests/aggregator.js ================================================ /* global describe, it, before, after */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const aggregator = require('../lib/aggregator.js') const uuidv1 = require('uuid/v1') const crypto = require('crypto') const BLAKE2s = require('blake2s-js') const MerkleTools = require('merkle-tools') describe('Aggregator Methods', () => { describe('startAggInterval', () => { it('should initiate interval as expected', async () => { let interval = aggregator.startAggInterval() expect(interval).to.be.a('object') clearInterval(interval) }) }) describe('aggregateSubmitAndPersistAsync with 0 hashes', () => { let hashCount = 0 let IncomingHashes = generateIncomingHashData(hashCount) let ProofStateData = [] before(() => { aggregator.setRocksDB({ getIncomingHashesUpToAsync: async () => { let delOps = IncomingHashes.map(item => { return { type: 'del', key: item.proof_id } }) return [IncomingHashes, delOps] } }) }) after(() => {}) it('should complete successfully', async () => { expect(IncomingHashes.length).to.equal(hashCount) await aggregator.aggregateSubmitAndPersistAsync() expect(IncomingHashes.length).to.equal(0) expect(ProofStateData.length).to.equal(hashCount) }) }) describe('aggregateSubmitAndPersistAsync with 100 hashes', () => { let hashCount = 100 let IncomingHashes = generateIncomingHashData(hashCount) let newHashIdCore1 = null let newHashIdCore2 = null let ProofStateData = null let ip1 = '65.21.21.122' let ip2 = '65.21.21.123' before(() => { aggregator.setRocksDB({ getIncomingHashesUpToAsync: async () => { let delOps = IncomingHashes.map(item => { return { type: 'del', key: item.proof_id } }) return [IncomingHashes, delOps] }, deleteBatchAsync: async delOps => { let delHashIds = delOps.map(item => item.key) IncomingHashes = IncomingHashes.filter(item => !delHashIds.includes(item.proof_id)) }, saveProofStatesBatchAsync: async items => { ProofStateData = items } }) aggregator.setCores({ submitHashAsync: async () => { let hash = crypto.randomBytes(32).toString('hex') newHashIdCore1 = generateBlakeEmbeddedUUID(hash) newHashIdCore2 = generateBlakeEmbeddedUUID(hash) return [ { ip: ip1, response: { hash_id: newHashIdCore1, hash: hash, processing_hints: 'hints' } }, { ip: ip2, response: { hash_id: newHashIdCore2, hash: hash, processing_hints: 'hints' } } ] } }) }) after(() => {}) it('should complete successfully', async () => { var merkleTools = new MerkleTools() expect(IncomingHashes.length).to.equal(hashCount) let aggRoot = await aggregator.aggregateSubmitAndPersistAsync() expect(IncomingHashes.length).to.equal(0) expect(ProofStateData.length).to.equal(hashCount) for (let x = 0; x < hashCount; x++) { expect(ProofStateData[x]) .to.have.property('proofId') .and.and.be.a('string') expect(ProofStateData[x]) .to.have.property('hash') .and.and.be.a('string') expect(ProofStateData[x]) .to.have.property('proofState') .and.and.be.a('array') // add the additional nodeId operation to get final leaf values let proofIdBuffer = Buffer.from(`node_id:${ProofStateData[x].proofId}`, 'utf8') let hashBuffer = Buffer.from(ProofStateData[x].hash, 'hex') ProofStateData[x].hash = crypto .createHash('sha256') .update(Buffer.concat([proofIdBuffer, hashBuffer])) .digest() // convert from binary let proofState = [] for (let y = 0; y < ProofStateData[x].proofState.length; y += 2) { let operand = ProofStateData[x].proofState[y + 1].toString('hex') let isLeft = ProofStateData[x].proofState[y].toString('hex') === '00' let fullOp if (isLeft) { fullOp = { left: operand } } else { fullOp = { right: operand } } proofState.push(fullOp) } expect(merkleTools.validateProof(proofState, ProofStateData[x].hash, aggRoot)).to.equal(true) expect(ProofStateData[x]).to.have.property('submission') expect(ProofStateData[x].submission).to.be.a('object') expect(ProofStateData[x].submission) .to.have.property('submitId') .and.and.be.a('string') expect(ProofStateData[x].submission).to.have.property('cores') expect(ProofStateData[x].submission.cores).to.to.a('array') expect(ProofStateData[x].submission.cores.length).to.equal(2) expect(ProofStateData[x].submission.cores[0]).to.be.a('object') expect(ProofStateData[x].submission.cores[0]) .to.have.property('ip') .and.and.be.a('string') .and.to.equal(ip1) expect(ProofStateData[x].submission.cores[0]) .to.have.property('proofId') .and.and.be.a('string') .and.to.equal(newHashIdCore1) expect(ProofStateData[x].submission.cores[1]).to.be.a('object') expect(ProofStateData[x].submission.cores[1]) .to.have.property('ip') .and.and.be.a('string') .and.to.equal(ip2) expect(ProofStateData[x].submission.cores[1]) .to.have.property('proofId') .and.and.be.a('string') .and.to.equal(newHashIdCore2) } }) }) }) // support functions function generateIncomingHashData(batchSize) { let hashes = [] for (let x = 0; x < batchSize; x++) { let newHashIdNode = uuidv1() hashes.push({ proof_id: newHashIdNode, hash: crypto.randomBytes(32).toString('hex') }) } return hashes } function generateBlakeEmbeddedUUID(hash) { let timestampDate = new Date() let timestampMS = timestampDate.getTime() // 5 byte length BLAKE2s hash w/ personalization let h = new BLAKE2s(5, { personalization: Buffer.from('CHAINPNT') }) let hashStr = [timestampMS.toString(), timestampMS.toString().length, hash, hash.length].join(':') h.update(Buffer.from(hashStr)) return uuidv1({ msecs: timestampMS, node: Buffer.concat([Buffer.from([0x01]), h.digest()]) }) } ================================================ FILE: tests/api-server.js ================================================ /* global describe, it, beforeEach, before */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const apiServer = require('../lib/api-server.js') let rocksData = {} const TOR_IPS_KEY = 'blacklist:tor:ips' describe('API Server Methods', () => { beforeEach(() => { apiServer.setRocksDB({ getAsync: async key => rocksData[key], setAsync: async (key, value) => { rocksData[key] = value.toString() } }) }) describe('startIPBlacklistRefreshInterval', () => { it('should initiate interval as expected', async () => { let interval = apiServer.startIPBlacklistRefreshInterval() expect(interval).to.be.a('object') clearInterval(interval) }) }) describe('refreshIPBlacklistAsync with tor request failure, cache read failure', () => { before(() => { apiServer.setRP(async () => { throw 'bad' }) apiServer.setRocksDB(null) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(0) }) }) describe('refreshIPBlacklistAsync with tor request failure, empty cache', () => { before(() => { apiServer.setRP(async () => { throw 'bad' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(0) }) }) describe('refreshIPBlacklistAsync with tor request failure, cache present', () => { before(() => { apiServer.setRP(async () => { throw 'bad' }) rocksData[TOR_IPS_KEY] = '65.1.1.1,202.10.0.12' }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('65.1.1.1') expect(ips[1]) .to.be.a('string') .and.to.equal('202.10.0.12') }) }) describe('refreshIPBlacklistAsync with tor request success, cache write failure', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setRocksDB(null) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, cache write success', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, no local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => false }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, empty local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, malformatted local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => 'these arent IPs!' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, semi-malformatted local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '162.247.74.202\ninvalid' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(3) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') expect(ips[2]) .to.be.a('string') .and.to.equal('162.247.74.202') }) }) describe('refreshIPBlacklistAsync with tor request success, semi-duplicate local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '162.247.74.201\ninvalid' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(2) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') }) }) describe('refreshIPBlacklistAsync with tor request success, semi-commented local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '162.247.74.204\n#67.1.1.1' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(3) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') expect(ips[2]) .to.be.a('string') .and.to.equal('162.247.74.204') }) }) describe('refreshIPBlacklistAsync with tor request success, IPv6 local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '::ffff:172.18.0.1' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(3) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') expect(ips[2]) .to.be.a('string') .and.to.equal('::ffff:172.18.0.1') }) }) describe('refreshIPBlacklistAsync with full tor request success, good local list', () => { before(() => { apiServer.setRP(async () => { return 'ExitNode 0011BD2485AD45D984EC4159C88FC066E5E3300E\nPublished 2019-02-17 23:51:28\nLastStatus 2019-02-18 01:18:34\nExitAddress 162.247.74.201 2019-02-18 01:22:36\nExitNode 003D78825E0B9609EECFF5E4E0529717772E53C7\nPublished 2019-02-18 14:56:19\nLastStatus 2019-02-18 16:02:35\nExitAddress 104.218.63.73 2019-02-18 16:07:38' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '65.1.1.1\n65.2.2.2\n65.3.3.3' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(5) expect(ips[0]) .to.be.a('string') .and.to.equal('162.247.74.201') expect(ips[1]) .to.be.a('string') .and.to.equal('104.218.63.73') expect(ips[2]) .to.be.a('string') .and.to.equal('65.1.1.1') expect(ips[3]) .to.be.a('string') .and.to.equal('65.2.2.2') expect(ips[4]) .to.be.a('string') .and.to.equal('65.3.3.3') }) }) describe('refreshIPBlacklistAsync with empty tor request success, good local list', () => { before(() => { apiServer.setRP(async () => { return '' }) apiServer.setFS({ existsSync: () => true, readFileSync: () => '65.1.1.1\n65.2.2.2\n65.3.3.3' }) }) it('should return expected value', async () => { let ips = await apiServer.refreshIPBlacklistAsync() expect(ips).to.be.a('array') expect(ips.length).to.equal(3) expect(ips[0]) .to.be.a('string') .and.to.equal('65.1.1.1') expect(ips[1]) .to.be.a('string') .and.to.equal('65.2.2.2') expect(ips[2]) .to.be.a('string') .and.to.equal('65.3.3.3') }) }) }) ================================================ FILE: tests/cached-proofs.js ================================================ /* global describe, it, before */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const fs = require('fs') const cachedProofs = require('../lib/cached-proofs.js') describe('Cached Proofs Methods', () => { describe('startPruneExpiredItemsInterval', () => { it('should initiate interval as expected', async () => { let interval = cachedProofs.startPruneExpiredItemsInterval() expect(interval).to.be.a('object') clearInterval(interval) }) }) describe('getPruneExpiredIntervalSeconds', () => { it('should return expected value', async () => { let seconds = cachedProofs.getPruneExpiredIntervalSeconds() expect(seconds) .to.be.a('number') .and.to.equal(10) }) }) describe('pruneExpiredItems', () => { it('should prune no entries with all new items', done => { let in15Minutes = Date.now() + 15 * 60 * 1000 cachedProofs.setCoreProofCache({ '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: in15Minutes }, '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: in15Minutes } }) let cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) cachedProofs.pruneExpiredItems() cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) done() }) it('should prune one of two entries with new and old items', done => { let in15Minutes = Date.now() + 15 * 60 * 1000 let ago15Minutes = Date.now() - 15 * 60 * 1000 cachedProofs.setCoreProofCache({ '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: in15Minutes }, '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: ago15Minutes } }) let cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) cachedProofs.pruneExpiredItems() cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.not.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') done() }) it('should prune all entries with old items', done => { let ago15Minutes = Date.now() - 15 * 60 * 1000 cachedProofs.setCoreProofCache({ '66a34bd0-f4e7-11e7-a52b-016a36a9d789': { expiresAt: ago15Minutes }, '66bd6380-f4e7-11e7-895d-0176dc2220aa': { expiresAt: ago15Minutes } }) let cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66a34bd0-f4e7-11e7-a52b-016a36a9d789'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) expect(cache).to.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['66bd6380-f4e7-11e7-895d-0176dc2220aa'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) cachedProofs.pruneExpiredItems() cache = cachedProofs.getCoreProofCache() expect(cache).to.be.a('object') expect(cache).to.not.have.property('66a34bd0-f4e7-11e7-a52b-016a36a9d789') expect(cache).to.not.have.property('66bd6380-f4e7-11e7-895d-0176dc2220aa') done() }) }) describe('getCachedCoreProofsAsync with unknown hash_ids', () => { let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: () => [{ hash_id: proofId1, proof: null }, { hash_id: proofId2, proof: null }] }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.equal(null) expect(results[0]).to.not.have.property('anchorsComplete') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.equal(null) expect(results[1]).to.not.have.property('anchorsComplete') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.equal(null) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000) .and.to.be.lessThan(Date.now() + 1.5 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.equal(null) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000) .and.to.be.lessThan(Date.now() + 1.1 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with valid, cached hash_ids - mainnet', () => { let in15Minutes = Date.now() + 15 * 60 * 1000 let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) let cacheContents = { [submitId1]: { coreProof: proofObj1, expiresAt: in15Minutes }, [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes } } before(() => { cachedProofs.setCoreProofCache(cacheContents) cachedProofs.setCores({ getProofsAsync: () => { throw 'Do not call!' } }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.deep.equal(cacheContents) }) }) describe('getCachedCoreProofsAsync with valid, cached hash_ids - testnet', () => { let in15Minutes = Date.now() + 15 * 60 * 1000 let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-tcal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-tbtc-proof.chp.json')) let cacheContents = { [submitId1]: { coreProof: proofObj1, expiresAt: in15Minutes }, [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes } } before(() => { cachedProofs.setCoreProofCache(cacheContents) cachedProofs.setCores({ getProofsAsync: () => { throw 'Do not call!' } }) cachedProofs.setENV({ NETWORK: 'testnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('tcal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('tcal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('tbtc') expect(cache).to.be.a('object') expect(cache).to.deep.equal(cacheContents) }) }) describe('getCachedCoreProofsAsync with valid, non-cached hash_ids', () => { let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: () => [{ hash_id: proofId1, proof: proofObj1 }, { hash_id: proofId2, proof: proofObj2 }] }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.deep.equal(proofObj1) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000) .and.to.be.lessThan(Date.now() + 16 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000) .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with valid, cached and non-cached hash_ids, cache a null result', () => { let in15Minutes = Date.now() + 15 * 60 * 100 let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let proofId3 = '66bd6380-f4e7-11e7-895d-0176dc2220ff' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId3 = '77bd6380-f4e7-11e7-895d-0176dc2220ff' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let submission3 = { submitId: submitId3, cores: [{ ip: ip, proofId: proofId3 }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) let cacheContents = { [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes } } before(() => { cachedProofs.setCoreProofCache(cacheContents) cachedProofs.setCores({ getProofsAsync: () => [{ hash_id: proofId1, proof: proofObj1 }, { hash_id: proofId3, proof: null }] }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2, submission3]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(3) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(results[2]).to.be.a('object') expect(results[2]) .to.have.property('submitId') .and.to.equal(submitId3) expect(results[2]) .to.have.property('proof') .and.to.equal(null) expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.deep.equal(proofObj1) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000) .and.to.be.lessThan(Date.now() + 16 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt).to.equal(in15Minutes) expect(cache).to.have.property(submitId3) expect(cache[submitId3]).to.be.a('object') expect(cache[submitId3]).to.have.property('coreProof') expect(cache[submitId3].coreProof).to.equal(null) expect(cache[submitId3]).to.have.property('expiresAt') expect(cache[submitId3].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000) .and.to.be.lessThan(Date.now() + 1.1 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with mixed, cached and unknown hash_ids, cache a null result', () => { let in15Minutes = Date.now() + 15 * 60 * 1000 let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) let cacheContents = { [submitId2]: { coreProof: proofObj2, expiresAt: in15Minutes } } before(() => { cachedProofs.setCoreProofCache(cacheContents) cachedProofs.setCores({ getProofsAsync: () => [{ hash_id: proofId1, proof: null }] }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.equal(null) expect(results[0]).to.not.have.property('anchorsComplete') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt).to.equal(in15Minutes) expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.equal(null) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000) .and.to.be.lessThan(Date.now() + 1.5 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with mixed, non-cached and unknown hash_ids', () => { let proofId1 = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2 = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip = '65.1.1.1' let submission1 = { submitId: submitId1, cores: [{ ip: ip, proofId: proofId1 }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip, proofId: proofId2 }] } let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: () => [{ hash_id: proofId1, proof: null }, { hash_id: proofId2, proof: proofObj2 }] }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.equal(null) expect(results[0]).to.not.have.property('anchorsComplete') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.equal(null) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 0.9 * 60 * 1000) .and.to.be.lessThan(Date.now() + 1.1 * 60 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000) .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, first IP bad', () => { let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId1b = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa' let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip1 = '65.1.1.1' let ip2 = '65.2.2.2' let submission1 = { submitId: submitId1, cores: [{ ip: ip1, proofId: proofId1a }, { ip: ip2, proofId: proofId1b }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip1, proofId: proofId2a }, { ip: ip2, proofId: proofId2b }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: ip => { if (ip === ip1) throw new Error('Bad IP') return [{ hash_id: proofId1b, proof: proofObj1 }, { hash_id: proofId2b, proof: proofObj2 }] } }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.deep.equal(proofObj1) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000) .and.to.be.lessThan(Date.now() + 16 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000) .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, IP bad, different sub counts', () => { let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa' let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '77a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let ip1 = '65.1.1.1' let ip2 = '65.2.2.2' let submission1 = { submitId: submitId1, cores: [{ ip: ip1, proofId: proofId1a }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip1, proofId: proofId2a }, { ip: ip2, proofId: proofId2b }] } let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: ip => { if (ip === ip1) throw new Error('Bad IP') return [{ hash_id: proofId2b, proof: proofObj2 }] } }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.equal(null) expect(results[0]).to.not.have.property('anchorsComplete') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.not.have.property(submitId1) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000) .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000) }) }) describe('getCachedCoreProofsAsync with valid, non-cached hash_ids, two IPs bad, different sub counts and IPs', () => { let proofId1a = '55a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId1b = '66a34bd0-f4e7-11e7-a52b-016a36a9d789' let proofId2a = '55bd6380-f4e7-11e7-895d-0176dc2220aa' let proofId2b = '66bd6380-f4e7-11e7-895d-0176dc2220aa' let proofId2c = '77bd6380-f4e7-11e7-895d-0176dc2220aa' let submitId1 = '88a34bd0-f4e7-11e7-a52b-016a36a9d789' let submitId2 = '88bd6380-f4e7-11e7-895d-0176dc2220aa' let ip1a = '65.1.1.1' let ip1b = '65.2.2.2' let ip2a = '65.3.3.3' let ip2b = '65.4.4.4' let ip2c = '65.5.5.5' let submission1 = { submitId: submitId1, cores: [{ ip: ip1a, proofId: proofId1a }, { ip: ip1b, proofId: proofId1b }] } let submission2 = { submitId: submitId2, cores: [{ ip: ip2a, proofId: proofId2a }, { ip: ip2b, proofId: proofId2b }, { ip: ip2c, proofId: proofId2c }] } let proofObj1 = JSON.parse(fs.readFileSync('./tests/sample-data/core-cal-proof.chp.json')) let proofObj2 = JSON.parse(fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json')) before(() => { cachedProofs.setCoreProofCache({}) cachedProofs.setCores({ getProofsAsync: ip => { if (ip === ip1a || ip == ip2a || ip == ip2b) throw new Error('Bad IP') if (ip == ip1b) return [{ hash_id: proofId1b, proof: proofObj1 }] if (ip == ip2c) return [{ hash_id: proofId2c, proof: proofObj2 }] } }) cachedProofs.setENV({ NETWORK: 'mainnet' }) }) it('should return expected value', async () => { let results = await cachedProofs.getCachedCoreProofsAsync([submission1, submission2]) let cache = cachedProofs.getCoreProofCache() expect(results).to.be.a('array') expect(results.length).to.equal(2) expect(results[0]).to.be.a('object') expect(results[0]) .to.have.property('submitId') .and.to.equal(submitId1) expect(results[0]) .to.have.property('proof') .and.to.deep.equal(proofObj1) expect(results[0]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[0].anchorsComplete.length).to.equal(1) expect(results[0].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1]).to.be.a('object') expect(results[1]) .to.have.property('submitId') .and.to.equal(submitId2) expect(results[1]) .to.have.property('proof') .and.to.deep.equal(proofObj2) expect(results[1]) .to.have.property('anchorsComplete') .and.to.be.a('array') expect(results[1].anchorsComplete.length).to.equal(2) expect(results[1].anchorsComplete[0]) .to.be.a('string') .and.to.equal('cal') expect(results[1].anchorsComplete[1]) .to.be.a('string') .and.to.equal('btc') expect(cache).to.be.a('object') expect(cache).to.have.property(submitId1) expect(cache[submitId1]).to.be.a('object') expect(cache[submitId1]).to.have.property('coreProof') expect(cache[submitId1].coreProof).to.deep.equal(proofObj1) expect(cache[submitId1]).to.have.property('expiresAt') expect(cache[submitId1].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 14 * 60 * 1000) .and.to.be.lessThan(Date.now() + 16 * 60 * 1000) expect(cache).to.have.property(submitId2) expect(cache[submitId2]).to.be.a('object') expect(cache[submitId2]).to.have.property('coreProof') expect(cache[submitId2].coreProof).to.deep.equal(proofObj2) expect(cache[submitId2]).to.have.property('expiresAt') expect(cache[submitId2].expiresAt) .to.be.a('number') .and.to.be.greaterThan(Date.now() + 24 * 60 * 60 * 1000) .and.to.be.lessThan(Date.now() + 26 * 60 * 60 * 1000) }) }) }) ================================================ FILE: tests/calendar.js ================================================ /* global describe, it beforeEach, afterEach */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const request = require('supertest') const app = require('../lib/api-server.js') const calendar = require('../lib/endpoints/calendar.js') describe('Calendar Controller', () => { let txIdKnown = '52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50' let txKnownData = { tx: { data: 'data!' } } let insecureServer = null beforeEach(async () => { insecureServer = await app.startInsecureRestifyServerAsync() calendar.setCores({ getCachedTransactionAsync: txId => { if (txId === txIdKnown) return txKnownData return null } }) }) afterEach(() => { insecureServer.close() }) describe('GET /calendar/:txId/data', () => { it('should return the proper error with non hex txId', done => { request(insecureServer) .get('/calendar/nothex/data') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, invalid txId present') done() }) }) it('should return the proper error with hex txId -- short', done => { request(insecureServer) .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d/data') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, invalid txId present') done() }) }) it('should return the proper error with hex txId -- long', done => { request(insecureServer) .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d5050/data') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, invalid txId present') done() }) }) it('should return the proper error with valid, not found', done => { request(insecureServer) .get('/calendar/52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d51/data') .expect('Content-type', /json/) .expect(404) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('NotFound') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('') done() }) }) it('should return the proper result on success', done => { request(insecureServer) .get('/calendar/' + txIdKnown + '/data') .expect('Content-type', /text/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.text).to.equal(txKnownData.tx.data) done() }) }) }) }) ================================================ FILE: tests/config.js ================================================ /* global describe, it, beforeEach, afterEach */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const request = require('supertest') const app = require('../lib/api-server.js') const { version } = require('../package.json') describe('Config Controller', () => { let insecureServer = null beforeEach(async () => { insecureServer = await app.startInsecureRestifyServerAsync() }) afterEach(() => { insecureServer.close() }) describe('GET /config', () => { it('should return a valid config object', done => { request(insecureServer) .get('/config') .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(Object.keys(res.body).length).to.equal(2) expect(res.body) .to.have.property('version') .and.to.be.a('string') .and.to.equal(version) expect(res.body) .to.have.property('time') .and.to.be.a('string') done() }) }) }) }) ================================================ FILE: tests/cores.js ================================================ /* global describe, it, before, beforeEach, afterEach */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const { Lsat } = require('lsat-js') const cores = require('../lib/cores.js') const data = require('./sample-data/lsat-data.json') const { version } = require('../package.json') describe.only('Cores Methods', function() { this.timeout(5000) describe('startPruneExpiredItemsInterval', () => { it('should initiate interval as expected', async () => { let interval = cores.startPruneExpiredItemsInterval() expect(interval).to.be.a('object') clearInterval(interval) }) }) describe('getPruneExpiredIntervalSeconds', () => { it('should return expected value', async () => { let seconds = cores.getPruneExpiredIntervalSeconds() expect(seconds) .to.be.a('number') .and.to.equal(10) }) }) describe('pruneExpiredItems', () => { it('should prune no entries with all new items', done => { let in15Minutes = Date.now() + 15 * 60 * 1000 cores.setCoreTxCache({ '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: in15Minutes }, '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: in15Minutes } }) let cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) cores.pruneExpiredItems() cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) done() }) it('should prune one of two entries with new and old items', done => { let in15Minutes = Date.now() + 15 * 60 * 1000 let ago15Minutes = Date.now() - 15 * 60 * 1000 cores.setCoreTxCache({ '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: in15Minutes }, '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: ago15Minutes } }) let cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) cores.pruneExpiredItems() cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt) .to.be.a('number') .and.to.equal(in15Minutes) expect(cache).to.not.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') done() }) it('should prune all entries with old items', done => { let ago15Minutes = Date.now() - 15 * 60 * 1000 cores.setCoreTxCache({ '1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b': { expiresAt: ago15Minutes }, '28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257': { expiresAt: ago15Minutes } }) let cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) expect(cache).to.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257']) .to.be.a('object') .and.to.have.property('expiresAt') expect(cache['28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257'].expiresAt) .to.be.a('number') .and.to.equal(ago15Minutes) cores.pruneExpiredItems() cache = cores.getCoreTxCache() expect(cache).to.be.a('object') expect(cache).to.not.have.property('1b7930a6fc0fe36d31318cfd3ebaed550cf28eaef171b06067bfbc184d2f206b') expect(cache).to.not.have.property('28a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257') done() }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' }) cores.setRP(async () => { throw 'Bad IP' }) }) it('should not connect and throw error with IP list and Bad IP', async () => { let coreConnectionCount = 2 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' }) cores.setRP(async () => { return { body: { network: 'testnet', sync_info: { catching_up: true } } } }) }) it('should not connect and throw error with IP list and non-synced Core', async () => { let coreConnectionCount = 2 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' }) cores.setRP(async () => { return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should not connect and throw error with IP list and synced Core, insufficient count', async () => { let coreConnectionCount = 2 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(1) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' }) cores.setRP(async () => { return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should connect with IP list and synced Core, sufficient count 1', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err } expect(errResult).to.equal(null) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(1) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.2.2.2', '65.3.3.3'], NETWORK: 'testnet' }) let counter = 1 cores.setRP(async () => { return { body: { network: 'testnet', sync_info: { catching_up: counter++ % 2 ? false : true } } } }) }) it('should connect with IP list and mixed-synced Core, sufficient count 2', async () => { let coreConnectionCount = 2 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err } expect(errResult).to.equal(null) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(2) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.2.2.2', '65.3.3.3'], NETWORK: 'testnet' }) cores.setRP(async () => { return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should connect with IP list and synced Core, sufficient count 3', async () => { let coreConnectionCount = 3 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err } expect(errResult).to.equal(null) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(3) }) }) describe('connectAsync', () => { let options = null before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'], NETWORK: 'testnet' }) cores.setRP(async o => { options = o return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should use proper headers on Core requests', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) await cores.connectAsync() expect(options).to.be.a('object') expect(options).to.have.property('headers') expect(options.headers).to.have.property('X-Node-Version') expect(options.headers['X-Node-Version']).to.equal(version) }) }) describe('connectAsync', () => { before(() => { cores.setRP(async () => { throw 'Bad IP' }) }) it('should not connect and throw error with Core discovery and bad discovery', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setRP(async opts => { if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] } throw 'Bad IP' }) }) it('should not connect and throw error with Core discovery and bad IP returned', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setRP(async opts => { if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] } return { body: { sync_info: { catching_up: true } } } }) }) it('should not connect and throw error with Core discovery and unsynced returned', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err.message } expect(errResult).to.equal(`Unable to connect to ${coreConnectionCount} Core(s) as required`) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ NETWORK: 'mainnet' }) cores.setRP(async opts => { if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] } return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should not connect with Core discovery and synced IP, network mismatch', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err } expect(errResult.message).to.equal('Unable to connect to 1 Core(s) as required') let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(0) }) }) describe('connectAsync', () => { before(() => { cores.setENV({ NETWORK: 'testnet' }) cores.setRP(async opts => { if (opts.uri.endsWith('peers')) return { body: [{ remote_ip: '65.1.1.1' }] } return { body: { network: 'testnet', sync_info: { catching_up: false } } } }) }) it('should connect with Core discovery and synced IP', async () => { let coreConnectionCount = 1 cores.setCoreConnectionCount(coreConnectionCount) let errResult = null try { await cores.connectAsync() } catch (err) { errResult = err } expect(errResult).to.equal(null) let connectedIPs = cores.getCoreConnectedIPs() expect(connectedIPs.length).to.equal(1) }) }) describe('parse402Response', () => { let lsat, challenge, response before(() => { challenge = data.challenge1000 lsat = Lsat.fromChallenge(challenge) response = { statusCode: 402, headers: { 'www-authenticate': challenge } } }) it('should throw if no LSAT challenge present in response or not a 402', () => { const parseWrongStatusCode = () => cores.parse402Response({ ...response, statusCode: 401 }) const parseMissingHeader = () => cores.parse402Response({ statusCode: 402 }) expect(parseWrongStatusCode).to.throw() expect(parseMissingHeader).to.throw() }) it('should should return an LSAT with invoice information', () => { const lsatFromResponse = cores.parse402Response(response) expect(lsatFromResponse.invoice).to.exist expect(lsatFromResponse.invoice).to.equal(lsat.invoice) }) }) describe('submitHashAsync', () => { let challengeResponse, env, coreList beforeEach(() => { coreList = ['65.1.1.1', '65.2.2.2', '65.3.3.3'] env = { MAX_SATOSHI_PER_HASH: 10, CHAINPOINT_CORE_CONNECT_IP_LIST: [coreList[0]] } cores.setENV(env) cores.setLN({ callMethodAsync: async (s, m) => { if (m === 'sendPayment') return { on: (n, func) => func('ok'), end: () => null, write: () => {} } return {} } }) challengeResponse = { statusCode: 402, response: { statusCode: 402, headers: { 'www-authenticate': data.challenge10 }, body: { error: { message: 'Payment Required.' } } } } }) afterEach(() => { cores.setENV({}) cores.setLN({}) cores.setRP(() => {}) }) it('should return [] on 1 of 1 invoice amount to high failure', async () => { cores.setENV({ ...env, MAX_SATOSHI_PER_HASH: 5 }) cores.setRP(async () => { throw challengeResponse }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(0) }) it('should return [] on 1 of 1 submit failure', async () => { let counter = 0 cores.setRP(async () => { if (++counter === 1) throw 'Bad Submit' throw challengeResponse }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(0) }) it('should succeed on 1 of 1 item submitted', async () => { cores.setRP(options => { if (options.headers['Authorization']) return { body: 'ok' } throw challengeResponse }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(1) expect(result[0]).to.be.a('object') expect(result[0]).to.have.property('ip') expect(result[0].ip).to.equal('65.1.1.1') expect(result[0]).to.have.property('response') expect(result[0].response).to.equal('ok') }) it('should succeed on 2 of 3 item submitted, one bad IP', async () => { cores.setRP(async options => { if (options.uri.includes(coreList[1])) throw 'Bad IP!' if (options.headers['Authorization']) return { body: 'ok' } throw challengeResponse }) cores.setENV({ ...env, CHAINPOINT_CORE_CONNECT_IP_LIST: coreList }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(2) expect(result[0]).to.be.a('object') expect(result[0]).to.have.property('ip') expect(result[0].ip).to.equal(coreList[0]) expect(result[0]).to.have.property('response') expect(result[0].response).to.equal('ok') expect(result[1]).to.be.a('object') expect(result[1]).to.have.property('ip') expect(result[1].ip).to.equal(coreList[2]) expect(result[1]).to.have.property('response') expect(result[1].response).to.equal('ok') }) it('should succeed on 2 of 3 item submitted, one invoice amount too high', async () => { cores.setENV({ ...env, CHAINPOINT_CORE_CONNECT_IP_LIST: coreList }) cores.setRP(async options => { if (options.uri.includes(coreList[1])) { let response = { statusCode: 402, response: { statusCode: 402, headers: { 'www-authenticate': data.challenge1000 }, body: { error: { message: 'Payment Required.' } } } } throw response } if (options.headers['Authorization']) return { body: 'ok' } throw challengeResponse }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(2) expect(result[0]).to.be.a('object') expect(result[0]).to.have.property('ip') expect(result[0].ip).to.equal(coreList[0]) expect(result[0]).to.have.property('response') expect(result[0].response).to.equal('ok') expect(result[1]).to.be.a('object') expect(result[1]).to.have.property('ip') expect(result[1].ip).to.equal(coreList[2]) expect(result[1]).to.have.property('response') expect(result[1].response).to.equal('ok') }) it('should succeed on 3 of 3 item submitted', async () => { cores.setENV({ ...env, CHAINPOINT_CORE_CONNECT_IP_LIST: coreList }) cores.setRP(async options => { if (options.headers['Authorization']) return { body: 'ok' } throw challengeResponse }) let result = await cores.submitHashAsync('deadbeefcafe') expect(result).to.be.a('array') expect(result.length).to.equal(3) expect(result[0]).to.be.a('object') expect(result[0]).to.have.property('ip') expect(result[0].ip).to.equal('65.1.1.1') expect(result[0]).to.have.property('response') expect(result[0].response).to.equal('ok') expect(result[1]).to.be.a('object') expect(result[1]).to.have.property('ip') expect(result[1].ip).to.equal('65.2.2.2') expect(result[1]).to.have.property('response') expect(result[1].response).to.equal('ok') expect(result[2]).to.be.a('object') expect(result[2]).to.have.property('ip') expect(result[2].ip).to.equal('65.3.3.3') expect(result[2]).to.have.property('response') expect(result[2].response).to.equal('ok') }) }) describe('getProofsAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { throw { message: 'Bad IP!!!!!', statusCode: 500 } }) }) it('should throw error with status code', async () => { let errResponse = null try { await cores.getProofsAsync('', []) } catch (err) { errResponse = err } expect(errResponse.message).to.equal('Invalid response on GET proof : 500 : Bad IP!!!!!') }) }) describe('getProofsAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { throw 'Error!' }) }) it('should throw error no status code', async () => { let errResponse = null try { await cores.getProofsAsync('', []) } catch (err) { errResponse = err } expect(errResponse.message).to.equal('Invalid response received on GET proof : Error!') }) }) describe('getProofsAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { return { body: 'ok' } }) }) it('should return success', async () => { let response = await cores.getProofsAsync('', []) expect(response).to.equal('ok') }) }) describe('getLatestCalBlockInfoAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { throw { message: 'Bad IP!!!!!', statusCode: 500 } }) }) it('should throw error with status code', async () => { let errResponse = null try { await cores.getLatestCalBlockInfoAsync() } catch (err) { errResponse = err } expect(errResponse.message).to.equal('Invalid response on GET status : 500') }) }) describe('getLatestCalBlockInfoAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { throw 'Error!' }) }) it('should throw error no status code', async () => { let errResponse = null try { await cores.getLatestCalBlockInfoAsync() } catch (err) { errResponse = err } expect(errResponse.message).to.equal('Invalid response received on GET status') }) }) describe('getLatestCalBlockInfoAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { return { body: { sync_info: { catching_up: false } } } }) }) it('should return success with one good IP', async () => { let response = await cores.getLatestCalBlockInfoAsync() expect(response).to.be.a('object') expect(response).to.have.property('catching_up') expect(response.catching_up).to.equal(false) }) }) describe('getLatestCalBlockInfoAsync', () => { let attempts = 0 before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.1.1.2'] }) cores.setRP(async () => { attempts++ if (attempts > 1) return { body: { sync_info: { catching_up: false } } } throw 'Error!' }) }) it('should return success with one bad and one good IP', async () => { let response = await cores.getLatestCalBlockInfoAsync() expect(response).to.be.a('object') expect(response).to.have.property('catching_up') expect(response.catching_up).to.equal(false) }) }) describe('getLatestCalBlockInfoAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setRP(async () => { return { body: { catching_up: true } } }) }) it('should throw error no status code when not synced', async () => { let errResponse = null try { await cores.getLatestCalBlockInfoAsync() } catch (err) { errResponse = err } expect(errResponse.message).to.equal('Invalid response received on GET status') }) }) describe('getLatestCalBlockInfoAsync', () => { let attempts = 0 before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1', '65.1.1.2'] }) cores.setRP(async () => { attempts++ if (attempts > 1) return { body: { sync_info: { catching_up: false } } } return { body: { sync_info: { catching_up: false } } } }) }) it('should return success with one unsynced and one good IP', async () => { let response = await cores.getLatestCalBlockInfoAsync() expect(response).to.be.a('object') expect(response).to.have.property('catching_up') expect(response.catching_up).to.equal(false) }) }) describe('getCachedTransactionAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setCoreTxCache({ a: { transaction: '1' } }) cores.setRP(async () => { throw 'Dont call!' }) }) it('should return value from cache', async () => { let response = await cores.getCachedTransactionAsync('a') expect(response).to.equal('1') }) }) describe('getCachedTransactionAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setCoreTxCache({}) cores.setRP(async () => { throw 'Bad IP!' }) }) it('should return null from bad IPs, no cache', async () => { let response = await cores.getCachedTransactionAsync('a') let cacheResult = cores.getCoreTxCache() expect(response).to.equal(null) expect(cacheResult).to.deep.equal({}) }) }) describe('getCachedTransactionAsync', () => { before(() => { cores.setENV({ CHAINPOINT_CORE_CONNECT_IP_LIST: ['65.1.1.1'] }) cores.setCoreTxCache({}) cores.setRP(async () => { return { body: 'result' } }) }) it('should return new tx and add to cache', async () => { let response = await cores.getCachedTransactionAsync('a') let cacheResult = cores.getCoreTxCache() expect(response).to.equal('result') expect(cacheResult).to.be.a('object') expect(cacheResult).to.have.property('a') expect(cacheResult.a).to.be.a('object') expect(cacheResult.a).to.have.property('transaction') expect(cacheResult.a.transaction).to.equal('result') expect(cacheResult.a).to.have.property('expiresAt') expect(cacheResult.a.expiresAt).to.be.a('number') }) }) }) ================================================ FILE: tests/hashes.js ================================================ /* global describe, it beforeEach, afterEach, before, after */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const request = require('supertest') const app = require('../lib/api-server.js') const hashes = require('../lib/endpoints/hashes.js') describe('Hashes Controller', () => { let insecureServer = null beforeEach(async () => { insecureServer = await app.startInsecureRestifyServerAsync() hashes.setRocksDB({ queueIncomingHashObjectsAsync: async () => {} }) hashes.setENV({ POST_HASHES_MAX: 1, AGGREGATION_INTERVAL_SECONDS: 60 }) }) afterEach(() => { insecureServer.close() }) describe('POST /hashes', () => { before(() => { app.setAcceptingHashes(false) }) after(() => { app.setAcceptingHashes(true) }) it('should return the proper error when not accepting hashes', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'text/plain') .expect('Content-type', /json/) .expect(503) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('ServiceUnavailable') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('Service is not currently accepting hashes') done() }) }) }) describe('POST /hashes', () => { it('should return the proper error with bad content type', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'text/plain') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid content type') done() }) }) it('should return the proper error with missing hashes property', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, missing hashes') done() }) }) it('should return the proper error with hashes not an array', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .send({ hashes: 'notarray' }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, hashes is not an Array') done() }) }) it('should return the proper error with empty hashes array', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .send({ hashes: [] }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid JSON body, hashes Array is empty') done() }) }) it('should return the proper error with max hashes exceeded', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .send({ hashes: ['a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1', 'b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1'] }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal(`invalid JSON body, hashes Array max size of 1 exceeded`) done() }) }) it('should return the proper error with invalid hashes', done => { request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .send({ hashes: ['invalid'] }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal(`invalid JSON body, invalid hashes present`) done() }) }) it('should return the proper result on success', done => { let hash = 'a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1' request(insecureServer) .post('/hashes') .set('Content-type', 'application/json') .send({ hashes: [hash] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body).to.have.property('meta') expect(res.body.meta) .to.have.property('submitted_at') .and.to.be.a('string') expect(res.body.meta).to.have.property('processing_hints') expect(res.body.meta.processing_hints) .to.have.property('cal') .and.to.be.a('string') expect(res.body.meta.processing_hints) .to.have.property('btc') .and.to.be.a('string') expect(res.body) .to.have.property('hashes') .and.to.be.a('array') expect(res.body.hashes).to.have.length(1) expect(Object.keys(res.body.hashes[0]).length).to.equal(2) expect(res.body.hashes[0]) .to.have.property('proof_id') .and.to.be.a('string') expect(res.body.hashes[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(hash) done() }) }) }) }) ================================================ FILE: tests/parse-env.js ================================================ /* global describe, it */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const parseEnv = require('../lib/parse-env.js') describe('Environment variables', () => { describe('valBase64', () => { it('should throw error with number', done => { expect(() => { parseEnv.valBase64(234) }).to.throw('Expected string but received a number') done() }) it('should throw error with boolean', done => { expect(() => { parseEnv.valBase64(true) }).to.throw('Expected string but received a boolean') done() }) it('should return error on empty string', done => { expect(() => { parseEnv.valBase64('') }).to.throw('The supplied value must be a valid Base 64 encoded string') done() }) it('should return error on non Base 64 encoded string', done => { expect(() => { parseEnv.valBase64('Not base 64 encoded') }).to.throw('The supplied value must be a valid Base 64 encoded string') done() }) it('should return value with proper base 64 encoded string', done => { let result = parseEnv.valBase64('cXdlCg==') expect(result).to.equal('cXdlCg==') done() }) }) describe('valSocket', () => { it('should throw error with number', done => { expect(() => { parseEnv.valSocket(234) }).to.throw('The supplied value must be a valid : string') done() }) it('should throw error with boolean', done => { expect(() => { parseEnv.valSocket(true) }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on empty string', done => { expect(() => { parseEnv.valSocket('') }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on single segment string', done => { expect(() => { parseEnv.valSocket('127.0.0.1') }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on 3+ segment string', done => { expect(() => { parseEnv.valSocket('127.0.0.1:2342:sdfs') }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on bad host', done => { expect(() => { parseEnv.valSocket('badhost:2342') }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on bad port string', done => { expect(() => { parseEnv.valSocket('goodhost.com:badport') }).to.throw('The supplied value must be a valid : string') done() }) it('should return error on invalid port number', done => { expect(() => { parseEnv.valSocket('goodhost.com:345345345345') }).to.throw('The supplied value must be a valid : string') done() }) it('should return value with host:port string', done => { let result = parseEnv.valSocket('goodhost.com:10009') expect(result).to.equal('goodhost.com:10009') done() }) }) describe('valCoreIPList', () => { it('should return success with empty string', done => { let result = parseEnv.valCoreIPList('') expect(result).to.equal('') done() }) it('should throw error with bad single IP', done => { expect(() => { parseEnv.valCoreIPList('234234.234234.234234.23434') }).to.throw('The Core IP list contains an invalid entry') done() }) it('should return true with valid v4 IP', done => { let result = parseEnv.valCoreIPList('65.1.1.1') expect(result).to.deep.equal(['65.1.1.1']) done() }) it('should return success with valid v6 IP', done => { let result = parseEnv.valCoreIPList('FE80:0000:0000:0000:0202:B3FF:FE1E:8329') expect(result).to.deep.equal(['FE80:0000:0000:0000:0202:B3FF:FE1E:8329']) done() }) it('should return success with valid collapsed v6 IP', done => { let result = parseEnv.valCoreIPList('FE80::0202:B3FF:FE1E:8329') expect(result).to.deep.equal(['FE80::0202:B3FF:FE1E:8329']) done() }) it('should return success with hybrid v6 IP', done => { let result = parseEnv.valCoreIPList('::ffff:65.1.1.1') expect(result).to.deep.equal(['::ffff:65.1.1.1']) done() }) it('should throw error with bad IP in group', done => { expect(() => { parseEnv.valCoreIPList('65.1.1.1,10.165.32.31,234234.234234.234234.23434') }).to.throw('The Core IP list contains an invalid entry') done() }) it('should throw error with missing IP in group', done => { expect(() => { parseEnv.valCoreIPList('65.1.1.1,,10.165.32.31') }).to.throw('The Core IP list contains an invalid entry') done() }) it('should throw error with duplicate IP in group', done => { expect(() => { parseEnv.valCoreIPList('65.1.1.1,65.1.1.1,10.165.32.31') }).to.throw('The Core IP list cannot contain duplicates') done() }) it('should return success with valid IP list', done => { let result = parseEnv.valCoreIPList('65.1.1.1,FE80::0202:B3FF:FE1E:8329,10.165.32.31') expect(result).to.deep.equal(['65.1.1.1', 'FE80::0202:B3FF:FE1E:8329', '10.165.32.31']) done() }) }) describe('valNetwork', () => { it('should throw error with number', done => { expect(() => { parseEnv.valNetwork(234) }).to.throw('The NETWORK value is invalid') done() }) it('should throw error with boolean', done => { expect(() => { parseEnv.valNetwork(true) }).to.throw('The NETWORK value is invalid') done() }) it('should return mainnet on empty', done => { let result = parseEnv.valNetwork('') expect(result).to.equal('mainnet') done() }) it('should return mainnet on mainnet', done => { let result = parseEnv.valNetwork('mainnet') expect(result).to.equal('mainnet') done() }) it('should return testnet on testnet', done => { let result = parseEnv.valNetwork('testnet') expect(result).to.equal('testnet') done() }) }) }) ================================================ FILE: tests/proofs.js ================================================ /* global describe, it beforeEach, afterEach */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const request = require('supertest') const fs = require('fs') const app = require('../lib/api-server.js') const proofs = require('../lib/endpoints/proofs.js') describe('Proofs Controller', () => { let insecureServer = null beforeEach(async () => { insecureServer = await app.startInsecureRestifyServerAsync() proofs.setRocksDB({ getProofStatesBatchByProofIdsAsync: async proofIds => { switch (proofIds[0]) { case 'bbb27662-2e21-11e9-b210-d663bd873d93': return [ { proofId: proofIds[0], hash: '18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784', proofState: [], submission: { submitId: 'e4c59c50-37cd-11e9-b270-d778f1c6df42', cores: [{ ip: '65.1.1.1', proofId: '000139a0-2e5c-11e9-bec9-01115ea738e6' }] } } ] default: return [ { proofId: proofIds[0], hash: null, proofState: null, submission: null } ] } } }) proofs.setCachedProofs({ getCachedCoreProofsAsync: async submissionData => { if (submissionData.length === 0) return [] switch (submissionData[0].submitId) { case 'e4c59c50-37cd-11e9-b270-d778f1c6df42': { let proofJSON = fs.readFileSync('./tests/sample-data/core-btc-proof.chp.json') return [ { submitId: submissionData[0].submitId, proof: JSON.parse(proofJSON), anchorsComplete: ['cal', 'btc'] } ] } default: return [] } } }) proofs.setENV({ GET_PROOFS_MAX: 1 }) }) afterEach(() => { insecureServer.close() }) describe('GET /proofs', () => { it('should return the proper error with bad hash_id in uri', done => { request(insecureServer) .get('/proofs/badproofid') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid request, bad hash_id') done() }) }) it('should return the proper error with no hash ids', done => { request(insecureServer) .get('/proofs') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid request, at least one hash id required') done() }) }) it('should return the proper error with too many hash_ids', done => { request(insecureServer) .get('/proofs') .set('proofids', 'a3127662-2e21-11e9-b210-d663bd873d93,a3127662-2e21-11e9-b210-d663bd873d99') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid request, too many hash ids (1 max)') done() }) }) it('should return the proper error with invalid hash_id in header', done => { request(insecureServer) .get('/proofs') .set('proofids', 'invalid') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('invalid request, bad hash_id') done() }) }) it('should return the proper empty result with unknown hash_id', done => { let proofId = 'a3127662-2e21-11e9-b210-d663bd873d93' request(insecureServer) .get('/proofs') .set('proofids', proofId) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body).to.be.a('array') expect(res.body).to.have.length(1) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(proofId) expect(res.body[0]) .to.have.property('proof') .and.to.equal(null) expect(res.body[0]) .to.have.property('anchors_complete') .and.to.be.a('array') expect(res.body[0].anchors_complete).to.have.length(0) done() }) }) it('should return successfully with a base64 proof with no Accept setting', done => { let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93' request(insecureServer) .get('/proofs') .set('proofids', proofId) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body).to.be.a('array') expect(res.body).to.have.length(1) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(proofId) expect(res.body[0]) .to.have.property('proof') .and.to.be.a('string') expect(res.body[0]) .to.have.property('anchors_complete') .and.to.be.a('array') expect(res.body[0].anchors_complete).to.have.length(2) expect(res.body[0].anchors_complete[0]) .to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors_complete[1]) .to.be.a('string') .and.to.equal('btc') done() }) }) it('should return successfully with a base64 proof with Accept Base64 setting', done => { let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93' request(insecureServer) .get('/proofs') .set('proofids', proofId) .set('Accept', 'application/vnd.chainpoint.json+base64') .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body).to.be.a('array') expect(res.body).to.have.length(1) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(proofId) expect(res.body[0]) .to.have.property('proof') .and.to.be.a('string') expect(res.body[0]) .to.have.property('anchors_complete') .and.to.be.a('array') expect(res.body[0].anchors_complete).to.have.length(2) expect(res.body[0].anchors_complete[0]) .to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors_complete[1]) .to.be.a('string') .and.to.equal('btc') done() }) }) it('should return successfully with a JSON proof with Accept JSON setting', done => { let proofId = 'bbb27662-2e21-11e9-b210-d663bd873d93' request(insecureServer) .get('/proofs') .set('proofids', proofId) .set('Accept', 'application/vnd.chainpoint.ld+json') .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body).to.be.a('array') expect(res.body).to.have.length(1) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(proofId) expect(res.body[0]).to.have.property('proof') expect(res.body[0].proof) .to.have.property('hash') .and.to.be.a('string') .and.to.equal('18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784') expect(res.body[0].proof) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(proofId) expect(res.body[0].proof) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal('000139a0-2e5c-11e9-bec9-01115ea738e6') expect(res.body[0]) .to.have.property('anchors_complete') .and.to.be.a('array') expect(res.body[0].anchors_complete).to.have.length(2) expect(res.body[0].anchors_complete[0]) .to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors_complete[1]) .to.be.a('string') .and.to.equal('btc') done() }) }) }) }) ================================================ FILE: tests/sample-data/btc-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "cal", "anchor_id": "9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d", "uris": [ "https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data" ] } ] } ], "branches": [ { "label": "btc_anchor_branch", "ops": [ { "l": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "r": "9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d" }, { "op": "sha-256" }, { "l": "28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb" }, { "op": "sha-256" }, { "r": "4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974" }, { "op": "sha-256" }, { "r": "f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0" }, { "op": "sha-256" }, { "r": "ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d" }, { "op": "sha-256" }, { "l": "fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab" }, { "op": "sha-256" }, { "l": "01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20" }, { "r": "ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000" }, { "op": "sha-256-x2" }, { "l": "aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985" }, { "op": "sha-256-x2" }, { "l": "f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500" }, { "op": "sha-256-x2" }, { "l": "a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a" }, { "op": "sha-256-x2" }, { "l": "12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb" }, { "op": "sha-256-x2" }, { "l": "0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e" }, { "op": "sha-256-x2" }, { "l": "52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50" }, { "op": "sha-256-x2" }, { "l": "bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1" }, { "op": "sha-256-x2" }, { "l": "25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d" }, { "op": "sha-256-x2" }, { "l": "a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6" }, { "op": "sha-256-x2" }, { "r": "3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1" }, { "op": "sha-256-x2" }, { "r": "af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d" }, { "op": "sha-256-x2" }, { "anchors": [ { "type": "btc", "anchor_id": "549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e", "uris": [ "https://a.chainpoint.org/calendar/549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e/data" ] } ] } ] } ] } ] } ================================================ FILE: tests/sample-data/cal-proof-l.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "cal", "anchor_id": "985635", "uris": ["https://a.chainpoint.org/calendar/985635/hash"] } ] } ] } ] } ================================================ FILE: tests/sample-data/cal-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "cal", "anchor_id": "9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d", "uris": [ "https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data" ] } ] } ] } ] } ================================================ FILE: tests/sample-data/core-btc-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784", "proof_id": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_node_at": "2019-02-12T00:20:28Z", "hash_id_core": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_core_at": "2019-02-12T00:20:28Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "core_id:000139a0-2e5c-11e9-bec9-01115ea738e6" }, { "op": "sha-256" }, { "l": "nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e" }, { "op": "sha-256" }, { "l": "87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf" }, { "op": "sha-256" }, { "r": "a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2" }, { "op": "sha-256" }, { "r": "3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2" }, { "op": "sha-256" }, { "l": "92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410" }, { "op": "sha-256" }, { "l": "2697833:1549930833:1:https://a.chainpoint.org:cal:2697833" }, { "r": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "anchors": [ { "type": "cal", "anchor_id": "2697833", "uris": ["https://a.chainpoint.org/calendar/2697833/hash"] } ] } ], "branches": [ { "label": "btc_anchor_branch", "ops": [ { "l": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "r": "4b414a5f4e67f4255027e37e9889b6a1f7f94b9285dfe6c45dc9cc70ea83238b" }, { "op": "sha-256" }, { "l": "19e96821c648366829f9b4656f439497bf8109a59bd211f7dea5f7bc38faca52" }, { "op": "sha-256" }, { "l": "3a22c564ca8be07c0d51a67874e5ef104ca37dc255b9c5e8043beecd2137343a" }, { "op": "sha-256" }, { "l": "56bb7aa1d380608cfc7328310020d79d90dadf8c8b93a4dc0c887ef080b30208" }, { "op": "sha-256" }, { "l": "2d52f99e44e3a6aed97e7a4c2b2580094f3df598e71ad60ce3e4c80b83289fc0" }, { "op": "sha-256" }, { "l": "2e1e454f149402f668e940b095e01303f2976b0c851de896c54d77d1b0ab7e76" }, { "op": "sha-256" }, { "r": "62726a2392223ad6793414fca1c87e2932421315690f4ebb9094243b4d75fc65" }, { "op": "sha-256" }, { "r": "0659c270d03e0ed74b183966f7a8ce35b31b51b861cb44a2d8a9e4c2144d7b11" }, { "op": "sha-256" }, { "l": "0100000001f74c607aec4e8552e97f5c6705331bd40971fee4615463e44dc237909eda8561010000006b4830450221009dea9d7cfef167627ae1d9fe5fae19ea23c4d262d35c951d15f282089a25f71502204e8ab4c0778e206e06a9512c2fbfa5092d5dbfe79394b6bd7523e6b17482b6b10121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20" }, { "r": "cb1ee800000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000" }, { "op": "sha-256-x2" }, { "r": "6530dedcdef49f2ba8ffad45cc2a3da9163c545cc25293fbdd38243cff32c856" }, { "op": "sha-256-x2" }, { "r": "3eb37791f8e63616aacc0c0a305bcb527696e7bac90f2f7e503b506a4c43faa6" }, { "op": "sha-256-x2" }, { "l": "5c32a9aa6c2d51cdf40a6abb84e310a915710a2eb68505f01e50f5f1a7f25d1b" }, { "op": "sha-256-x2" }, { "r": "52d7ead6f09051315f6508b5691fa6213643510ced81066e3ca6717151ebc6d4" }, { "op": "sha-256-x2" }, { "l": "2007ea7ac74528f25264e50f5ba848675c7a2f28e7dd196e33874a50c05107b5" }, { "op": "sha-256-x2" }, { "l": "755908c4d69007fc1f0df3e6211d1041850a81afe32b262f9a9a7ed9c2abf0a4" }, { "op": "sha-256-x2" }, { "r": "e75bb3c9dccd54b626579f08d1e517b458716cc308a6257a7fbe2697c749bc96" }, { "op": "sha-256-x2" }, { "l": "98e2e5e17003189b4ef1a1fdf8de94af1a0bbb7b9e9d6310ad953719385f4a1e" }, { "op": "sha-256-x2" }, { "l": "a7af3e0d38da4c2b3bc958c63176d349a53379f1467c4be65a86ef5e3f91747b" }, { "op": "sha-256-x2" }, { "r": "a2a69c02444bc5f396d87537be7e15ee29dada648c95f9ce731a9d0db285dc91" }, { "op": "sha-256-x2" }, { "r": "1b3c4074444b6926f5ac2fc5c9d4a7cb91bdb6dcd90356abf2c3fda5d20b655a" }, { "op": "sha-256-x2" }, { "l": "25bfc69c9c2a6026eaef5b528c9b354dfaf6f313f614dd41f88d8d4f0c046aa8" }, { "op": "sha-256-x2" }, { "anchors": [ { "type": "btc", "anchor_id": "562658", "uris": ["https://a.chainpoint.org/calendar/2698364/data"] } ] } ] } ] } ] } ================================================ FILE: tests/sample-data/core-cal-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784", "proof_id": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_node_at": "2019-02-12T00:20:28Z", "hash_id_core": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_core_at": "2019-02-12T00:20:28Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "core_id:000139a0-2e5c-11e9-bec9-01115ea738e6" }, { "op": "sha-256" }, { "l": "nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e" }, { "op": "sha-256" }, { "l": "87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf" }, { "op": "sha-256" }, { "r": "a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2" }, { "op": "sha-256" }, { "r": "3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2" }, { "op": "sha-256" }, { "l": "92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410" }, { "op": "sha-256" }, { "l": "2697833:1549930833:1:https://a.chainpoint.org:cal:2697833" }, { "r": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "anchors": [ { "type": "cal", "anchor_id": "2697833", "uris": ["https://a.chainpoint.org/calendar/2697833/hash"] } ] } ] } ] } ================================================ FILE: tests/sample-data/core-tbtc-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784", "proof_id": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_node_at": "2019-02-12T00:20:28Z", "hash_id_core": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_core_at": "2019-02-12T00:20:28Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "core_id:000139a0-2e5c-11e9-bec9-01115ea738e6" }, { "op": "sha-256" }, { "l": "nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e" }, { "op": "sha-256" }, { "l": "87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf" }, { "op": "sha-256" }, { "r": "a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2" }, { "op": "sha-256" }, { "r": "3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2" }, { "op": "sha-256" }, { "l": "92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410" }, { "op": "sha-256" }, { "l": "2697833:1549930833:1:https://a.chainpoint.org:cal:2697833" }, { "r": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "anchors": [ { "type": "tcal", "anchor_id": "2697833", "uris": ["https://a.chainpoint.org/calendar/2697833/hash"] } ] } ], "branches": [ { "label": "btc_anchor_branch", "ops": [ { "l": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "r": "4b414a5f4e67f4255027e37e9889b6a1f7f94b9285dfe6c45dc9cc70ea83238b" }, { "op": "sha-256" }, { "l": "19e96821c648366829f9b4656f439497bf8109a59bd211f7dea5f7bc38faca52" }, { "op": "sha-256" }, { "l": "3a22c564ca8be07c0d51a67874e5ef104ca37dc255b9c5e8043beecd2137343a" }, { "op": "sha-256" }, { "l": "56bb7aa1d380608cfc7328310020d79d90dadf8c8b93a4dc0c887ef080b30208" }, { "op": "sha-256" }, { "l": "2d52f99e44e3a6aed97e7a4c2b2580094f3df598e71ad60ce3e4c80b83289fc0" }, { "op": "sha-256" }, { "l": "2e1e454f149402f668e940b095e01303f2976b0c851de896c54d77d1b0ab7e76" }, { "op": "sha-256" }, { "r": "62726a2392223ad6793414fca1c87e2932421315690f4ebb9094243b4d75fc65" }, { "op": "sha-256" }, { "r": "0659c270d03e0ed74b183966f7a8ce35b31b51b861cb44a2d8a9e4c2144d7b11" }, { "op": "sha-256" }, { "l": "0100000001f74c607aec4e8552e97f5c6705331bd40971fee4615463e44dc237909eda8561010000006b4830450221009dea9d7cfef167627ae1d9fe5fae19ea23c4d262d35c951d15f282089a25f71502204e8ab4c0778e206e06a9512c2fbfa5092d5dbfe79394b6bd7523e6b17482b6b10121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20" }, { "r": "cb1ee800000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000" }, { "op": "sha-256-x2" }, { "r": "6530dedcdef49f2ba8ffad45cc2a3da9163c545cc25293fbdd38243cff32c856" }, { "op": "sha-256-x2" }, { "r": "3eb37791f8e63616aacc0c0a305bcb527696e7bac90f2f7e503b506a4c43faa6" }, { "op": "sha-256-x2" }, { "l": "5c32a9aa6c2d51cdf40a6abb84e310a915710a2eb68505f01e50f5f1a7f25d1b" }, { "op": "sha-256-x2" }, { "r": "52d7ead6f09051315f6508b5691fa6213643510ced81066e3ca6717151ebc6d4" }, { "op": "sha-256-x2" }, { "l": "2007ea7ac74528f25264e50f5ba848675c7a2f28e7dd196e33874a50c05107b5" }, { "op": "sha-256-x2" }, { "l": "755908c4d69007fc1f0df3e6211d1041850a81afe32b262f9a9a7ed9c2abf0a4" }, { "op": "sha-256-x2" }, { "r": "e75bb3c9dccd54b626579f08d1e517b458716cc308a6257a7fbe2697c749bc96" }, { "op": "sha-256-x2" }, { "l": "98e2e5e17003189b4ef1a1fdf8de94af1a0bbb7b9e9d6310ad953719385f4a1e" }, { "op": "sha-256-x2" }, { "l": "a7af3e0d38da4c2b3bc958c63176d349a53379f1467c4be65a86ef5e3f91747b" }, { "op": "sha-256-x2" }, { "r": "a2a69c02444bc5f396d87537be7e15ee29dada648c95f9ce731a9d0db285dc91" }, { "op": "sha-256-x2" }, { "r": "1b3c4074444b6926f5ac2fc5c9d4a7cb91bdb6dcd90356abf2c3fda5d20b655a" }, { "op": "sha-256-x2" }, { "l": "25bfc69c9c2a6026eaef5b528c9b354dfaf6f313f614dd41f88d8d4f0c046aa8" }, { "op": "sha-256-x2" }, { "anchors": [ { "type": "tbtc", "anchor_id": "562658", "uris": ["https://a.chainpoint.org/calendar/2698364/data"] } ] } ] } ] } ] } ================================================ FILE: tests/sample-data/core-tcal-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "18af1184ae64160f8a4019f43ddc825db95f11a0e468f8da6cb9f8bbe1dbd784", "proof_id": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_node_at": "2019-02-12T00:20:28Z", "hash_id_core": "000139a0-2e5c-11e9-bec9-01115ea738e6", "hash_submitted_core_at": "2019-02-12T00:20:28Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "core_id:000139a0-2e5c-11e9-bec9-01115ea738e6" }, { "op": "sha-256" }, { "l": "nistv2:1549930800000:bfecaeee123fb3eabf58791ad4bf142c919724a253658f4a2b44fc997700e6dbb0c43691810cfd3749cb31aa0a900f9f5c8a413175c41106de9504aaf7cfce3e" }, { "op": "sha-256" }, { "l": "87ed4683760418a70f1054db3f6c74262ab261b5d29c5dc96844d2970a3d01cf" }, { "op": "sha-256" }, { "r": "a78588d08be7e46c61b7dd3b5e60fdb7220bd27ca3ab9e43964774d46c2e64a2" }, { "op": "sha-256" }, { "r": "3cef66e643b856b74f5bd7cff2547c275eb167b386c160721ab2ecc225dea3a2" }, { "op": "sha-256" }, { "l": "92ad35e664813183e02bf2426482d039837eb2a97a71311d2b464a7d1f6f4410" }, { "op": "sha-256" }, { "l": "2697833:1549930833:1:https://a.chainpoint.org:cal:2697833" }, { "r": "456fd2c9ab38caf43642cddfb2f01651204a358e11da67a1cc286e8b7a6c6acf" }, { "op": "sha-256" }, { "anchors": [ { "type": "tcal", "anchor_id": "2697833", "uris": ["https://a.chainpoint.org/calendar/2697833/hash"] } ] } ] } ] } ================================================ FILE: tests/sample-data/lsat-data.json ================================================ { "challenge1000": "LSAT macaroon=\"MDAxY2xvY2F0aW9uIDEyNy4wLjAuMTo4MDgwCjAwOTRpZGVudGlmaWVyIDAwMDA3Y2VmOTNmMmM1MWFhNjUyMDhiZWMxNDQ3ZmMzOGZjNThkOWJjZTEzNzVhNTMyZWRiMGRjZDI5MGEyYzMzMGFlMmNhOTMxYTFjMzZiNDhmNTQ5NDhiODk4YTI3MWE1M2VkOTFmZjdkMDA4MTkzOWE1ZmE1MTEyNDllODFjYmE1YwowMDJmc2lnbmF0dXJlIFAvS7iENFK0Z7Hc0GBM3wLOu0zB5Ino6DoXosjg4cpcCg\", invoice=\"lntb10u1pw7kfm8pp50nhe8uk9r2n9yz97c9z8lsu0ckxehnsnwkjn9mdsmnffpgkrxzhqdq5w3jhxapqd9h8vmmfvdjscqzpgllq2qvdlgkllc27kpd87lz8pdfsfmtteyc3kwq734jpwnvqt96e4nuy0yauzdrtkumxsvawgda8dlljxu3nnjlhs6w75390wy7ukj6cpfmygah\"", "challenge10": "LSAT macaroon=\"MDAzMmxvY2F0aW9uIGh0dHBzOi8vbHNhdC1wbGF5Z3JvdW5kLmJ1Y2tvLm5vdy5zaAowMDk0aWRlbnRpZmllciAwMDAwYjE3NTUyMWQ5MWRhYTFjNmMzYTQ1ODdhYmQ2ZDc1MTk0ZDlmYjcxY2ExMDA2ZTM4ZjRhNjZiZjhlZGFmOTY3Y2RmMDI1ZDg5NjllMTFhZGZhMTlmODBlY2E3MjZhNDlmMDk4ZjBkNGIxZTliMjQ0NWExMDM1ODlhMDU2OGJkNWMKMDAyZnNpZ25hdHVyZSCbqEd6onEZANYxldTicJGq5k5esN0S3bR8ijHoS5UoZAo\", invoice=\"lntb100n1p0zy4xqpp5k964y8v3m2sudsaytpat6mt4r9xeldcu5yqxuw855e4l3md0je7qdquw3jhxapqwa5hg6pqxyczqumpw3escqzpgxqyz5vq0gl98qcw2t8vsqtvwhz3vjn6hmadq97g4hwllwd5rshraxpeeppnkldcg7y8et5qk7yn647lhflyt8j8wvln9xl9d2chdhahc240n8gqs83x93\"", "challenge5": "LSAT macaroon=\"MDAzMmxvY2F0aW9uIGh0dHBzOi8vbHNhdC1wbGF5Z3JvdW5kLmJ1Y2tvLm5vdy5zaAowMDk0aWRlbnRpZmllciAwMDAwYzliNjFkOTBjZGNlM2QwN2UzNGY1OTI4MmQzMjk3NjI3NWQ5YWUxYTcwMzcyMDE4MDcxYjM2MGMwZGI3MDA2MTQ0OWUwMDNiOGZjNGNmOWNhNzg1NzhmMzZiZTgyN2RmNzgyZTJiMDk2ODk5MGM0MWUzMDkzOTM3N2ExYTA0MTYKMDAyZnNpZ25hdHVyZSALMsWsLgJvJFIs6ewLvLcYTp6sRq_sYg9-mlBWDQr6Kgo\", invoice=\"lntb50n1p0zy4gjpp5exmpmyxdec7s0c60ty5z6v5hvf6ants6wqmjqxq8rvmqcrdhqpssdq6w3jhxapqwa5hg6pqx5s8xct5wvcqzpgxqyz5vq54j6jvy6qwmynt0n5h8ewpwwj4trg2pz0ammz20ww6fyqathmrdk94e2hly296n7ag3cstcagjnjhxu7esml32fkteh3lmaz973p9ngqz8ak65\"" } ================================================ FILE: tests/sample-data/tbtc-proof-l.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "tcal", "anchor_id": "985635", "uris": ["https://a.chainpoint.org/calendar/985635/hash"] } ] } ], "branches": [ { "label": "btc_anchor_branch", "ops": [ { "l": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "r": "9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d" }, { "op": "sha-256" }, { "l": "28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb" }, { "op": "sha-256" }, { "r": "4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974" }, { "op": "sha-256" }, { "r": "f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0" }, { "op": "sha-256" }, { "r": "ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d" }, { "op": "sha-256" }, { "l": "fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab" }, { "op": "sha-256" }, { "l": "01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20" }, { "r": "ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000" }, { "op": "sha-256-x2" }, { "l": "aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985" }, { "op": "sha-256-x2" }, { "l": "f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500" }, { "op": "sha-256-x2" }, { "l": "a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a" }, { "op": "sha-256-x2" }, { "l": "12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb" }, { "op": "sha-256-x2" }, { "l": "0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e" }, { "op": "sha-256-x2" }, { "l": "52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50" }, { "op": "sha-256-x2" }, { "l": "bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1" }, { "op": "sha-256-x2" }, { "l": "25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d" }, { "op": "sha-256-x2" }, { "l": "a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6" }, { "op": "sha-256-x2" }, { "r": "3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1" }, { "op": "sha-256-x2" }, { "r": "af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d" }, { "op": "sha-256-x2" }, { "anchors": [ { "type": "tbtc", "anchor_id": "503275", "uris": ["https://a.chainpoint.org/calendar/985814/data"] } ] } ] } ] } ] } ================================================ FILE: tests/sample-data/tbtc-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "tcal", "anchor_id": "9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d", "uris": [ "https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data" ] } ] } ], "branches": [ { "label": "btc_anchor_branch", "ops": [ { "l": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "r": "9d7e8027c869d7446db8f2a5f371d967f5ba9d3a88f1703a1674f57963d3448d" }, { "op": "sha-256" }, { "l": "28c6aa4416d1b0aa474bc52fd32175ec7d15980772874617b5000aff043ac6cb" }, { "op": "sha-256" }, { "r": "4c297218f2015d4f84a6561ca06c1c28b2f6cca1500315ef6d4944ad6822b974" }, { "op": "sha-256" }, { "r": "f6a15401357e6e177583dbf5aa82b5ed5ae1043d1bda3faba88ca0fdb90e01c0" }, { "op": "sha-256" }, { "r": "ae9137386a03fdcdb9a1554a6e4fcd9697efed17caaa0221ce35e12bfc9fbf2d" }, { "op": "sha-256" }, { "l": "fa5643778470a9175644affe35e0177a13b2446d73182be0963d53b1d09214ab" }, { "op": "sha-256" }, { "l": "01000000013d9bfb8c553b3a7c9c030ea9b0f47c7e4c457e47a1ad2d9c751c8eb0e02fee70010000006a47304402201eac07288c3881f354564bb9da0d8267174cdc9e8c42ca82c2129a0416c806220220104e9932a89259472c84be7722f77324efa43a65ca79dd5bb8b6aab0ac9788000121032695ca0d3c0f7f8082a6ef66e7127e48d4eb99bef86be99432b897c485962fa8ffffffff020000000000000000226a20" }, { "r": "ca694202000000001976a9149f1f4038857beedd34cc5ba9f26ac7a20c04d51988ac00000000" }, { "op": "sha-256-x2" }, { "l": "aa7008cdf722a674cc3532727ee39e9ebc810fb047cc7f4edc302705fcee3985" }, { "op": "sha-256-x2" }, { "l": "f0fae6f1dc00b678596e230584430b95bad9c1439f03293250b5a9bfb993b500" }, { "op": "sha-256-x2" }, { "l": "a79b18abcde7db6554e95c14ed544231f59670318033fc6e2e28142341ef223a" }, { "op": "sha-256-x2" }, { "l": "12105db21e488b1d8eb44fbce8bc5e3fcb7becc35fe4d9d30696ef7baff853eb" }, { "op": "sha-256-x2" }, { "l": "0ce1848d74ea8705858e468e045e7891f2b5f9c8ed37eeaa00be51846460294e" }, { "op": "sha-256-x2" }, { "l": "52af6b21e7b370f680e984b8a1e34ffdb45770d3cf599357ce245bad8c820d50" }, { "op": "sha-256-x2" }, { "l": "bb5bd9669a3bc3202e460091185f8103863da4263f417e85479fc3bb40a882d1" }, { "op": "sha-256-x2" }, { "l": "25bb84e8a36904224182b28adb04956d1251d4312b4e975c4ee3ff74a50bce1d" }, { "op": "sha-256-x2" }, { "l": "a55b52dc8079febc3a8b673ee123829c176aca7dabb330299afdeac2bfea16d6" }, { "op": "sha-256-x2" }, { "r": "3bf18e7d4ffaab9988d14b1402fe9817ea6c50fa626dd78bcaba18a9b16184f1" }, { "op": "sha-256-x2" }, { "r": "af9ae1010333cf6e5ea124e5827a8bf0f40f68ab9a5bf283f93f744046b07a5d" }, { "op": "sha-256-x2" }, { "anchors": [ { "type": "tbtc", "anchor_id": "549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e", "uris": [ "https://a.chainpoint.org/calendar/549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e/data" ] } ] } ] } ] } ] } ================================================ FILE: tests/sample-data/tcal-proof.chp.json ================================================ { "@context": "https://w3id.org/chainpoint/v3", "type": "Chainpoint", "hash": "ffff27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391", "proof_id": "66a34bd0-f4e7-11e7-a52b-016a36a9d789", "hash_submitted_node_at": "2018-01-09T02:47:15Z", "hash_id_core": "66bd6380-f4e7-11e7-895d-0176dc2220aa", "hash_submitted_core_at": "2018-01-09T02:47:15Z", "branches": [ { "label": "cal_anchor_branch", "ops": [ { "l": "node_id:66a34bd0-f4e7-11e7-a52b-016a36a9d789" }, { "op": "sha-256" }, { "l": "core_id:66bd6380-f4e7-11e7-895d-0176dc2220aa" }, { "op": "sha-256" }, { "l": "nist:1515465960:1041862e0f3987dca3aab3a91767d2a2ebbf251451b740879adb0926f0ee325e608d5c311e3f64a002dc5266337efc34ebdbf0032c7a253a8fbb64c1b0fb625f" }, { "op": "sha-256" }, { "r": "725a969557e64600aa2bbe50e75fc12dd913620144660836441a97f6d36babf9" }, { "op": "sha-256" }, { "l": "f21aac3945aee46d0cd888faff3364cc7640f88c9bdfefb1072a4bb82c6702b6" }, { "op": "sha-256" }, { "r": "c59058f17b93b609f4b49366c8808099a715836b6c08b45a1dc6ac762820ae27" }, { "op": "sha-256" }, { "l": "985635:1515466042:1:https://a.chainpoint.org:cal:985635" }, { "r": "0e20cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837" }, { "op": "sha-256" }, { "anchors": [ { "type": "tcal", "anchor_id": "9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d", "uris": [ "https://a.chainpoint.org/calendar/9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d/data" ] } ] } ] } ] } ================================================ FILE: tests/utils.js ================================================ /* global describe, it */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const fs = require('fs') const app = require('../lib/utils.js') describe('Utils Methods', () => { describe('Sleep function', () => { it('should sleep for 100ms', done => { let amount = 100 let startMS = Date.now() app.sleepAsync(amount).then(() => { let elapsedMS = Date.now() - startMS expect(elapsedMS).to.be.greaterThan(amount - 1) expect(elapsedMS).to.be.lessThan(amount + 25) done() }) }) it('should sleep for 1000ms', done => { let amount = 1000 let startMS = Date.now() app.sleepAsync(amount).then(() => { let elapsedMS = Date.now() - startMS expect(elapsedMS).to.be.greaterThan(amount - 1) expect(elapsedMS).to.be.lessThan(amount + 25) done() }) }) }) describe('Date functions', () => { it('addSeconds should return correct result', done => { let addend = 55 let startDate = new Date(2019, 0, 1, 0, 0, 0, 0) let expectedDate = new Date(2019, 0, 1, 0, 0, addend, 0) let calculatedDate = app.addSeconds(startDate, addend) expect(calculatedDate.getTime()).to.equal(expectedDate.getTime()) done() }) it('addMinutes should return correct result', done => { let addend = 55 let startDate = new Date(2019, 0, 1, 0, 0, 0, 0) let expectedDate = new Date(2019, 0, 1, 0, addend, 0, 0) let calculatedDate = app.addMinutes(startDate, addend) expect(calculatedDate.getTime()).to.equal(expectedDate.getTime()) done() }) it('formatDateISO8601NoMs should return correct result', done => { let startDate = new Date('2019-02-06T18:14:35.576Z') let expectedDate = '2019-02-06T18:14:35Z' let calculatedDate = app.formatDateISO8601NoMs(startDate) expect(calculatedDate.toString()).to.equal(expectedDate) done() }) }) describe('Hash format function', () => { it('lowerCaseHashes should return correct result', done => { let startHashes = ['A1b2, ABCDef010101Cd'] let expectedHashes = ['a1b2, abcdef010101cd'] let calculatedHashes = app.lowerCaseHashes(startHashes) expect(calculatedHashes.length).to.equal(expectedHashes.length) expect(calculatedHashes[0]).to.equal(expectedHashes[0]) expect(calculatedHashes[1]).to.equal(expectedHashes[1]) done() }) }) describe('Proof parsing function - mainnet', () => { it('parseAnchorsComplete should return correct result for cal proof', done => { let proofJSON = fs.readFileSync('./tests/sample-data/cal-proof.chp.json') let proofObj = JSON.parse(proofJSON) let res = app.parseAnchorsComplete(proofObj, 'mainnet') expect(res.length).to.equal(1) expect(res[0]).to.equal('cal') done() }) it('parseAnchorsComplete should return correct result for btc proof', done => { let proofJSON = fs.readFileSync('./tests/sample-data/btc-proof.chp.json') let proofObj = JSON.parse(proofJSON) let res = app.parseAnchorsComplete(proofObj, 'mainnet') expect(res.length).to.equal(2) expect(res[0]).to.equal('cal') expect(res[1]).to.equal('btc') done() }) }) describe('Proof parsing function - testnet', () => { it('parseAnchorsComplete should return correct result for tcal proof', done => { let proofJSON = fs.readFileSync('./tests/sample-data/tcal-proof.chp.json') let proofObj = JSON.parse(proofJSON) let res = app.parseAnchorsComplete(proofObj, 'testnet') expect(res.length).to.equal(1) expect(res[0]).to.equal('tcal') done() }) it('parseAnchorsComplete should return correct result for tbtc proof', done => { let proofJSON = fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json') let proofObj = JSON.parse(proofJSON) let res = app.parseAnchorsComplete(proofObj, 'testnet') expect(res.length).to.equal(2) expect(res[0]).to.equal('tcal') expect(res[1]).to.equal('tbtc') done() }) }) describe('Hex validation function', () => { it('isHex should return false for non hex value', done => { let val = 'nonhex' let res = app.isHex(val) expect(res).to.equal(false) done() }) it('isHex should return false for non hex value', done => { let val = 'deadbeefcafe' let res = app.isHex(val) expect(res).to.equal(true) done() }) }) describe('Random number function', () => { it('randomIntFromInterval should produce random numbers within the specified range', done => { let iterations = 10000 for (let i = 0; i < iterations; i++) { let min = Math.floor(Math.random() * 100) let max = min * Math.ceil(Math.random() * 99) let rnd = app.randomIntFromInterval(min, max) expect(rnd).to.be.gte(min) expect(rnd).to.be.lte(max) } // and test if bounds are inclusive let rnd = app.randomIntFromInterval(10, 10) expect(rnd).to.be.gte(10) expect(rnd).to.be.lte(10) done() }) }) describe('UI password check function', () => { it('should return false when value is false', done => { let val = false let res = app.nodeUIPasswordBooleanCheck(val) expect(res).to.equal(false) done() }) it("should return false when value is 'false'", done => { let val = 'false' let res = app.nodeUIPasswordBooleanCheck(val) expect(res).to.equal(false) done() }) it("should return false when value is 'FALSE'", done => { let val = 'FALSE' let res = app.nodeUIPasswordBooleanCheck(val) expect(res).to.equal(false) done() }) it("should return false when value is 'False'", done => { let val = 'False' let res = app.nodeUIPasswordBooleanCheck(val) expect(res).to.equal(false) done() }) it('should return password if not any variation of false', done => { let val = 'not false' let res = app.nodeUIPasswordBooleanCheck(val) expect(res).to.equal(val) done() }) }) }) ================================================ FILE: tests/verify.js ================================================ /* global describe, it beforeEach, afterEach */ process.env.NODE_ENV = 'test' // test related packages const expect = require('chai').expect const request = require('supertest') const fs = require('fs') const app = require('../lib/api-server.js') const verify = require('../lib/endpoints/verify.js') const cpb = require('chainpoint-binary') describe('Verify Controller', () => { let insecureServer = null beforeEach(async () => { insecureServer = await app.startInsecureRestifyServerAsync() beforeEach(() => { verify.setENV({ POST_VERIFY_PROOFS_MAX: 1 }) }) verify.setCores({ getCachedTransactionAsync: async txId => { switch (txId) { case '9f656ff0aa53b2cf7c85b8dbe3127ef9e141fdd25c70d2dc01768b3cf798261d': { return { tx: { data: '4690932f928fb7f7ce6e6c49ee95851742231709360be28b7ce2af7b92cfa95b' } } } case '549ea0ff2382858b9b29e3f3615afe2a537a4dbf76c1e58f73fe0e2b0220365e': { return { tx: { data: 'c617f5faca34474bea7020d75c39cb8427a32145f9646586ecb9184002131ad9' } } } default: { return { tx: { data: '' } } } } } }) }) afterEach(() => { insecureServer.close() }) describe('POST /verify mainnet', () => { beforeEach(() => { verify.setENV({ POST_VERIFY_PROOFS_MAX: 1, NETWORK: 'mainnet' }) }) it('should return the proper error with bad content type', done => { request(insecureServer) .post('/verify') .set('Content-type', 'text/plain') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('Invalid content type') done() }) }) it('should return the proper error with missing proofs property', done => { request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('Invalid JSON body, missing proofs') done() }) }) it('should return the proper error with proofs not an array', done => { request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: 'notarray' }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('Invalid JSON body, proofs is not an Array') done() }) }) it('should return the proper error with empty proofs array', done => { request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [] }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal('Invalid JSON body, proofs Array is empty') done() }) }) it('should return the proper error with max proofs exceeded', done => { request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: ['p1', 'p2'] }) .expect('Content-type', /json/) .expect(409) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.have.property('code') .and.to.be.a('string') .and.to.equal('InvalidArgument') expect(res.body) .to.have.property('message') .and.to.be.a('string') .and.to.equal(`Invalid JSON body, proofs Array max size of 1 exceeded`) done() }) }) it('should return successful result with malformed proof', done => { request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: ['p1'] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('malformed') done() }) }) it('should return successful result with bad network proof', done => { let tcalProof = JSON.parse(fs.readFileSync('./tests/sample-data/tcal-proof.chp.json')) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [tcalProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal( `This is a 'mainnet' Node supporting 'cal' and 'btc' anchor types. Cannot verify 'tcal' anchors.` ) done() }) }) it('should return successful result with invalid cal proof (json) and legacy anchor', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof-l.chp.json')) calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal(`Cannot verify legacy anchors.`) done() }) }) it('should return successful result with invalid cal proof (json)', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json')) calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(calProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(calProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(calProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(1) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[0]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('invalid') done() }) }) it('should return successful result with invalid btc proof (json)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) btcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('invalid') done() }) }) it('should return successful result with valid cal proof (json)', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json')) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(calProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(calProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(calProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(1) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('verified') done() }) }) it('should return successful result with valid btc proof (json)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('verified') done() }) }) it('should return successful result with mixed (cal ok, btc bad) btc proof (json)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) btcProof.branches[0].branches[0].ops[0].l = 'bad0cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('mixed') done() }) }) it('should return successful result with invalid cal proof (b64)', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json')) calProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' let calProofB64 = cpb.objectToBase64Sync(calProof) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProofB64] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(calProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(calProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(calProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(1) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[0]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('invalid') done() }) }) it('should return successful result with invalid btc proof (b64)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) btcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' let btcProofB64 = cpb.objectToBase64Sync(btcProof) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProofB64] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('invalid') done() }) }) it('should return successful result with valid cal proof (b64)', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json')) let calProofB64 = cpb.objectToBase64Sync(calProof) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProofB64] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(calProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(calProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(calProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(calProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(1) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('verified') done() }) }) it('should return successful result with valid btc proof (b64)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) let btcProofB64 = cpb.objectToBase64Sync(btcProof) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProofB64] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('verified') done() }) }) it('should return successful result with mixed (cal ok, btc bad) btc proof (b64)', done => { let btcProof = JSON.parse(fs.readFileSync('./tests/sample-data/btc-proof.chp.json')) btcProof.branches[0].branches[0].ops[0].l = 'bad0cff025777bec277cd3a0599eaf5efbeb1ea7adf5ec5a39126a77fa57f837' let btcProofB64 = cpb.objectToBase64Sync(btcProof) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [btcProofB64] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(btcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(btcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(btcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(btcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('cal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('btc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('mixed') done() }) }) }) describe('POST /verify testnet', () => { beforeEach(() => { verify.setENV({ POST_VERIFY_PROOFS_MAX: 1, NETWORK: 'testnet' }) }) it('should return successful result with bad network proof', done => { let calProof = JSON.parse(fs.readFileSync('./tests/sample-data/cal-proof.chp.json')) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [calProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal( `This is a 'testnet' Node supporting 'tcal' and 'tbtc' anchor types. Cannot verify 'cal' anchors.` ) done() }) }) it('should return successful result with invalid tbtc proof (json) and legacy anchor', done => { let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof-l.chp.json')) tbtcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [tbtcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal(`Cannot verify legacy anchors.`) done() }) }) it('should return successful result with invalid tbtc proof (json)', done => { let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json')) tbtcProof.hash = 'badf27222fe366d0b8988b7312c6ba60ee422418d92b62cdcb71fe2991ee7391' request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [tbtcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(tbtcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(tbtcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('tcal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('tbtc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(false) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('invalid') done() }) }) it('should return successful result with valid tbtc proof (json)', done => { let tbtcProof = JSON.parse(fs.readFileSync('./tests/sample-data/tbtc-proof.chp.json')) request(insecureServer) .post('/verify') .set('Content-type', 'application/json') .send({ proofs: [tbtcProof] }) .expect('Content-type', /json/) .expect(200) .end((err, res) => { expect(err).to.equal(null) expect(res.body) .to.be.a('array') .and.to.have.length(1) expect(res.body[0]) .to.have.property('proof_index') .and.to.be.a('number') .and.to.equal(0) expect(res.body[0]) .to.have.property('hash') .and.to.be.a('string') .and.to.equal(tbtcProof.hash) expect(res.body[0]) .to.have.property('proof_id') .and.to.be.a('string') .and.to.equal(tbtcProof.proof_id) expect(res.body[0]) .to.have.property('hash_submitted_node_at') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_submitted_node_at) expect(res.body[0]) .to.have.property('hash_id_core') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_id_core) expect(res.body[0]) .to.have.property('hash_submitted_core_at') .and.to.be.a('string') .and.to.equal(tbtcProof.hash_submitted_core_at) expect(res.body[0]) .to.have.property('anchors') .and.to.be.a('array') expect(res.body[0].anchors).to.have.length(2) expect(res.body[0].anchors[0]).to.be.a('object') expect(Object.keys(res.body[0].anchors[0]).length).to.equal(3) expect(res.body[0].anchors[0]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('cal_anchor_branch') expect(res.body[0].anchors[0]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('tcal') expect(res.body[0].anchors[0]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[0]) expect(res.body[0].anchors[1]).to.be.a('object') expect(Object.keys(res.body[0].anchors[1]).length).to.equal(3) expect(res.body[0].anchors[1]) .to.have.property('branch') .and.to.be.a('string') .and.to.equal('btc_anchor_branch') expect(res.body[0].anchors[1]) .to.have.property('type') .and.to.be.a('string') .and.to.equal('tbtc') expect(res.body[0].anchors[1]) .to.have.property('valid') .and.to.be.a('boolean') .and.to.equal(true) expect(res.body[0].anchors[1]) expect(res.body[0]) .to.have.property('status') .and.to.be.a('string') .and.to.equal('verified') done() }) }) }) })