├── .eslintignore ├── img └── proof.png ├── integration-tests ├── .gitignore ├── Cargo.toml ├── build.rs ├── src │ └── proto │ │ ├── mod.rs │ │ └── wallet.proto └── README.md ├── src ├── index.js ├── blockchain │ ├── index.js │ ├── table.js │ ├── constants.js │ ├── transport.js │ ├── block.js │ ├── ProofPath.js │ ├── merkle.js │ └── merkle-patricia.js ├── types │ ├── index.js │ ├── generic.js │ ├── validate.js │ ├── message.js │ ├── hexadecimal.js │ └── convert.js ├── helpers.js └── crypto │ └── index.js ├── test ├── sources │ ├── data │ │ ├── convertors │ │ │ ├── stringToUint8Array.json │ │ │ ├── hexadecimalToUint8Array.json │ │ │ ├── uint8ArrayToHexadecimal.json │ │ │ ├── binaryStringToHexadecimal.json │ │ │ ├── hexadecimalToBinaryString.json │ │ │ ├── uint8ArrayToBinaryString.json │ │ │ └── binaryStringToUint8Array.json │ │ ├── merkle-tree │ │ │ ├── small.json │ │ │ ├── single-element.json │ │ │ └── several-elements.json │ │ └── map-proof.json │ ├── proto │ │ ├── timestamping.proto │ │ └── cryptocurrency.proto │ ├── readme.js │ ├── types.js │ ├── convertors.js │ ├── protobuf-message.js │ ├── merkle-proof.js │ ├── cryptography.js │ └── blockchain.js └── library │ └── version.js ├── .eslintrc.json ├── .gitignore ├── .babelrc ├── .editorconfig ├── proto ├── exonum │ ├── crypto │ │ └── types.proto │ ├── common │ │ └── bit_vec.proto │ ├── key_value_sequence.proto │ ├── runtime │ │ ├── auth.proto │ │ └── base.proto │ ├── messages.proto │ └── blockchain.proto └── google │ └── protobuf │ ├── empty.proto │ └── timestamp.proto ├── git-publish.sh ├── Gruntfile.js ├── .travis.yml ├── examples ├── list-proof.js ├── transactions.js └── map-proof.js ├── package.json ├── CHANGELOG.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | /proto/*.js 2 | /test/sources/proto/*.js 3 | -------------------------------------------------------------------------------- /img/proof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exonum/exonum-client/HEAD/img/proof.png -------------------------------------------------------------------------------- /integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /src/proto/stubs.js 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export const version = '@@version' 2 | export * from './types' 3 | export * from './crypto' 4 | export * from './blockchain' 5 | -------------------------------------------------------------------------------- /test/sources/data/convertors/stringToUint8Array.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "Hello world", 3 | "to": [72,101,108,108,111,32,119,111,114,108,100] 4 | } 5 | -------------------------------------------------------------------------------- /src/blockchain/index.js: -------------------------------------------------------------------------------- 1 | export * from './merkle' 2 | export * from './merkle-patricia' 3 | export * from './block' 4 | export * from './transport' 5 | export * from './table' 6 | -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | import * as protocol from '../../proto/protocol' 2 | 3 | export * from './generic' 4 | export * from './message' 5 | export * from './convert' 6 | export * from './hexadecimal' 7 | export { protocol } 8 | -------------------------------------------------------------------------------- /test/sources/data/convertors/hexadecimalToUint8Array.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "0438082601f8b38ae010a621a48f4b4cd021c4e6e69219e3c2d8abab482039e9", 3 | "to": [4,56,8,38,1,248,179,138,224,16,166,33,164,143,75,76,208,33,196,230,230,146,25,227,194,216,171,171,72,32,57,233] 4 | } 5 | -------------------------------------------------------------------------------- /test/sources/data/convertors/uint8ArrayToHexadecimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": [4,56,8,38,1,248,179,138,224,16,166,33,164,143,75,76,208,33,196,230,230,146,25,227,194,216,171,171,72,32,57,233], 3 | "to": "0438082601f8b38ae010a621a48f4b4cd021c4e6e69219e3c2d8abab482039e9" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": "standard", 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "object-curly-spacing" : [2, "always"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim files 2 | *.swp 3 | # macos files 4 | *.DS_Store 5 | 6 | # IDE files 7 | .idea 8 | .nyc_output 9 | .vscode 10 | *.iml 11 | 12 | # npm output 13 | node_modules 14 | npm-debug.log 15 | 16 | # Auto-generated files 17 | coverage 18 | dist 19 | lib 20 | /proto/**/*.js 21 | /test/sources/proto/**/*.js 22 | /npm 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": "> 0.25%, not dead" 5 | }] 6 | ], 7 | "env": { 8 | "test": { 9 | "plugins": [ 10 | "istanbul" 11 | ], 12 | "presets": [ 13 | ["@babel/preset-env", { 14 | "targets": { 15 | "node": true 16 | } 17 | }] 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/sources/data/convertors/binaryStringToHexadecimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "0000010000111000000010000010011000000001111110001011001110001010111000000001000010100110001000011010010010001111010010110100110011010000001000011100010011100110111001101001001000011001111000111100001011011000101010111010101101001000001000000011100111101001", 3 | "to": "201c1064801fcd510708658425f1d2320b842367674998c7431bd5d512049c97" 4 | } 5 | -------------------------------------------------------------------------------- /test/sources/data/convertors/hexadecimalToBinaryString.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "0438082601f8b38ae010a621a48f4b4cd021c4e6e69219e3c2d8abab482039e9", 3 | "to": "0010000000011100000100000110010010000000000111111100110101010001000001110000100001100101100001000010010111110001110100100011001000001011100001000010001101100111011001110100100110011000110001110100001100011011110101011101010100010010000001001001110010010111" 4 | } 5 | -------------------------------------------------------------------------------- /test/library/version.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | const expect = require('chai').expect 5 | const Exonum = require('../../lib') 6 | 7 | describe('Serialize data into array of 8-bit integers', function () { 8 | it('should return current library version', function () { 9 | const version = require('../../package.json').version 10 | expect(Exonum.version).to.equal(version) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/sources/data/convertors/uint8ArrayToBinaryString.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": [4,56,8,38,1,248,179,138,224,16,166,33,164,143,75,76,208,33,196,230,230,146,25,227,194,216,171,171,72,32,57,233], 3 | "to": "0010000000011100000100000110010010000000000111111100110101010001000001110000100001100101100001000010010111110001110100100011001000001011100001000010001101100111011001110100100110011000110001110100001100011011110101011101010100010010000001001001110010010111" 4 | } 5 | -------------------------------------------------------------------------------- /test/sources/data/merkle-tree/small.json: -------------------------------------------------------------------------------- 1 | { 2 | "proof": { 3 | "proof": [], 4 | "entries": [ 5 | [0, "d6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b"], 6 | [1, "ef9c89edc71fdf62b1642aa13b5d6f6e9b09717b4e77c045dcbd24c1318a50e9"], 7 | [2, "463249d0a1109e8469a2af46419655c9cd2a41f6ce5d2afc16597927467ab56c"], 8 | [3, "c808416e4ce59b474a9e6311d8619f5519bc72cea884b435215b52540912ca93"] 9 | ], 10 | "length": 4 11 | }, 12 | "trustedRoot": "33c91cbef14c785e8d87e383bad7dc8db81a5911c6061de77f786565b12ac46b" 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # we recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.rs] 23 | indent_size = 4 24 | 25 | [{package,bower}.json] 26 | indent_style = space 27 | indent_size = 2 28 | -------------------------------------------------------------------------------- /integration-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integration_tests" 3 | version = "0.0.0" 4 | authors = ["The Exonum Team "] 5 | publish = false 6 | edition = "2018" 7 | 8 | [dependencies] 9 | exonum = "=1.0.0" 10 | exonum-derive = "=1.0.0" 11 | exonum-merkledb = "=1.0.0" 12 | exonum-proto = "=1.0.0" 13 | 14 | actix-web = { version = "0.7.19", default-features = false } 15 | anyhow = "1.0" 16 | backtrace = "0.3" 17 | chrono = "0.4" 18 | hex = "0.4" 19 | protobuf = "2.25" 20 | rand = "0.8" 21 | rand_chacha = "0.3" 22 | serde = "1.0" 23 | serde_derive = "1.0" 24 | uuid = "0.8" 25 | 26 | [build-dependencies] 27 | exonum-build = "=1.0.0" 28 | -------------------------------------------------------------------------------- /test/sources/data/convertors/binaryStringToUint8Array.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": "1110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110111001101110011011100110", 3 | "to": [ 4 | 103, 5 | 103, 6 | 103, 7 | 103, 8 | 103, 9 | 103, 10 | 103, 11 | 103, 12 | 103, 13 | 103, 14 | 103, 15 | 103, 16 | 103, 17 | 103, 18 | 103, 19 | 103, 20 | 103, 21 | 103, 22 | 103, 23 | 103, 24 | 103, 25 | 103, 26 | 103, 27 | 103, 28 | 103, 29 | 103, 30 | 103, 31 | 103, 32 | 103, 33 | 103, 34 | 103, 35 | 103 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/sources/data/merkle-tree/single-element.json: -------------------------------------------------------------------------------- 1 | { 2 | "proof": { 3 | "proof": [ 4 | { 5 | "index": 3, 6 | "height": 1, 7 | "hash": "61119196a01db39f0b3a381579c366c8f85bf5186f53754531efe95f7e7a47c8" 8 | }, 9 | { 10 | "index": 0, 11 | "height": 2, 12 | "hash": "7bfe099e406e9c23b06ef1c6d50268524941cfa19152720fe841a9268b9375dc" 13 | }, 14 | { 15 | "index": 1, 16 | "height": 3, 17 | "hash": "5f5a1116ef235eeba051b17ba1e16e96d6a2126cbcfe0c27528ffa63ef30c6b9" 18 | } 19 | ], 20 | "entries": [ 21 | [ 22 | 2, 23 | "463249d0a1109e8469a2af46419655c9cd2a41f6ce5d2afc16597927467ab56c" 24 | ] 25 | ], 26 | "length": 6 27 | }, 28 | "trustedRoot": "48a29bbce0f0054a8e75a653c9fb1408dce5b808e27e5922e7c8bc7b6e919ae9" 29 | } 30 | -------------------------------------------------------------------------------- /integration-tests/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use exonum_build::ProtobufGenerator; 16 | 17 | fn main() { 18 | ProtobufGenerator::with_mod_name("protobuf_mod.rs") 19 | .with_input_dir("src/proto") 20 | .with_crypto() 21 | .generate(); 22 | } 23 | -------------------------------------------------------------------------------- /proto/exonum/crypto/types.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.crypto; 18 | 19 | option java_package = "com.exonum.messages.crypto"; 20 | 21 | message Hash { bytes data = 1; } 22 | 23 | message PublicKey { bytes data = 1; } 24 | 25 | message Signature { bytes data = 1; } 26 | -------------------------------------------------------------------------------- /src/blockchain/table.js: -------------------------------------------------------------------------------- 1 | import { Hash } from '../types/hexadecimal' 2 | import { MapProof } from './merkle-patricia' 3 | 4 | /** 5 | * Validate path from tree root to some table 6 | * @param {Object} proof 7 | * @param {string} stateHash 8 | * @param {string} tableFullName 9 | * @returns {string} 10 | */ 11 | export function verifyTable (proof, stateHash, tableFullName) { 12 | const stringKeys = { 13 | serialize (str) { 14 | return Buffer.from(str) 15 | } 16 | } 17 | 18 | // Validate proof of table existence in the state hash. 19 | const tableProof = new MapProof(proof, stringKeys, Hash) 20 | if (tableProof.merkleRoot !== stateHash) { 21 | throw new Error('Table proof is corrupted') 22 | } 23 | 24 | // Get root hash of the table. 25 | const rootHash = tableProof.entries.get(tableFullName) 26 | if (typeof rootHash === 'undefined') { 27 | throw new Error('Table not found in the root tree') 28 | } 29 | return rootHash 30 | } 31 | -------------------------------------------------------------------------------- /integration-tests/src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Module of the rust-protobuf generated files. 16 | 17 | // For protobuf generated files. 18 | #![allow(bare_trait_objects, renamed_and_removed_lints)] 19 | 20 | pub use self::wallet::{MockPayload, Wallet}; 21 | 22 | include!(concat!(env!("OUT_DIR"), "/protobuf_mod.rs")); 23 | 24 | use exonum::crypto::proto::*; 25 | -------------------------------------------------------------------------------- /src/blockchain/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prefix marker for a blob node in a `ProofMapIndex`. 3 | * 4 | * @type {number} 5 | */ 6 | export const BLOB_PREFIX = 0 7 | /** 8 | * Prefix marker for a `ProofListIndex` object. 9 | * It is defined in the `HashTag` enum in the `exonum_merkledb` crate. 10 | * 11 | * @type {number} 12 | */ 13 | export const LIST_PREFIX = 2 14 | /** 15 | * Prefix marker for an intermediate `ProofListIndex` node. 16 | * It is defined in the `HashTag` enum in the `exonum_merkledb` crate. 17 | * 18 | * @type {number} 19 | */ 20 | export const LIST_BRANCH_PREFIX = 1 21 | /** 22 | * Prefix marker for a `ProofMapIndex` object. 23 | * It is defined in the `HashTag` enum in the `exonum_merkledb` crate. 24 | * 25 | * @type {number} 26 | */ 27 | export const MAP_PREFIX = 3 28 | /** 29 | * Prefix marker for an intermediate `ProofMapIndex` nodes. 30 | * It is defined in the `HashTag` enum in the `exonum_merkledb` crate. 31 | * 32 | * @type {number} 33 | */ 34 | export const MAP_BRANCH_PREFIX = 4 35 | -------------------------------------------------------------------------------- /proto/exonum/common/bit_vec.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.common; 18 | 19 | option java_package = "com.exonum.messages.common"; 20 | 21 | // Vector of bits. 22 | message BitVec { 23 | // Buffer containing the bits. Most significant bits of each byte 24 | // have lower indexes in the vector. 25 | bytes data = 1; 26 | // Number of bits. 27 | uint64 len = 2; 28 | } 29 | -------------------------------------------------------------------------------- /proto/exonum/key_value_sequence.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Storage with keys and values represented as bytes arrays. 16 | 17 | syntax = "proto3"; 18 | 19 | package exonum; 20 | 21 | option java_package = "com.exonum.messages.core"; 22 | 23 | // Some non-scalar key-value pair. 24 | message KeyValue { 25 | string key = 1; 26 | bytes value = 2; 27 | } 28 | 29 | // A sequence of key-value pairs. 30 | message KeyValueSequence { 31 | repeated KeyValue entries = 1; 32 | } 33 | -------------------------------------------------------------------------------- /test/sources/data/merkle-tree/several-elements.json: -------------------------------------------------------------------------------- 1 | { 2 | "proof": { 3 | "proof": [ 4 | { 5 | "index": 2, 6 | "height": 1, 7 | "hash": "0d0a1b82c48c7165af77ee15d78ca5015a44f8ec52d168b38131702359de9920" 8 | }, 9 | { 10 | "index": 0, 11 | "height": 2, 12 | "hash": "7bfe099e406e9c23b06ef1c6d50268524941cfa19152720fe841a9268b9375dc" 13 | }, 14 | { 15 | "index": 3, 16 | "height": 2, 17 | "hash": "fc33b4e9d9fbaf2c44f29201605b309b2c736ac5061e95bd3bf192076cd434e0" 18 | }, 19 | { 20 | "index": 1, 21 | "height": 4, 22 | "hash": "af00f545fa8c4de315fa2e4f5de45e9f2f4fbd92e383b4b7b3b831e7bc28f538" 23 | } 24 | ], 25 | "entries": [ 26 | [3, "c808416e4ce59b474a9e6311d8619f5519bc72cea884b435215b52540912ca93"], 27 | [4, "06702cf775c0e5dc267d33299e80efc8e8328a50db24fc11a74c1897b5f16c22"], 28 | [5, "996ee3fda175e2c199dadd447d5dd8c9dfa74248cf1e55582b865d75f5461315"] 29 | ], 30 | "length": 14 31 | }, 32 | "trustedRoot": "7ee9e27d6704b3e5ad1fe6aca8030f318a9873d607b0f28205ae23e16d053569" 33 | } 34 | -------------------------------------------------------------------------------- /integration-tests/src/proto/wallet.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.client.integration_tests; 18 | 19 | import "exonum/crypto/types.proto"; 20 | 21 | // Wallet struct used to persist data within the service. 22 | message Wallet { 23 | // Public key of the wallet owner. 24 | exonum.crypto.PublicKey pub_key = 1; 25 | // Name of the wallet owner. 26 | string name = 2; 27 | // Current balance. 28 | uint64 balance = 3; 29 | // Unique id 30 | string uniq_id = 4; 31 | } 32 | 33 | message MockPayload { 34 | string metadata = 1; 35 | uint32 retries = 2; 36 | } 37 | -------------------------------------------------------------------------------- /git-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to push output for npm to the `npm` branch of the repository. 4 | # This allows to use the package as a `git` dependency by pointing at this branch 5 | # (or a specific commit). 6 | 7 | # Inspired by a similar script in `immutable.js`: 8 | # https://github.com/facebook/immutable-js/blob/master/resources/gitpublish.sh 9 | 10 | set -e 11 | 12 | REPO=exonum/exonum-client 13 | BOT_NAME="Travis CI" 14 | BOT_EMAIL="ostrovski.alex@gmail.com" 15 | 16 | rm -rf npm 17 | git clone -b npm "https://${GH_TOKEN}@github.com/${REPO}.git" npm 18 | 19 | rm -rf npm/* npm/.gitkeep 20 | 21 | cp -r lib npm/ 22 | cp -r proto npm/ 23 | cp LICENSE npm/ 24 | cp README.md npm/ 25 | 26 | node -e "var package = require('./package.json'); \ 27 | delete package.scripts; \ 28 | delete package.nyc; \ 29 | delete package.devDependencies; \ 30 | require('fs').writeFileSync('./npm/package.json', JSON.stringify(package, null, 2));" 31 | 32 | cd npm 33 | git config user.name "${BOT_NAME}" 34 | git config user.email "${BOT_EMAIL}" 35 | git config push.default "simple" 36 | git add -A . 37 | if git diff --staged --quiet; then 38 | echo "Nothing to publish" 39 | else 40 | git commit -a -m "Deploy master branch on GitHub" 41 | git push >/dev/null 2>&1 42 | echo "Published to npm branch of ${REPO}" 43 | fi 44 | -------------------------------------------------------------------------------- /test/sources/proto/timestamping.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.examples.timestamping; 18 | 19 | import "exonum/crypto/types.proto"; 20 | import "google/protobuf/timestamp.proto"; 21 | 22 | // Stores content's hash and some metadata about it. 23 | message Timestamp { 24 | exonum.crypto.Hash content_hash = 1; 25 | string metadata = 2; 26 | } 27 | 28 | message TimestampEntry { 29 | // Timestamp data. 30 | Timestamp timestamp = 1; 31 | // Hash of transaction. 32 | exonum.crypto.Hash tx_hash = 2; 33 | // Timestamp time. 34 | google.protobuf.Timestamp time = 3; 35 | } 36 | 37 | /// Timestamping transaction. 38 | message TxTimestamp { Timestamp content = 1; } 39 | -------------------------------------------------------------------------------- /integration-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This directory provides automated integration testing between the light client 4 | library and the [Exonum core Rust facilities][exonum]. This is accomplished 5 | by building a simple web server powered by Exonum and querying it / parsing 6 | its responses with the help of the client. 7 | 8 | ## Prerequisites 9 | 10 | You need to have a Rust toolchain installed, which can be accomplished 11 | with the help of [`rustup`][rustup]. 12 | 13 | ## Running Tests 14 | 15 | On \*NIX, execute 16 | 17 | ```shell 18 | npm run integration:unix 19 | ``` 20 | 21 | This will automatically build the web server, launch it, perform Mocha-powered 22 | client tests, and finally stop the server. 23 | 24 | On Windows, or in other environments where the above command does not work, 25 | you can manually compile the server with 26 | 27 | ```shell 28 | npm run integration:build 29 | ``` 30 | 31 | Then, run it with 32 | 33 | ```shell 34 | cargo run --manifest-path integration-tests/Cargo.toml 35 | ``` 36 | 37 | (assuming you are in the root directory of the project; otherwise change the 38 | `manifest-path` correspondingly). 39 | 40 | Then, run the tests with 41 | 42 | ```shell 43 | npm run integration 44 | ``` 45 | 46 | [exonum]: https://github.com/exonum/exonum 47 | [rustup]: https://www.rustup.rs/ 48 | -------------------------------------------------------------------------------- /proto/exonum/runtime/auth.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.runtime; 18 | 19 | option java_package = "com.exonum.messages.core.runtime"; 20 | 21 | import "exonum/crypto/types.proto"; 22 | import "google/protobuf/empty.proto"; 23 | 24 | // The authorization information for a call to the service. 25 | message Caller { 26 | oneof caller { 27 | // The caller is identified by the specified Ed25519 public key. 28 | exonum.crypto.PublicKey transaction_author = 1; 29 | // The call is invoked with the authority of a blockchain service 30 | // with the specified identifier. 31 | uint32 instance_id = 2; 32 | // The call is invoked by one of the blockchain lifecycle events. 33 | google.protobuf.Empty blockchain = 3; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/types/generic.js: -------------------------------------------------------------------------------- 1 | import * as crypto from '../crypto' 2 | import { cleanZeroValuedFields } from '../helpers' 3 | 4 | /** 5 | * @constructor 6 | * @param {Object} schema 7 | */ 8 | class Type { 9 | constructor (schema) { 10 | this.schema = schema 11 | } 12 | 13 | /** 14 | * Serialize data into array of 8-bit integers 15 | * @param {Object} data 16 | * @returns {Uint8Array} 17 | */ 18 | serialize (data) { 19 | const object = cleanZeroValuedFields(data, {}) 20 | return this.schema.encode(object).finish() 21 | } 22 | 23 | /** 24 | * Get SHA256 hash 25 | * @param {Object} data 26 | * @returns {string} 27 | */ 28 | hash (data) { 29 | return crypto.hash(data, this) 30 | } 31 | 32 | /** 33 | * Get ED25519 signature 34 | * @param {string} secretKey 35 | * @param {Object} data 36 | * @returns {string} 37 | */ 38 | sign (secretKey, data) { 39 | return crypto.sign(secretKey, data, this) 40 | } 41 | 42 | /** 43 | * Verifies ED25519 signature 44 | * @param {string} signature 45 | * @param {string} publicKey 46 | * @param {Object} data 47 | * @returns {boolean} 48 | */ 49 | verifySignature (signature, publicKey, data) { 50 | return crypto.verifySignature(signature, publicKey, data, this) 51 | } 52 | } 53 | 54 | /** 55 | * Create element of Type class 56 | * @param {Object} type 57 | * @returns {Type} 58 | */ 59 | export function newType (type) { 60 | return new Type(type) 61 | } 62 | 63 | /** 64 | * Check if passed object is of type Type 65 | * @param {Object} type 66 | * @returns {boolean} 67 | */ 68 | export function isType (type) { 69 | return type instanceof Type 70 | } 71 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | function isStrictTypedArray (arr) { 2 | return ( 3 | arr instanceof Int8Array || 4 | arr instanceof Int16Array || 5 | arr instanceof Int32Array || 6 | arr instanceof Uint8Array || 7 | arr instanceof Uint8ClampedArray || 8 | arr instanceof Uint16Array || 9 | arr instanceof Uint32Array || 10 | arr instanceof Float32Array || 11 | arr instanceof Float64Array 12 | ) 13 | } 14 | 15 | /** 16 | * Check if element is of type Object 17 | * @param obj 18 | * @returns {boolean} 19 | */ 20 | export function isObject (obj) { 21 | return (typeof obj === 'object' && !Array.isArray(obj) && obj !== null && !(obj instanceof Date)) 22 | } 23 | 24 | /** 25 | * @param {Object} element 26 | * @returns {boolean} 27 | */ 28 | export function verifyElement (element) { 29 | switch (typeof element) { 30 | case 'string': 31 | return element !== '0' && element.length !== 0 32 | case 'number': 33 | return element !== 0 34 | } 35 | return true 36 | } 37 | 38 | /** 39 | * @param {Object} data 40 | * @param {Object} object 41 | * @returns {Object} 42 | */ 43 | // FIXME: This is incorrect; `'0'` strings are removed even if they don't correspond to int field 44 | export function cleanZeroValuedFields (data, object) { 45 | const keys = Object.keys(data) 46 | keys.forEach(key => { 47 | if (isStrictTypedArray(data[key]) || data[key] instanceof Array) { 48 | object[key] = data[key] 49 | } else { 50 | if (typeof data[key] === 'object') { 51 | object[key] = cleanZeroValuedFields(data[key], {}) 52 | } else { 53 | if (verifyElement(data[key])) { 54 | object[key] = data[key] 55 | } 56 | } 57 | } 58 | }) 59 | return object 60 | } 61 | -------------------------------------------------------------------------------- /test/sources/proto/cryptocurrency.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.examples.cryptocurrency_advanced; 18 | 19 | import "exonum/crypto/types.proto"; 20 | 21 | /// Transfer `amount` of the currency from one wallet to another. 22 | message Transfer { 23 | // `PublicKey` of receiver's wallet. 24 | exonum.crypto.PublicKey to = 1; 25 | // Amount of currency to transfer. 26 | uint64 amount = 2; 27 | // Auxiliary number to guarantee non-idempotence of transactions. 28 | uint64 seed = 3; 29 | } 30 | 31 | // Issue `amount` of the currency to the `wallet`. 32 | message Issue { 33 | // Issued amount of currency. 34 | uint64 amount = 1; 35 | // Auxiliary number to guarantee non-idempotence of transactions. 36 | uint64 seed = 2; 37 | } 38 | 39 | // Create wallet with the given `name`. 40 | message CreateWallet { 41 | // Name of the new wallet. 42 | string name = 1; 43 | } 44 | 45 | // Wallet information stored in the database. 46 | message Wallet { 47 | // `PublicKey` of the wallet. 48 | exonum.crypto.PublicKey pub_key = 1; 49 | // Name of the wallet. 50 | string name = 2; 51 | // Current balance of the wallet. 52 | uint64 balance = 3; 53 | // Length of the transactions history. 54 | uint64 history_len = 4; 55 | // `Hash` of the transactions history. 56 | exonum.crypto.Hash history_hash = 5; 57 | } 58 | -------------------------------------------------------------------------------- /src/types/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} value 3 | * @param {number} min 4 | * @param {number} max 5 | * @param {number} from 6 | * @param {number} length 7 | * @returns {boolean} 8 | */ 9 | export function validateInteger (value, min, max, from, length) { 10 | if (typeof value !== 'number' || value < min || value > max) { 11 | return false 12 | } 13 | 14 | return true 15 | } 16 | 17 | /** 18 | * @param {string} hash 19 | * @param {number} [bytes=32] - optional 20 | * @returns {boolean} 21 | */ 22 | export function validateHexadecimal (hash, bytes) { 23 | bytes = bytes || 32 24 | 25 | if (typeof hash !== 'string') { 26 | return false 27 | } 28 | if (hash.length !== bytes * 2) { 29 | // 'hexadecimal string is of wrong length 30 | return false 31 | } 32 | 33 | for (let i = 0; i < hash.length; i++) { 34 | if (isNaN(parseInt(hash[i], 16))) { 35 | // invalid symbol in hexadecimal string 36 | return false 37 | } 38 | } 39 | 40 | return true 41 | } 42 | 43 | /** 44 | * @param {Array} arr 45 | * @param {number} [bytes] - optional 46 | * @returns {boolean} 47 | */ 48 | export function validateBytesArray (arr, bytes) { 49 | if (!Array.isArray(arr) && !(arr instanceof Uint8Array)) { 50 | return false 51 | } 52 | if (bytes && arr.length !== bytes) { 53 | // array is of wrong length 54 | return false 55 | } 56 | 57 | for (let i = 0; i < arr.length; i++) { 58 | if (typeof arr[i] !== 'number' || arr[i] < 0 || arr[i] > 255) { 59 | return false 60 | } 61 | } 62 | 63 | return true 64 | } 65 | 66 | /** 67 | * @param {string} str 68 | * @param {number} [bits] - optional 69 | * @returns {*} 70 | */ 71 | export function validateBinaryString (str, bits) { 72 | if (bits !== undefined && str.length !== bits) { 73 | return false 74 | } 75 | 76 | for (let i = 0; i < str.length; i++) { 77 | if (str[i] !== '0' && str[i] !== '1') { 78 | // wrong bit 79 | return false 80 | } 81 | } 82 | 83 | return true 84 | } 85 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('load-grunt-tasks')(grunt) 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | eslint: { 7 | dist: { 8 | src: ['./src/**/*.js'] 9 | }, 10 | tests: { 11 | src: ['./test/**/*.js'] 12 | } 13 | }, 14 | clean: { 15 | dist: { 16 | src: ['./dist', './lib'] 17 | } 18 | }, 19 | babel: { 20 | options: { 21 | presets: ['@babel/preset-env'] 22 | }, 23 | dist: { 24 | expand: true, 25 | cwd: './src/', 26 | src: ['**/*.js'], 27 | dest: './lib/' 28 | } 29 | }, 30 | browserify: { 31 | dist: { 32 | options: { 33 | browserifyOptions: { debug: false, standalone: 'Exonum' }, 34 | transform: [['babelify', { 'presets': ['@babel/env'] }]] 35 | }, 36 | src: './src/index.js', 37 | dest: './dist/<%= pkg.name %>.js' 38 | } 39 | }, 40 | 'string-replace': { 41 | dist: { 42 | files: [ 43 | { src: ['./lib/index.js'], dest: './lib/index.js' } 44 | ], 45 | options: { 46 | replacements: [{ 47 | pattern: '@@version', 48 | replacement: '<%= pkg.version %>' 49 | }] 50 | } 51 | } 52 | }, 53 | uglify: { 54 | dist: { 55 | src: './dist/<%= pkg.name %>.js', 56 | dest: './dist/<%= pkg.name %>.min.js' 57 | } 58 | }, 59 | mochaTest: { 60 | dist: { 61 | options: { 62 | reporter: 'spec', 63 | require: ['@babel/register'] 64 | }, 65 | src: ['./test/sources/**/*.js'] 66 | }, 67 | all: { 68 | options: { 69 | reporter: 'spec', 70 | require: ['@babel/register'] 71 | }, 72 | src: ['./test/**/*.js'] 73 | } 74 | } 75 | }) 76 | 77 | grunt.registerTask('compile', ['eslint', 'clean', 'babel', 'string-replace', 'browserify', 'uglify']) 78 | grunt.registerTask('test', ['eslint:tests', 'mochaTest']) 79 | grunt.registerTask('default', ['compile', 'eslint:tests', 'mochaTest:all']) 80 | } 81 | -------------------------------------------------------------------------------- /proto/exonum/messages.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The messages collection for the default Exonum consensus implementation. 16 | 17 | syntax = "proto3"; 18 | 19 | package exonum; 20 | 21 | option java_package = "com.exonum.messages.core"; 22 | 23 | import "exonum/crypto/types.proto"; 24 | import "exonum/runtime/base.proto"; 25 | import "google/protobuf/timestamp.proto"; 26 | 27 | // Container for the signed messages. 28 | message SignedMessage { 29 | // Payload of the message as a serialized `ExonumMessage`. 30 | bytes payload = 1; 31 | // Public key of the author of the message. 32 | exonum.crypto.PublicKey author = 2; 33 | // Digital signature over the payload created with a secret key of the author of the message. 34 | exonum.crypto.Signature signature = 3; 35 | } 36 | 37 | // Subset of Exonum messages defined in the Exonum core. 38 | message CoreMessage { 39 | oneof kind { 40 | // Transaction message. 41 | exonum.runtime.AnyTx any_tx = 1; 42 | // Precommit (block endorsement) message. 43 | Precommit precommit = 2; 44 | } 45 | } 46 | 47 | // Pre-commit for a block, essentially meaning that a validator node endorses the block. 48 | message Precommit { 49 | // ID of the validator endorsing the block. 50 | uint32 validator = 1; 51 | // The height to which the message is related. 52 | uint64 height = 2; 53 | // The round to which the message is related. 54 | uint32 round = 3; 55 | // Hash of the block proposal. Note that the proposal format is not defined by the core. 56 | exonum.crypto.Hash propose_hash = 4; 57 | // Hash of the new block. 58 | exonum.crypto.Hash block_hash = 5; 59 | // Local time of the validator node when the `Precommit` was created. 60 | google.protobuf.Timestamp time = 6; 61 | } 62 | -------------------------------------------------------------------------------- /proto/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "github.com/golang/protobuf/ptypes/empty"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | -------------------------------------------------------------------------------- /src/blockchain/transport.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { uint8ArrayToHexadecimal } from '../types/convert' 3 | 4 | const ATTEMPTS = 10 5 | const ATTEMPT_TIMEOUT = 500 6 | 7 | /** 8 | * Send transaction to the blockchain 9 | * @param {string} explorerBasePath 10 | * @param {Uint8Array | string} transaction 11 | * @param {number} attempts 12 | * @param {number} timeout 13 | * @return {Promise} 14 | */ 15 | export async function send (explorerBasePath, transaction, attempts = ATTEMPTS, timeout = ATTEMPT_TIMEOUT) { 16 | if (typeof explorerBasePath !== 'string') { 17 | throw new TypeError('Explorer base path endpoint of wrong data type is passed. String is required.') 18 | } 19 | 20 | function sleep (timeout) { 21 | return new Promise(resolve => { 22 | setTimeout(resolve, timeout) 23 | }) 24 | } 25 | 26 | attempts = +attempts 27 | timeout = +timeout 28 | if (typeof transaction !== 'string') { 29 | transaction = uint8ArrayToHexadecimal(new Uint8Array(transaction)) 30 | } 31 | 32 | const response = await axios.post(`${explorerBasePath}`, { 33 | tx_body: transaction 34 | }) 35 | const txHash = response.data.tx_hash 36 | 37 | let count = attempts 38 | let errored = false 39 | while (count >= 0) { 40 | try { 41 | const response = await axios.get(`${explorerBasePath}?hash=${txHash}`) 42 | if (response.data.type === 'committed') { 43 | return txHash 44 | } 45 | errored = false 46 | } catch (error) { 47 | errored = true 48 | } 49 | count-- 50 | await sleep(timeout) 51 | } 52 | 53 | if (errored) { 54 | throw new Error('The request failed or the blockchain node did not respond.') 55 | } else { 56 | throw new Error('The transaction was not accepted to the block for the expected period.') 57 | } 58 | } 59 | 60 | /** 61 | * Sends several transactions to the blockchain. 62 | * 63 | * @param {string} explorerBasePath 64 | * @param {Array} transactions 65 | * @param {number} attempts 66 | * @param {number} timeout 67 | * @return {Promise} 68 | */ 69 | export function sendQueue ( 70 | explorerBasePath, 71 | transactions, 72 | attempts = ATTEMPTS, 73 | timeout = ATTEMPT_TIMEOUT 74 | ) { 75 | let index = 0 76 | const responses = [] 77 | 78 | return (function shift () { 79 | const transaction = transactions[index++] 80 | 81 | return send(explorerBasePath, transaction, attempts, timeout).then(response => { 82 | responses.push(response) 83 | if (index < transactions.length) { 84 | return shift() 85 | } else { 86 | return responses 87 | } 88 | }) 89 | })() 90 | } 91 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | addons: 4 | apt: 5 | sources: 6 | - sourceline: 'ppa:exonum/rocksdb' 7 | - sourceline: 'ppa:maarten-fonville/protobuf' 8 | - sourceline: 'ppa:fsgmhoward/shadowsocks-libev' 9 | packages: 10 | - binutils-dev 11 | - build-essential 12 | - cmake 13 | - g++ 14 | - gcc 15 | - libcurl4-openssl-dev 16 | - libdw-dev 17 | - libelf-dev 18 | - libiberty-dev 19 | - libprotobuf-dev 20 | - librocksdb6.2 21 | - libsnappy-dev 22 | - libsodium-dev 23 | - libssl-dev 24 | - pkg-config 25 | - protobuf-compiler 26 | 27 | language: node_js 28 | node_js: 29 | - '8' 30 | - '10' 31 | - '12' 32 | 33 | env: 34 | global: 35 | - ROCKSDB_LIB_DIR=/usr/lib 36 | - SNAPPY_LIB_DIR=/usr/lib/x86_64-linux-gnu 37 | 38 | cache: 39 | cargo: true 40 | npm: true 41 | 42 | script: 43 | - npm test 44 | - node examples/list-proof.js 45 | - node examples/map-proof.js 46 | 47 | jobs: 48 | include: 49 | - env: FEATURE=integration 50 | language: rust 51 | rust: 1.57.0 52 | before_install: 53 | - nvm install 12 && nvm use 12 54 | - node --version 55 | - npm --version 56 | install: 57 | - npm install 58 | - npm run integration:build 59 | script: 60 | - npm run integration:unix 61 | # The transactions example requires the integration server, so we test it here. 62 | - npm run preintegration:unix && node examples/transactions.js 63 | after_script: 64 | - npm run postintegration:unix 65 | 66 | - stage: deploy 67 | env: FEATURE=deploy 68 | node_js: '10' 69 | script: skip 70 | after_success: echo "Done" 71 | deploy: 72 | - provider: npm 73 | email: exonum.bitfury@gmail.com 74 | skip_cleanup: true 75 | api_token: 76 | secure: KQihlRK+otmBcmKWA3w/HKPQxT19KnNVyP+nxaZp/lVrcUqszN1Qr0FAegz8Yaai6y2LVHA7ZKpGIMj9VZ1jEzcp59+jjYDj+gef8/kgPTgUjbmGnOs3p82ljZj4efQSuVwSRiEp4M9hyz2OlIbcPzu9bufLRx8DTmLKbFRWQ/7kaPVEzyJ1Me9kAPwFBVOptMkNR7mFFCNna0jJemi3hBwy59I0tTMAZNh/UUIu5kzG8JJ1kDxEuFLMNq65b6btjcd3gVs/nb3QjTtfx67BcMUCmdOvdgrXdZ1NtcN2SthDG+Ldott1wfL3vpPjCdILB5FrNQz//LSM7REGiNL7x/X51hhkMkD8oZq+SsdTMzODd6N+hoo+z/peKAqGskhsui671isdrwFwW/7u8PLz/kwfT6UEcHSrctwMl4ICICYpF81L/fSpysHMt83+1XqqECEu7pXwAsbgu37GO2vPg3QU3+HtFHdFaIQuGedXw+pbskMn/pbj4Ekh+//nVkhcDM6eJAR5WMXPnznEl9QEY7uSYepPRNLPG4LcBPIHARhSddq07ksWwD2uT2GogQr//Z2lRHjvy4eY+mEwVH4vna/FmQ2mRA9sgrX7E9TvptmamBMcCRZYol3VIDo1Eee3BuR9E5wxjHEyzkfEMrLIr0Oqe3K4L+rrNjfE8wnxBvw= 77 | on: 78 | branch: master 79 | tags: true 80 | repo: exonum/exonum-client 81 | - provider: script 82 | skip_cleanup: true 83 | script: npm run git-publish 84 | on: 85 | branch: master 86 | repo: exonum/exonum-client 87 | 88 | after_success: 89 | - npm run coveralls 90 | -------------------------------------------------------------------------------- /src/blockchain/block.js: -------------------------------------------------------------------------------- 1 | import * as Long from 'long' 2 | import { Verified } from '../types/message' 3 | import { hexadecimalToUint8Array, uint8ArrayToHexadecimal } from '../types/convert' 4 | import { hash } from '../crypto' 5 | 6 | import * as protobuf from '../../proto/protocol' 7 | import { cleanZeroValuedFields } from '../helpers' 8 | const Block = protobuf.exonum.Block 9 | const { CoreMessage } = protobuf.exonum 10 | 11 | /** 12 | * Validate block and each precommit in block 13 | * @param {Object} data 14 | * @param {Array} validators 15 | */ 16 | export function verifyBlock ({ block, precommits }, validators) { 17 | const blockMessage = cleanZeroValuedFields(block, {}) 18 | // Transform hashes to the format accepted by Protobuf. 19 | const fields = ['prev_hash', 'tx_hash', 'state_hash', 'error_hash'] 20 | fields.forEach(fieldName => { 21 | blockMessage[fieldName] = { data: hexadecimalToUint8Array(blockMessage[fieldName]) } 22 | }) 23 | // Transform additional headers to the Protobuf format. 24 | const additionalHeaders = Object.entries(block.additional_headers.headers) 25 | .map(([key, value]) => ({ 26 | key, 27 | value: Uint8Array.from(value) 28 | })) 29 | blockMessage.additional_headers = { headers: { entries: additionalHeaders } } 30 | 31 | const buffer = Block.encode(blockMessage).finish() 32 | const blockHash = hash(buffer) 33 | 34 | if (precommits.length < quorumSize(validators.length)) { 35 | throw new Error('Insufficient number of precommits') 36 | } 37 | 38 | const endorsingValidators = new Set() 39 | for (let i = 0; i < precommits.length; i++) { 40 | const message = Verified.deserialize(CoreMessage, hexadecimalToUint8Array(precommits[i])) 41 | if (!message) { 42 | throw new Error('Precommit signature is wrong') 43 | } 44 | const plain = message.payload.precommit 45 | if (!plain) { 46 | throw new Error('Invalid message type (not a Precommit)') 47 | } 48 | 49 | if (Long.fromValue(plain.height).compare(Long.fromValue(block.height)) !== 0) { 50 | throw new Error('Precommit height does not match block height') 51 | } 52 | 53 | if (uint8ArrayToHexadecimal(plain.block_hash.data) !== blockHash) { 54 | throw new Error('Precommit block hash does not match calculated block hash') 55 | } 56 | 57 | const validatorId = plain.validator || 0 58 | if (endorsingValidators.has(validatorId)) { 59 | throw new Error('Double endorsement from a validator') 60 | } 61 | endorsingValidators.add(validatorId) 62 | 63 | const expectedKey = validators[validatorId] 64 | if (message.author !== expectedKey) { 65 | throw new Error('Precommit public key does not match key of corresponding validator') 66 | } 67 | } 68 | } 69 | 70 | function quorumSize (validatorCount) { 71 | return Math.floor(validatorCount * 2 / 3) + 1 72 | } 73 | -------------------------------------------------------------------------------- /proto/exonum/runtime/base.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum.runtime; 18 | 19 | option java_package = "com.exonum.messages.core.runtime"; 20 | 21 | // Unique service transaction identifier. 22 | message CallInfo { 23 | // Unique service instance identifier. The dispatcher uses this identifier to 24 | // find the corresponding runtime to execute a transaction. 25 | uint32 instance_id = 1; 26 | // Identifier of the method in the service interface required for the call. 27 | uint32 method_id = 2; 28 | } 29 | 30 | // Transaction with the information required to dispatch it to a service. 31 | message AnyTx { 32 | // Information required for the call of the corresponding executor. 33 | CallInfo call_info = 1; 34 | // Serialized transaction arguments. 35 | bytes arguments = 2; 36 | } 37 | 38 | // The artifact identifier is required to construct service instances. 39 | // In other words, an artifact identifier is similar to a class name, 40 | // and a specific service instance is similar to a class instance. 41 | message ArtifactId { 42 | // Runtime identifier. 43 | uint32 runtime_id = 1; 44 | // Artifact name. 45 | string name = 2; 46 | // Semantic version of the artifact. 47 | string version = 3; 48 | } 49 | 50 | // Exhaustive artifact specification. This information is enough 51 | // to deploy an artifact. 52 | message ArtifactSpec { 53 | // Information uniquely identifying the artifact. 54 | ArtifactId artifact = 1; 55 | // Runtime-specific artifact payload. 56 | bytes payload = 2; 57 | } 58 | 59 | // Exhaustive service instance specification. 60 | message InstanceSpec { 61 | // Unique numeric ID of the service instance. 62 | // 63 | // Exonum assigns an ID to the service on instantiation. It is mainly used 64 | // to route transaction messages belonging to this instance. 65 | uint32 id = 1; 66 | // Unique name of the service instance. 67 | // 68 | // The name serves as a primary identifier of this service in most operations. 69 | // It is assigned by the network administrators. 70 | // 71 | // The name must correspond to the following regular expression: `[a-zA-Z0-9/\:-_]+`. 72 | string name = 2; 73 | // Identifier of the corresponding artifact. 74 | ArtifactId artifact = 3; 75 | } 76 | -------------------------------------------------------------------------------- /examples/list-proof.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example how to use `ListProof`s in client apps. 3 | */ 4 | 5 | const Exonum = require('..') 6 | const { ListProof, Hash } = Exonum 7 | const { expect } = require('chai') 8 | 9 | // This proof JSON for hashes with indexes 3 to 6 (exclusive) in a list of 20 hashes. It is generated 10 | // with the help of the `/hash-list/random` endpoint of the integration test server 11 | // (located in the `integration-tests` directory): 12 | // 13 | // cd integration-tests 14 | // cargo run & 15 | // curl http://localhost:8000/hash-list/random?seed=1337&count=20&start=3&end=6 16 | let proof = { 17 | proof: [ 18 | { 19 | index: 2, 20 | height: 1, 21 | hash: '6bb42bf3010407734e434e1f3c350b51236add748bb8f0eafe01e744f76f3442' 22 | }, 23 | { 24 | index: 0, 25 | height: 2, 26 | hash: 'ebf2cc018985999c773e986d18d8b78ee412b9d4bd4897b376423d743c6fe7a2' 27 | }, 28 | { 29 | index: 3, 30 | height: 2, 31 | hash: '059623695d87270700225cad666f8b4a0f69ae5821f7efaa562eb993b860c1e9' 32 | }, 33 | { 34 | index: 1, 35 | height: 4, 36 | hash: '533ce70d1713da610ebda38c1526496573d2459160190d0b7a9928ea25c4cc86' 37 | }, 38 | { 39 | index: 1, 40 | height: 5, 41 | hash: '8a5882abe9b6a6b8dcc60dd3e5ec1ed50902b8924d47170e1fc1dc6737dad690' 42 | } 43 | ], 44 | entries: [ 45 | [3, '51a9af560377af0994fe4be465ea5adff3372623c6ac692c4d3e23b323ef8486'], 46 | [4, '2b7e888246a3b450c67396062e53c8b6c4b776e082e7d2a81c5536e89fe6013e'], 47 | [5, '000b3b9ca14f1136c076b7f681b0a496f5108f721833e6465d0671c014e60b43'] 48 | ], 49 | length: 20 50 | } 51 | 52 | // Create a `ListProof` instance. The constructor will throw an error if 53 | // the supplied JSON is malformed. The constructor receives proof JSON and the 54 | // type of the elements in the proof. The latter is application-specific; here, 55 | // we use `Hash`es (this corresponds, for example, to the [cryptocurrency tutorial]). 56 | // 57 | // [cryptocurrency tutorial]: https://exonum.com/doc/version/latest/get-started/data-proofs/ 58 | proof = new ListProof(proof, Hash) 59 | // The checked entries are stored in the corresponding field. The internal structure 60 | // of the field is explored slightly later. 61 | expect(proof.entries.length).to.equal(3) 62 | // Besides entries, the proof checks the length of the underlying list. 63 | expect(proof.length).to.equal(20) 64 | 65 | // Output elements asserted by the proof to exist in the underlying index. 66 | // As you see, each entry contains an `index` in the underlying list, and a `value`. 67 | console.log('\nEntries:') 68 | for (const { index, value } of proof.entries) { 69 | console.log(`list[${index}] = ${value}`) 70 | } 71 | 72 | // The Merkle root of the proof is usually propagated further to the trust anchor 73 | // (usually, a `state_hash` field in the block header). Here, we just compare 74 | // it to the reference value for simplicity. 75 | expect(proof.merkleRoot).to.equal('7b61e77d992c448939e21a463f00b1353565b31793af5bd03f044d4568aa1c0d') 76 | -------------------------------------------------------------------------------- /proto/exonum/blockchain.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Exonum Team 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package exonum; 18 | 19 | option java_package = "com.exonum.messages.core"; 20 | 21 | import "exonum/crypto/types.proto"; 22 | import "exonum/key_value_sequence.proto"; 23 | 24 | // Extensible set of additional headers, represented 25 | // as a sequence of key-value pairs. 26 | message AdditionalHeaders { 27 | KeyValueSequence headers = 1; 28 | } 29 | 30 | message Block { 31 | uint32 proposer_id = 1; 32 | uint64 height = 2; 33 | uint32 tx_count = 3; 34 | exonum.crypto.Hash prev_hash = 4; 35 | exonum.crypto.Hash tx_hash = 5; 36 | exonum.crypto.Hash state_hash = 6; 37 | exonum.crypto.Hash error_hash = 7; 38 | AdditionalHeaders additional_headers = 8; 39 | } 40 | 41 | message TxLocation { 42 | uint64 block_height = 1; 43 | uint32 position_in_block = 2; 44 | } 45 | 46 | // Location of an isolated call within a block. 47 | message CallInBlock { 48 | oneof call { 49 | // Call of a transaction within the block. The value is the zero-based 50 | // transaction index. 51 | uint32 transaction = 1; 52 | // Call of `before_transactions` hook in a service. The value is 53 | // the service identifier. 54 | uint32 before_transactions = 2; 55 | // Call of `after_transactions` hook in a service. The value is 56 | // the service identifier. 57 | uint32 after_transactions = 3; 58 | } 59 | } 60 | 61 | // Consensus configuration parameters 62 | 63 | // Public keys of a validator. 64 | message ValidatorKeys { 65 | // Consensus key is used for messages related to the consensus algorithm. 66 | exonum.crypto.PublicKey consensus_key = 1; 67 | // Service key is used for services, for example, the configuration 68 | // updater service, the anchoring service, etc. 69 | exonum.crypto.PublicKey service_key = 2; 70 | } 71 | 72 | // Consensus algorithm parameters. 73 | message Config { 74 | // List of validators public keys. 75 | repeated ValidatorKeys validator_keys = 1; 76 | // Interval between first two rounds. 77 | uint64 first_round_timeout = 2; 78 | // Period of sending a Status message. 79 | uint64 status_timeout = 3; 80 | // Peer exchange timeout. 81 | uint64 peers_timeout = 4; 82 | // Maximum number of transactions per block. 83 | uint32 txs_block_limit = 5; 84 | // Maximum message length (in bytes). 85 | uint32 max_message_len = 6; 86 | // Minimal propose timeout. 87 | uint64 min_propose_timeout = 7; 88 | // Maximal propose timeout. 89 | uint64 max_propose_timeout = 8; 90 | // Amount of transactions in pool to start use `min_propose_timeout`. 91 | uint32 propose_timeout_threshold = 9; 92 | } 93 | -------------------------------------------------------------------------------- /examples/transactions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example how to create, sign and send transactions in client apps. 3 | */ 4 | 5 | const Exonum = require('..') 6 | const { Transaction, keyPair } = Exonum 7 | const { expect } = require('chai') 8 | const { Type, Field } = require('protobufjs/light') 9 | require('regenerator-runtime') 10 | 11 | // Declare the transaction payload layout we will use in this example. 12 | // It is dictated by the layout defined in the integration tests. 13 | // 14 | // In a more advanced setup, you can use `pbjs` binary from `protobufjs` package 15 | // to compile a JS module from the Protobuf declarations 16 | // as described in the [light client tutorial]. 17 | // 18 | // [light client tutorial]: https://exonum.com/doc/version/latest/get-started/light-client/ 19 | const MockPayloadSchema = new Type('MockPayload') 20 | .add(new Field('metadata', 1, 'string')) 21 | .add(new Field('retries', 2, 'uint32')) 22 | 23 | // First, we define a transaction type. 24 | const Mock = new Transaction({ 25 | // Schema of the transaction payload. 26 | schema: MockPayloadSchema, 27 | // Identifier of the service we will send transactions to. `serviceId` is assigned 28 | // during service instantiation; you can find it out via an endpoint in the system API. 29 | // 30 | // In this example, we keep `serviceId` constant for simplicity. 31 | serviceId: 100, 32 | // Method identifier within the service interface. Since there is no language-independent 33 | // interface description in Exonum yet, you will need to find out the ID by researching 34 | // service docs / source code. 35 | methodId: 1 36 | }) 37 | 38 | async function main () { 39 | // We generate a keypair to sign the transactions with. 40 | const keys = keyPair() 41 | console.log('\nKeypair:') 42 | console.log(keys) 43 | 44 | // Let's generate a transaction payload. It should adhere to the format expected 45 | // by `protobufjs`. 46 | let payload = { 47 | metadata: 'Our metadata', 48 | retries: 0 49 | } 50 | 51 | // We can sign the payload with the generated keys. 52 | let signed = Mock.create(payload, keys) 53 | console.log('\nSigned transaction:') 54 | console.log(signed) 55 | 56 | // Send a transaction to the node. For this example, we will use the integration server 57 | // which mocks the interface of a node. The server can be launched with 58 | // 59 | // cd integration-tests 60 | // cargo run & 61 | const URL = 'http://localhost:8000/mock/transactions' 62 | 63 | let txHash = await Exonum.send(URL, signed.serialize()) 64 | expect(txHash).to.equal(signed.hash()) 65 | console.log('Sent transaction!') 66 | 67 | // `send` allows to specify custom number of retries and the interval between retries: 68 | const retries = 5 69 | const retryInterval = 100 // in milliseconds 70 | 71 | payload = { 72 | metadata: 'Other metadata', 73 | retries 74 | } 75 | signed = Mock.create(payload, keys) 76 | txHash = await Exonum.send(URL, signed.serialize(), retries, retryInterval) 77 | expect(txHash).to.equal(signed.hash()) 78 | console.log('Sent transaction with custom retries!') 79 | 80 | // `sendQueue` function can be used to send one transaction after another, 81 | // waiting for transaction acceptance. 82 | const transactions = ['foo', 'bar', 'baz'] 83 | .map(metadata => Mock.create({ metadata, retries: 1 }, keys)) 84 | const expectedHashes = transactions.map(tx => tx.hash()) 85 | const hashes = await Exonum.sendQueue(URL, transactions.map(tx => tx.serialize())) 86 | expect(hashes).to.deep.equal(expectedHashes) 87 | console.log('Sent queued transactions!') 88 | } 89 | 90 | main().catch(e => { 91 | console.error(e.toString()) 92 | console.info('If you are getting a connection error, make sure you have launched ' + 93 | 'the integration server. See the script source for more details') 94 | }) 95 | -------------------------------------------------------------------------------- /src/types/message.js: -------------------------------------------------------------------------------- 1 | import nacl from 'tweetnacl' 2 | import * as crypto from '../crypto' 3 | import { cleanZeroValuedFields } from '../helpers' 4 | 5 | import * as protobuf from '../../proto/protocol' 6 | import { hexadecimalToUint8Array, uint8ArrayToHexadecimal } from './convert' 7 | const { CoreMessage, SignedMessage } = protobuf.exonum 8 | 9 | export class Verified { 10 | constructor (schema, payload, author, signature) { 11 | this.schema = schema 12 | this.payload = payload 13 | this.author = author 14 | this.signature = signature 15 | this.bytes = SignedMessage.encode({ 16 | payload: schema.encode(payload).finish(), 17 | author: { data: hexadecimalToUint8Array(author) }, 18 | signature: { data: hexadecimalToUint8Array(signature) } 19 | }).finish() 20 | } 21 | 22 | static sign (schema, payload, { publicKey, secretKey }) { 23 | const signingKey = hexadecimalToUint8Array(secretKey) 24 | const rawSignature = nacl.sign.detached(schema.encode(payload).finish(), signingKey) 25 | const signature = uint8ArrayToHexadecimal(rawSignature) 26 | return new this(schema, payload, publicKey, signature) 27 | } 28 | 29 | static deserialize (schema, bytes) { 30 | const { payload, author: rawAuthor, signature: rawSignature } = SignedMessage.decode(bytes) 31 | if (!nacl.sign.detached.verify(payload, rawSignature.data, rawAuthor.data)) { 32 | return null 33 | } else { 34 | const decoded = schema.decode(payload) 35 | const author = uint8ArrayToHexadecimal(rawAuthor.data) 36 | const signature = uint8ArrayToHexadecimal(rawSignature.data) 37 | return new this(schema, decoded, author, signature) 38 | } 39 | } 40 | 41 | serialize () { 42 | return this.bytes 43 | } 44 | 45 | /** 46 | * Gets the SHA-256 digest of the message. 47 | * @returns {string} 48 | */ 49 | hash () { 50 | return crypto.hash(this.bytes) 51 | } 52 | } 53 | 54 | /** 55 | * @constructor 56 | * @param {Object} type 57 | */ 58 | export class Transaction { 59 | constructor ({ schema, serviceId, methodId }) { 60 | this.serviceId = serviceId 61 | this.methodId = methodId 62 | this.schema = schema 63 | } 64 | 65 | /** 66 | * Creates a signature transaction. 67 | * 68 | * @param {Object} payload 69 | * transaction payload 70 | * @param {Uint8Array | {publicKey: string, secretKey: string}} authorOrKeypair 71 | * author or keypair 72 | * @param {Uint8Array?} signature 73 | * transaction signature 74 | * @returns {Verified} 75 | * signature transaction message 76 | */ 77 | create (payload, authorOrKeypair, signature) { 78 | const fullPayload = this._serializePayload(payload) 79 | if (signature === undefined) { 80 | return Verified.sign(CoreMessage, fullPayload, authorOrKeypair) 81 | } else { 82 | return new Verified(CoreMessage, fullPayload, authorOrKeypair, signature) 83 | } 84 | } 85 | 86 | _serializePayload (payload) { 87 | const args = this.schema.encode(cleanZeroValuedFields(payload, {})).finish() 88 | const transaction = { 89 | call_info: { 90 | instance_id: this.serviceId, 91 | method_id: this.methodId 92 | }, 93 | arguments: args 94 | } 95 | return { any_tx: transaction } 96 | } 97 | 98 | serialize (payload) { 99 | return CoreMessage.encode(this._serializePayload(payload)).finish() 100 | } 101 | 102 | deserialize (bytes) { 103 | const verified = Verified.deserialize(CoreMessage, bytes) 104 | if (!verified) { 105 | return null 106 | } 107 | const payload = verified.payload.any_tx 108 | if (!payload) { 109 | return null 110 | } 111 | 112 | if ( 113 | payload.call_info.instance_id !== this.serviceId || 114 | payload.call_info.method_id !== this.methodId 115 | ) { 116 | return null 117 | } 118 | verified.payload = this.schema.decode(payload.arguments) 119 | return verified 120 | } 121 | } 122 | 123 | /** 124 | * Check if passed object is of type Transaction 125 | * @param type 126 | * @returns {boolean} 127 | */ 128 | export function isTransaction (type) { 129 | return type instanceof Transaction 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exonum-client", 3 | "version": "0.18.4", 4 | "description": "Light Client for Exonum Blockchain", 5 | "main": "./lib/index.js", 6 | "engines": { 7 | "node": ">=8" 8 | }, 9 | "directories": { 10 | "lib": "lib", 11 | "test": "test" 12 | }, 13 | "files": [ 14 | "dist", 15 | "lib", 16 | "proto" 17 | ], 18 | "dependencies": { 19 | "axios": "^0.21.1", 20 | "big-integer": "^1.6.48", 21 | "binary-search": "^1.3.6", 22 | "long": "^4.0.0", 23 | "protobufjs": "6.8.8", 24 | "sha.js": "^2.4.11", 25 | "tweetnacl": "^1.0.3" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.8.4", 29 | "@babel/core": "^7.9.6", 30 | "@babel/preset-env": "^7.9.6", 31 | "@babel/register": "^7.9.0", 32 | "axios-mock-adapter": "^1.18.1", 33 | "babel-eslint": "^10.1.0", 34 | "babel-plugin-istanbul": "^6.0.0", 35 | "babelify": "^10.0.0", 36 | "chai": "^4.2.0", 37 | "chai-as-promised": "^7.1.1", 38 | "coveralls": "^3.1.0", 39 | "cross-env": "^7.0.2", 40 | "deep-eql": "^4.0.0", 41 | "dirty-chai": "^2.0.1", 42 | "eslint-config-standard": "^14.1.1", 43 | "eslint-plugin-import": "^2.20.2", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^4.2.1", 46 | "eslint-plugin-standard": "^4.0.1", 47 | "grunt": "^1.1.0", 48 | "grunt-babel": "^8.0.0", 49 | "grunt-browserify": "^5.3.0", 50 | "grunt-cli": "^1.3.2", 51 | "grunt-contrib-clean": "^2.0.0", 52 | "grunt-contrib-uglify": "^4.0.1", 53 | "grunt-eslint": "^22.0.0", 54 | "grunt-mocha-test": "^0.13.3", 55 | "grunt-string-replace": "^1.3.1", 56 | "json-loader": "^0.5.7", 57 | "load-grunt-tasks": "^5.1.0", 58 | "mocha": "^7.1.2", 59 | "mocha-lcov-reporter": "^1.3.0", 60 | "node-fetch": "^2.6.1", 61 | "nyc": "^15.0.1", 62 | "regenerator-runtime": "^0.13.5", 63 | "uuid": "^7.0.3" 64 | }, 65 | "scripts": { 66 | "test": "cross-env BABEL_ENV=test grunt test", 67 | "proto": "pbjs --keep-case -t static-module -p proto proto/exonum/*.proto proto/exonum/**/*.proto -o ./proto/protocol.js", 68 | "proto:test": "pbjs --keep-case -t static-module -r tests -p proto test/sources/proto/*.proto -o ./test/sources/proto/stubs.js", 69 | "proto:integration-tests": "pbjs --keep-case -t static-module -r tests -p proto integration-tests/src/proto/*.proto -o ./integration-tests/src/proto/stubs.js", 70 | "integration:build": "cargo build --manifest-path integration-tests/Cargo.toml", 71 | "integration": "npm run proto:integration-tests && cross-env BABEL_ENV=test mocha -r @babel/register integration-tests/test.js", 72 | "preintegration:unix": "npm run integration:build && npm run postintegration:unix && cargo run --manifest-path integration-tests/Cargo.toml & sleep 10", 73 | "integration:unix": "npm run proto:integration-tests && cross-env BABEL_ENV=test mocha -r @babel/register integration-tests/test.js", 74 | "postintegration:unix": "lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null | awk '{ if ($9 ~ /:8000$/) { print $2 } }' | xargs -r kill -KILL", 75 | "coveralls": "cross-env NODE_ENV=test nyc mocha ./test/sources/*.js && cat ./coverage/lcov.info | coveralls", 76 | "prepare": "npm run proto && npm run proto:test && grunt compile", 77 | "lint": "eslint ./src ./test ./examples", 78 | "lint:fix": "npm run lint -- --fix", 79 | "git-publish": "npm run prepare && . ./git-publish.sh" 80 | }, 81 | "repository": { 82 | "type": "git", 83 | "url": "https://github.com/exonum/exonum-client.git" 84 | }, 85 | "author": "The Exonum Team ", 86 | "license": "Apache-2.0", 87 | "bugs": { 88 | "url": "https://github.com/exonum/exonum-client/issues" 89 | }, 90 | "nyc": { 91 | "require": [ 92 | "@babel/register" 93 | ], 94 | "reporter": [ 95 | "lcov" 96 | ], 97 | "sourceMap": false, 98 | "instrument": false, 99 | "exclude": [ 100 | "proto/*.js", 101 | "test/sources/proto/*.js" 102 | ] 103 | }, 104 | "homepage": "https://github.com/exonum/exonum-client#readme", 105 | "keywords": [ 106 | "exonum", 107 | "blockchain", 108 | "transactions", 109 | "cryptography", 110 | "ed25519", 111 | "nacl", 112 | "sha256", 113 | "merkle tree" 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /test/sources/readme.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | const $protobuf = require('protobufjs/light') 5 | const { expect } = require('chai') 6 | const Exonum = require('../../src') 7 | 8 | const Type = $protobuf.Type 9 | const Field = $protobuf.Field 10 | 11 | describe('Examples from README.md', function () { 12 | describe('Custom type section', function () { 13 | const Message = new Type('CustomMessage') 14 | Message.add(new Field('balance', 1, 'uint32')) 15 | Message.add(new Field('name', 2, 'string')) 16 | const User = Exonum.newType(Message) 17 | const data = { 18 | balance: 100, 19 | name: 'John Doe' 20 | } 21 | const keyPair = { 22 | publicKey: 'fa7f9ee43aff70c879f80fa7fd15955c18b98c72310b09e7818310325050cf7a', 23 | secretKey: '978e3321bd6331d56e5f4c2bdb95bf471e95a77a6839e68d4241e7b0932ebe2bfa7f9ee43aff70c879f80fa7fd15955c18b98c72310b09e7818310325050cf7a' 24 | } 25 | const hash = '9786347be1ab7e8f3d68a49ef8a995a4decb31103c53565a108170dec4c1c2fa' 26 | const buffer = [8, 100, 18, 8, 74, 111, 104, 110, 32, 68, 111, 101] 27 | const signature = 'a4cf7c457e3f4d54ef0c87900e7c860d2faa17a8dccbaafa573a3a960cda3f6627911088138526d9d7e46feba471e6bc7b93262349a5ed18262cbc39c8a47b04' 28 | 29 | it('should get hash of custom type', function () { 30 | expect(Exonum.hash(data, User)).to.equal(hash) 31 | }) 32 | 33 | it('should get hash of byte array', function () { 34 | expect(Exonum.hash(buffer)).to.equal(hash) 35 | }) 36 | 37 | it('should sign custom type', function () { 38 | expect(Exonum.sign(keyPair.secretKey, data, User)).to.equal(signature) 39 | }) 40 | 41 | it('should verify custom type signature', function () { 42 | expect(Exonum.verifySignature(signature, keyPair.publicKey, data, User)).to.be.true 43 | }) 44 | }) 45 | 46 | describe('Transaction section', function () { 47 | const Transaction = new Type('CustomMessage') 48 | Transaction.add(new Field('to', 1, 'string')) 49 | Transaction.add(new Field('amount', 2, 'uint32')) 50 | const sendFunds = new Exonum.Transaction({ 51 | serviceId: 130, 52 | methodId: 0, 53 | schema: Transaction 54 | }) 55 | 56 | it('should work', () => { 57 | const keyPair = { 58 | publicKey: 'fa7f9ee43aff70c879f80fa7fd15955c18b98c72310b09e7818310325050cf7a', 59 | secretKey: '978e3321bd6331d56e5f4c2bdb95bf471e95a77a6839e68d4241e7b0932ebe2bfa7f9ee43aff70c879f80fa7fd15955c18b98c72310b09e7818310325050cf7a' 60 | } 61 | const data = { 62 | to: 'Adam', 63 | amount: 50 64 | } 65 | const signed = sendFunds.create(data, keyPair) 66 | expect(signed.payload.any_tx.call_info.instance_id).to.equal(sendFunds.serviceId) 67 | expect(signed.payload.any_tx.arguments).to.deep.equal(Transaction.encode(data).finish()) 68 | expect(signed.author).to.equal(keyPair.publicKey) 69 | 70 | const deserialized = sendFunds.deserialize(signed.serialize()) 71 | expect(deserialized.payload.to).to.equal('Adam') 72 | expect(deserialized.payload.amount).to.equal(50) 73 | }) 74 | }) 75 | 76 | describe('Merkle tree verifying example', function () { 77 | it('should verify a Merkle tree', function () { 78 | const rootHash = '92cb0ac4e56995b3dfe002be166dd18d2965535d9ad812b055b953c9b3d35456' 79 | const proofData = { 80 | proof: [ 81 | { height: 1, index: 1, hash: '8dc134fc6f0e0b7fccd32bb0f6090e68600710234c1cb318261d5e78be659bd1' }, 82 | { height: 2, index: 1, hash: '3b45eedc6952cbec6a8b769c3e50f96d1d059853bbedb7c26f8621243b308e9a' } 83 | ], 84 | entries: [ 85 | [0, { 86 | firstName: 'John', 87 | lastName: 'Doe', 88 | age: 28, 89 | balance: 2500 90 | }] 91 | ], 92 | length: 3 93 | } 94 | 95 | const User = Exonum.newType(new Type('CustomMessage') 96 | .add(new Field('firstName', 1, 'string')) 97 | .add(new Field('lastName', 2, 'string')) 98 | .add(new Field('age', 3, 'uint32')) 99 | .add(new Field('balance', 4, 'uint32'))) 100 | 101 | const listProof = new Exonum.ListProof(proofData, User) 102 | expect(listProof.merkleRoot).to.equal(rootHash) 103 | }) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /test/sources/types.js: -------------------------------------------------------------------------------- 1 | // /* eslint-env node, mocha */ 2 | // /* eslint-disable no-unused-expressions */ 3 | import { Field, Root, Type } from 'protobufjs/light' 4 | 5 | const root = new Root() 6 | 7 | const expect = require('chai').expect 8 | const Exonum = require('../../src') 9 | 10 | describe('Check protobuf serialization', function () { 11 | describe('Process Bytes', function () { 12 | it('should create data and return array of 8-bit integers when the value is valid for bytes', function () { 13 | const Type1Protobuf = new Type('Type1').add(new Field('name', 1, 'bytes')) 14 | Type1Protobuf.add(new Field('type', 2, 'string')) 15 | root.define('Type1Protobuf').add(Type1Protobuf) 16 | const Type1 = Exonum.newType(Type1Protobuf) 17 | const data = { 18 | data: { 19 | name: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 20 | type: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 21 | }, 22 | serialized: Uint8Array.from([ 23 | 10, 48, 127, 159, 58, 225, 166, 250, 107, 150, 182, 215, 221, 58, 24 | 235, 166, 248, 237, 206, 187, 233, 183, 31, 215, 150, 181, 127, 103, 244, 239, 189, 55, 115, 150, 220, 25 | 105, 246, 249, 239, 143, 90, 107, 189, 249, 113, 239, 27, 237, 205, 250, 18, 64, 102, 53, 56, 54, 52, 97, 26 | 98, 54, 97, 53, 97, 50, 49, 57, 48, 54, 54, 54, 98, 52, 55, 99, 54, 55, 54, 98, 99, 102, 49, 53, 97, 49, 102, 27 | 50, 102, 48, 55, 55, 48, 51, 99, 53, 98, 99, 97, 102, 98, 53, 55, 52, 57, 97, 97, 55, 51, 53, 99, 101, 56, 98, 28 | 55, 99, 51, 54 29 | ]) 30 | } 31 | const buffer = Type1.serialize(data.data) 32 | expect(buffer).to.deep.equal(data.serialized) 33 | }) 34 | 35 | it('should throw error when the value is invalid string', function () { 36 | const Type2Protobuf = new Type('Type2').add(new Field('name', 1, 'bytes')) 37 | Type2Protobuf.add(new Field('type', 2, 'string')) 38 | root.define('Type2Protobuf').add(Type2Protobuf) 39 | const Type2 = Exonum.newType(Type2Protobuf) 40 | const data = { 41 | data: { 42 | name: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 43 | type: 1 44 | }, 45 | serialized: [ 46 | 10, 48, 127, 159, 58, 225, 166, 250, 107, 150, 182, 215, 221, 58, 47 | 235, 166, 248, 237, 206, 187, 233, 183, 31, 215, 150, 181, 127, 103, 244, 239, 189, 55, 115, 150, 220, 48 | 105, 246, 249, 239, 143, 90, 107, 189, 249, 113, 239, 27, 237, 205, 250, 18, 64, 102, 53, 56, 54, 52, 97, 49 | 98, 54, 97, 53, 97, 50, 49, 57, 48, 54, 54, 54, 98, 52, 55, 99, 54, 55, 54, 98, 99, 102, 49, 53, 97, 49, 102, 50 | 50, 102, 48, 55, 55, 48, 51, 99, 53, 98, 99, 97, 102, 98, 53, 55, 52, 57, 97, 97, 55, 51, 53, 99, 101, 56, 98, 51 | 55, 99, 51, 54 52 | ] 53 | } 54 | expect(() => Type2.serialize(data.data)) 55 | .to.throw(TypeError) 56 | }) 57 | }) 58 | 59 | describe('Process int', function () { 60 | it('should create data and return array of 8-bit integers when the value is valid for int 32', function () { 61 | const IntType1Protobuf = new Type('IntType1').add(new Field('amount', 1, 'int32')) 62 | root.define('IntType1Protobuf').add(IntType1Protobuf) 63 | const IntType1 = Exonum.newType(IntType1Protobuf) 64 | const data = { 65 | data: { 66 | amount: 1 67 | }, 68 | serialized: Uint8Array.from([8, 1]) 69 | } 70 | const buffer = IntType1.serialize(data.data) 71 | expect(buffer).to.deep.equal(data.serialized) 72 | }) 73 | 74 | it('should create data and return array of 8-bit integers when the value is valid for int 32 in string', function () { 75 | const IntType2Protobuf = new Type('IntType2').add(new Field('amount', 1, 'int32')) 76 | root.define('IntType2Protobuf').add(IntType2Protobuf) 77 | const IntType2 = Exonum.newType(IntType2Protobuf) 78 | const data = { 79 | data: { 80 | amount: '34506' 81 | }, 82 | serialized: Uint8Array.from([8, 202, 141, 2]) 83 | } 84 | expect(IntType2.serialize(data.data)).to.deep.equal(data.serialized) 85 | }) 86 | 87 | it('should create data and return array of 8-bit integers when the value is valid for negative int 32', function () { 88 | const IntType3Protobuf = new Type('IntType3').add(new Field('amount', 1, 'int32')) 89 | root.define('IntType3Protobuf').add(IntType3Protobuf) 90 | const IntType3 = Exonum.newType(IntType3Protobuf) 91 | const data = { 92 | data: { 93 | amount: -1 94 | }, 95 | serialized: Uint8Array.from([8, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1]) 96 | } 97 | expect(IntType3.serialize(data.data)).to.deep.equal(data.serialized) 98 | }) 99 | 100 | it('should create data and return array of 8-bit integers with lenght and 0 when the value not found', function () { 101 | const IntType4Protobuf = new Type('IntType4').add(new Field('amount', 1, 'int32')) 102 | root.define('IntType4Protobuf').add(IntType4Protobuf) 103 | const IntType4 = Exonum.newType(IntType4Protobuf) 104 | const data = { 105 | data: { 106 | amount: 'dsad' 107 | }, 108 | serialized: Uint8Array.from([8, 0]) 109 | } 110 | expect(IntType4.serialize(data.data)).to.deep.equal(data.serialized) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /src/crypto/index.js: -------------------------------------------------------------------------------- 1 | import bigInt from 'big-integer' 2 | import sha from 'sha.js' 3 | import nacl from 'tweetnacl' 4 | import { isType } from '../types/generic' 5 | import { isTransaction } from '../types/message' 6 | import * as validate from '../types/validate' 7 | import * as convert from '../types/convert' 8 | import * as protobuf from '../../proto/protocol' 9 | 10 | const { Caller } = protobuf.exonum.runtime 11 | 12 | /** 13 | * Byte size of a hash. 14 | * @type {number} 15 | */ 16 | export const HASH_LENGTH = 32 17 | 18 | /** 19 | * Get SHA256 hash 20 | * @param {Object|Array|Uint8Array} data - object of NewType type or array of 8-bit integers 21 | * @param {Type|Transaction} [type] - optional, used only if data of {Object} type is passed 22 | * @return {string} 23 | */ 24 | export function hash (data, type) { 25 | let buffer 26 | 27 | if (isType(type) || isTransaction(type)) { 28 | buffer = type.serialize(data) 29 | } else { 30 | if (type !== undefined) { 31 | throw new TypeError('Wrong type of data.') 32 | } 33 | 34 | if (data instanceof Uint8Array) { 35 | buffer = data 36 | } else { 37 | if (!Array.isArray(data)) { 38 | throw new TypeError('Invalid data parameter.') 39 | } 40 | buffer = new Uint8Array(data) 41 | } 42 | } 43 | 44 | return sha('sha256').update(buffer, 'utf8').digest('hex') 45 | } 46 | 47 | /** 48 | * Get ED25519 signature 49 | * @param {string} secretKey 50 | * @param {Object|Array} data - object of NewType type or array of 8-bit integers 51 | * @param {Type|Transaction} [type] - optional, used only if data of {Object} type is passed 52 | * @return {string} 53 | */ 54 | export function sign (secretKey, data, type) { 55 | let buffer 56 | 57 | if (!validate.validateHexadecimal(secretKey, 64)) { 58 | throw new TypeError('secretKey of wrong type is passed. Hexadecimal expected.') 59 | } 60 | 61 | const secretKeyUint8Array = convert.hexadecimalToUint8Array(secretKey) 62 | 63 | if (isType(type) || isTransaction(type)) { 64 | buffer = type.serialize(data) 65 | } else { 66 | if (type !== undefined) { 67 | throw new TypeError('Wrong type of data.') 68 | } 69 | 70 | if (data instanceof Uint8Array) { 71 | buffer = data 72 | } else { 73 | if (!Array.isArray(data)) { 74 | throw new TypeError('Invalid data parameter.') 75 | } 76 | buffer = new Uint8Array(data) 77 | } 78 | } 79 | const signature = nacl.sign.detached(buffer, secretKeyUint8Array) 80 | 81 | return convert.uint8ArrayToHexadecimal(signature) 82 | } 83 | 84 | /** 85 | * Verifies ED25519 signature 86 | * @param {string} signature 87 | * @param {string} publicKey 88 | * @param {Object|Array} data - object of NewType type or array of 8-bit integers 89 | * @param {Type|Transaction} [type] - optional, used only if data of {Object} type is passed 90 | * @return {boolean} 91 | */ 92 | export function verifySignature (signature, publicKey, data, type) { 93 | let buffer 94 | 95 | if (!validate.validateHexadecimal(signature, 64)) { 96 | throw new TypeError('Signature of wrong type is passed. Hexadecimal expected.') 97 | } 98 | 99 | const signatureUint8Array = convert.hexadecimalToUint8Array(signature) 100 | 101 | if (!validate.validateHexadecimal(publicKey)) { 102 | throw new TypeError('publicKey of wrong type is passed. Hexadecimal expected.') 103 | } 104 | 105 | const publicKeyUint8Array = convert.hexadecimalToUint8Array(publicKey) 106 | 107 | if (isType(type) || isTransaction(type)) { 108 | buffer = type.schema.encode(data).finish() 109 | } else if (type === undefined) { 110 | if (data instanceof Uint8Array) { 111 | buffer = data 112 | } else if (Array.isArray(data)) { 113 | buffer = new Uint8Array(data) 114 | } 115 | } else { 116 | throw new TypeError('Wrong type of data.') 117 | } 118 | 119 | return nacl.sign.detached.verify(buffer, signatureUint8Array, publicKeyUint8Array) 120 | } 121 | 122 | /** 123 | * Generate random pair of publicKey and secretKey 124 | * @return {Object} 125 | * publicKey {string} 126 | * secretKey {string} 127 | */ 128 | export function keyPair () { 129 | const pair = nacl.sign.keyPair() 130 | const publicKey = convert.uint8ArrayToHexadecimal(pair.publicKey) 131 | const secretKey = convert.uint8ArrayToHexadecimal(pair.secretKey) 132 | 133 | return { 134 | publicKey, 135 | secretKey 136 | } 137 | } 138 | 139 | /** 140 | * Returns a new signing key pair generated deterministically from a 32-byte seed 141 | * @return {Object} 142 | * publicKey {string} 143 | * secretKey {string} 144 | */ 145 | export function fromSeed (seed) { 146 | const pair = nacl.sign.keyPair.fromSeed(seed) 147 | const publicKey = convert.uint8ArrayToHexadecimal(pair.publicKey) 148 | const secretKey = convert.uint8ArrayToHexadecimal(pair.secretKey) 149 | 150 | return { 151 | publicKey, 152 | secretKey 153 | } 154 | } 155 | 156 | /** 157 | * Gets a random number of cryptographic quality. 158 | * @returns {string} 159 | */ 160 | export function randomUint64 () { 161 | const buffer = nacl.randomBytes(8) 162 | return bigInt.fromArray(Array.from(buffer), 256).toString() 163 | } 164 | 165 | /** 166 | * Converts a public key into a caller address, which is a uniform presentation 167 | * of any transaction authorization supported by Exonum. Addresses may be used 168 | * in `MapProof`s. 169 | * 170 | * @param {string} publicKey 171 | * @returns {string} 172 | */ 173 | export function publicKeyToAddress (publicKey) { 174 | const keyBytes = { data: convert.hexadecimalToUint8Array(publicKey) } 175 | const caller = Caller.encode({ transaction_author: keyBytes }).finish() 176 | return hash(caller) 177 | } 178 | -------------------------------------------------------------------------------- /src/types/hexadecimal.js: -------------------------------------------------------------------------------- 1 | import * as validate from './validate' 2 | 3 | export const PUBLIC_KEY_LENGTH = 32 4 | const HASH_LENGTH = 32 5 | 6 | /** 7 | * Encoder 8 | * 9 | * @param {string} str string to encode 10 | * @param {Array} buffer buffer to place result to 11 | * @param {number} from position to write from 12 | * @returns {Array} modified buffer 13 | * @private 14 | */ 15 | function insertHexadecimalToByteArray (str, buffer, from) { 16 | for (let i = 0; i < str.length; i += 2) { 17 | buffer[from] = parseInt(str.substr(i, 2), 16) 18 | from++ 19 | } 20 | return buffer 21 | } 22 | 23 | /** 24 | * Validator wrapper 25 | * 26 | * @param {string} name structure name 27 | * @param {number} size value size in bytes 28 | * @param {string} value value representation 29 | * @returns {string} value if validation passes 30 | * @throws {TypeError} in case of validation break 31 | * @private 32 | */ 33 | function validateHexadecimal (name, size, value) { 34 | if (!validate.validateHexadecimal(value, size)) { 35 | throw new TypeError(`${name} of wrong type is passed: ${value}`) 36 | } 37 | 38 | return value 39 | } 40 | 41 | /** 42 | * Factory for building Hex Types 43 | * 44 | * @param {function(value, buffer, from)} serizalizer function accepting value, buffer, position and returns modified buffer 45 | * @param {number} size type size in bytes 46 | * @param {string} name type name to distinguish between types 47 | * @returns {Object} hex type 48 | */ 49 | function hexTypeFactory (serizalizer, size, name) { 50 | return Object.defineProperties({}, { 51 | size: { 52 | get: () => () => size, 53 | enumerable: true 54 | }, 55 | name: { 56 | get: () => name, 57 | enumerable: true 58 | }, 59 | serialize: { 60 | get: () => serizalizer 61 | } 62 | }) 63 | } 64 | 65 | /** 66 | * Common serializer 67 | * 68 | * @param {function(name, size, value)} validator hexadecimal validator 69 | * @param {function(value, buffer, from)} encoder function accepting value, buffer, position and returns modified buffer 70 | * @returns {function(value, buffer, from)} encoder wrapper 71 | * @throws {TypeError} in case of validation break 72 | * @private 73 | */ 74 | function serializer (encoder, validator) { 75 | return (value, buffer, from) => encoder(validator(value), buffer, from) 76 | } 77 | 78 | /** 79 | * Uuid type factory 80 | * 81 | * @param {function(name, size, value)} validator hexadecimal validator 82 | * @param {function(validator, value, buffer, from)} encoder function accepting validator, value, buffer, position and returns modified buffer 83 | * @param {function(serizalizer, size, name)} factory type builder factory 84 | * @returns {Object} hex type 85 | * @private 86 | */ 87 | function Uuid (validator, serializer, factory) { 88 | const size = 16 89 | const name = 'Uuid' 90 | 91 | function cleaner (value) { 92 | return String(value).replace(/-/g, '') 93 | } 94 | 95 | validator = validator.bind(null, name, size) 96 | serializer = serializer((value) => validator(cleaner(value))) 97 | 98 | return factory(serializer, size, name) 99 | } 100 | 101 | /** 102 | * Hash type factory 103 | * 104 | * @param {function(name, size, value)} validator hexadecimal validator 105 | * @param {function(validator, value, buffer, from)} encoder function accepting validator, value, buffer, position and returns modified buffer 106 | * @param {function(serizalizer, size, name)} factory type builder factory 107 | * @returns {Object} hex type 108 | * @private 109 | */ 110 | function Hash (validator, serializer, factory) { 111 | const size = HASH_LENGTH 112 | const name = 'Hash' 113 | 114 | validator = validator.bind(null, name, size) 115 | serializer = serializer(validator) 116 | 117 | const hasher = function (value) { 118 | return validator(value) && value 119 | } 120 | 121 | return Object.defineProperty(factory(serializer, size, name), 122 | 'hash', 123 | { 124 | value: hasher 125 | }) 126 | } 127 | 128 | /** 129 | * Digest type factory 130 | * 131 | * @param {function(name, size, value)} validator hexadecimal validator 132 | * @param {function(validator, value, buffer, from)} encoder function accepting validator, value, buffer, position and returns modified buffer 133 | * @param {function(serizalizer, size, name)} factory type builder factory 134 | * @returns {Object} hex type 135 | * @private 136 | */ 137 | function Digest (validator, serializer, factory) { 138 | const size = 64 139 | const name = 'Digest' 140 | 141 | validator = validator.bind(null, name, size) 142 | serializer = serializer(validator) 143 | 144 | return factory(serializer, size, name) 145 | } 146 | 147 | /** 148 | * PublicKey type factory 149 | * 150 | * @param {function(name, size, value)} validator hexadecimal validator 151 | * @param {function(validator, value, buffer, from)} encoder function accepting validator, value, buffer, position and returns modified buffer 152 | * @param {function(serizalizer, size, name)} factory type builder factory 153 | * @returns {Object} hex type 154 | * @private 155 | */ 156 | function PublicKey (validator, serializer, factory) { 157 | const size = PUBLIC_KEY_LENGTH 158 | const name = 'PublicKey' 159 | 160 | validator = validator.bind(null, name, size) 161 | serializer = serializer(validator) 162 | 163 | return factory(serializer, size, name) 164 | } 165 | 166 | const baseSerializer = serializer.bind(null, insertHexadecimalToByteArray) 167 | 168 | const uuid = Uuid(validateHexadecimal, baseSerializer, hexTypeFactory) 169 | const hash = Hash(validateHexadecimal, baseSerializer, hexTypeFactory) 170 | const digest = Digest(validateHexadecimal, baseSerializer, hexTypeFactory) 171 | const publicKey = PublicKey(validateHexadecimal, baseSerializer, hexTypeFactory) 172 | 173 | export { uuid as Uuid, hash as Hash, digest as Digest, publicKey as PublicKey } 174 | -------------------------------------------------------------------------------- /src/types/convert.js: -------------------------------------------------------------------------------- 1 | import * as validate from '../types/validate' 2 | 3 | /** 4 | * Convert hexadecimal string into uint8Array 5 | * @param {string} str 6 | * @returns {Uint8Array} 7 | */ 8 | export function hexadecimalToUint8Array (str) { 9 | if (typeof str !== 'string') { 10 | throw new TypeError('Wrong data type passed to convertor. Hexadecimal string is expected') 11 | } 12 | 13 | if (!validate.validateHexadecimal(str, str.length / 2)) { 14 | throw new TypeError('String of wrong type is passed. Hexadecimal expected.') 15 | } 16 | 17 | const uint8arr = new Uint8Array(str.length / 2) 18 | 19 | for (let i = 0, j = 0; i < str.length; i += 2, j++) { 20 | uint8arr[j] = parseInt(str.substr(i, 2), 16) 21 | } 22 | 23 | return uint8arr 24 | } 25 | 26 | /** 27 | * Convert hexadecimal string into binary string 28 | * @param {string} str 29 | * @returns {string} 30 | */ 31 | export function hexadecimalToBinaryString (str) { 32 | let binaryStr = '' 33 | 34 | if (typeof str !== 'string') { 35 | throw new TypeError('Wrong data type passed to convertor. Hexadecimal string is expected') 36 | } 37 | 38 | if (!validate.validateHexadecimal(str, Math.ceil(str.length / 2))) { 39 | throw new TypeError('String of wrong type is passed. Hexadecimal expected.') 40 | } 41 | let prevBin = null 42 | for (let i = 0; i < str.length; i++) { 43 | let bin = strReverse(parseInt(str[i], 16).toString(2)) 44 | while (bin.length < 4) { 45 | bin = bin + '0' 46 | } 47 | if (!prevBin) { 48 | prevBin = bin 49 | } else { 50 | binaryStr += bin + prevBin 51 | prevBin = null 52 | } 53 | } 54 | 55 | return binaryStr 56 | } 57 | 58 | /** 59 | * Convert uint8Array into string 60 | * @param {Uint8Array} uint8arr 61 | * @returns {string} 62 | */ 63 | export function uint8ArrayToHexadecimal (uint8arr) { 64 | let str = '' 65 | 66 | if (!(uint8arr instanceof Uint8Array)) { 67 | throw new TypeError('Wrong data type of array of 8-bit integers. Uint8Array is expected') 68 | } 69 | 70 | for (let i = 0; i < uint8arr.length; i++) { 71 | let hex = uint8arr[i].toString(16) 72 | hex = (hex.length === 1) ? '0' + hex : hex 73 | str += hex 74 | } 75 | 76 | return str.toLowerCase() 77 | } 78 | 79 | /** 80 | * Convert uint8Array into binary string 81 | * @param {Uint8Array} uint8arr 82 | * @returns {string} 83 | */ 84 | export function uint8ArrayToBinaryString (uint8arr) { 85 | let binaryStr = '' 86 | 87 | if (!(uint8arr instanceof Uint8Array)) { 88 | throw new TypeError('Wrong data type of array of 8-bit integers. Uint8Array is expected') 89 | } 90 | 91 | for (let i = 0; i < uint8arr.length; i++) { 92 | let bin = strReverse(uint8arr[i].toString(2)) 93 | while (bin.length < 8) { 94 | bin = bin + '0' 95 | } 96 | binaryStr += bin 97 | } 98 | 99 | return binaryStr 100 | } 101 | 102 | /** 103 | * Convert binary string into uint8Array 104 | * @param {string} binaryStr 105 | * @returns {Uint8Array} 106 | */ 107 | export function binaryStringToUint8Array (binaryStr) { 108 | const array = [] 109 | 110 | if (typeof binaryStr !== 'string') { 111 | throw new TypeError('Wrong data type passed to convertor. Binary string is expected') 112 | } 113 | 114 | if (!validate.validateBinaryString(binaryStr)) { 115 | throw new TypeError('String of wrong type is passed. Binary string expected.') 116 | } 117 | 118 | for (let i = 0; i < binaryStr.length; i += 8) { 119 | array.push(parseInt(strReverse(binaryStr.substr(i, 8)), 2)) 120 | } 121 | 122 | return new Uint8Array(array) 123 | } 124 | 125 | /** 126 | * Convert string into reverse string 127 | * @param str 128 | * @returns {string} 129 | */ 130 | function strReverse (str) { 131 | return str.split('').reverse().join('') 132 | } 133 | 134 | /** 135 | * Convert binary string into hexadecimal string 136 | * @param {string} binaryStr 137 | * @returns {string} 138 | */ 139 | export function binaryStringToHexadecimal (binaryStr) { 140 | let str = '' 141 | 142 | if (typeof binaryStr !== 'string') { 143 | throw new TypeError('Wrong data type passed to convertor. Binary string is expected') 144 | } 145 | 146 | if (!validate.validateBinaryString(binaryStr)) { 147 | throw new TypeError('String of wrong type is passed. Binary string expected.') 148 | } 149 | 150 | for (let i = 0; i < binaryStr.length; i += 8) { 151 | let hex = parseInt(strReverse(binaryStr.substr(i, 8)), 2).toString(16) 152 | hex = (hex.length === 1) ? '0' + hex : hex 153 | str += hex 154 | } 155 | 156 | return str.toLowerCase() 157 | } 158 | 159 | /** 160 | * Convert sting into uint8Array 161 | * @param {string} str 162 | * @param {number} [len] - optional 163 | * @returns {Uint8Array} 164 | */ 165 | export function stringToUint8Array (str, len) { 166 | let array 167 | let from = 0 168 | 169 | if (typeof str !== 'string') { 170 | throw new TypeError('Wrong data type passed to convertor. String is expected') 171 | } 172 | 173 | if (len > 0) { 174 | array = new Array(len) 175 | array.fill(0) 176 | } else { 177 | array = [] 178 | } 179 | 180 | for (let i = 0; i < str.length; i++) { 181 | let c = str.charCodeAt(i) 182 | 183 | if (c < 128) { 184 | array[from++] = c 185 | } else if (c < 2048) { 186 | array[from++] = (c >> 6) | 192 187 | array[from++] = (c & 63) | 128 188 | } else if (((c & 0xFC00) === 0xD800) && (i + 1) < str.length && ((str.charCodeAt(i + 1) & 0xFC00) === 0xDC00)) { 189 | // surrogate pair 190 | c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF) 191 | array[from++] = (c >> 18) | 240 192 | array[from++] = ((c >> 12) & 63) | 128 193 | array[from++] = ((c >> 6) & 63) | 128 194 | array[from++] = (c & 63) | 128 195 | } else { 196 | array[from++] = (c >> 12) | 224 197 | array[from++] = ((c >> 6) & 63) | 128 198 | array[from++] = (c & 63) | 128 199 | } 200 | } 201 | 202 | return new Uint8Array(array) 203 | } 204 | -------------------------------------------------------------------------------- /test/sources/convertors.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | const expect = require('chai').expect 5 | const Exonum = require('../../src') 6 | 7 | describe('Convert data from one type to another', function () { 8 | describe('Check Exonum.hexadecimalToUint8Array', function () { 9 | it('should convert valid hexadecimal into Uint8Array', function () { 10 | const data = require('./data/convertors/hexadecimalToUint8Array.json') 11 | expect(Exonum.hexadecimalToUint8Array(data.from)).to.deep.equal(new Uint8Array(data.to)) 12 | }) 13 | 14 | it('should throw error when convert invalid hexadecimal into Uint8Array', function () { 15 | expect(() => Exonum.hexadecimalToUint8Array('0438082601f8b38ae010a621a48f4b4cd021c4e6e69219e3c2d8abab482039ez')) 16 | .to.throw(TypeError, 'String of wrong type is passed. Hexadecimal expected.'); 17 | 18 | [null, false, 42, new Date(), {}, []].forEach(function (value) { 19 | expect(() => Exonum.hexadecimalToUint8Array(value)) 20 | .to.throw(TypeError, 'Wrong data type passed to convertor. Hexadecimal string is expected') 21 | }) 22 | }) 23 | }) 24 | 25 | describe('Check Exonum.stringToUint8Array', function () { 26 | it('should convert valid string into Uint8Array', function () { 27 | const data = require('./data/convertors/stringToUint8Array.json') 28 | expect(Exonum.stringToUint8Array(data.from)).to.deep.equal(new Uint8Array(data.to)) 29 | }) 30 | 31 | it('should throw error when convert invalid string into Uint8Array', function () { 32 | [null, false, 42, new Date(), {}, []].forEach(function (value) { 33 | expect(() => Exonum.stringToUint8Array(value)) 34 | .to.throw(TypeError, 'Wrong data type passed to convertor. String is expected') 35 | }) 36 | }) 37 | }) 38 | 39 | describe('Check Exonum.binaryStringToUint8Array', function () { 40 | it('should convert valid binaryString into Uint8Array', function () { 41 | const data = require('./data/convertors/binaryStringToUint8Array.json') 42 | expect(Exonum.binaryStringToUint8Array(data.from)).to.deep.equal(new Uint8Array(data.to)) 43 | }) 44 | 45 | it('should throw error when convert wrong binaryString into Uint8Array', function () { 46 | [null, false, new Date(), {}, [], 42].forEach(function (value) { 47 | expect(() => Exonum.binaryStringToUint8Array(value)) 48 | .to.throw(TypeError, 'Wrong data type passed to convertor. Binary string is expected') 49 | }) 50 | }) 51 | 52 | it('should throw error when convert invalid binaryString into Uint8Array', function () { 53 | ['102'].forEach(function (value) { 54 | expect(() => Exonum.binaryStringToUint8Array(value)) 55 | .to.throw(TypeError, 'String of wrong type is passed. Binary string expected.') 56 | }) 57 | }) 58 | }) 59 | 60 | describe('Check Exonum.uint8ArrayToHexadecimal', function () { 61 | it('should convert valid Uint8Array into hexadecimal', function () { 62 | const data = require('./data/convertors/uint8ArrayToHexadecimal.json') 63 | expect(Exonum.uint8ArrayToHexadecimal(new Uint8Array(data.from))).to.equal(data.to) 64 | }) 65 | 66 | it('should throw error when convert invalid Uint8Array into hexadecimal', function () { 67 | [null, false, 42, new Date(), {}, 'Hello world', [4, 56]].forEach(function (value) { 68 | expect(() => Exonum.uint8ArrayToHexadecimal(value)) 69 | .to.throw(TypeError, 'Wrong data type of array of 8-bit integers. Uint8Array is expected') 70 | }) 71 | }) 72 | }) 73 | 74 | describe('Check Exonum.uint8ArrayToBinaryString', function () { 75 | it('should convert valid Uint8Array into binaryString', function () { 76 | const data = require('./data/convertors/uint8ArrayToBinaryString.json') 77 | expect(Exonum.uint8ArrayToBinaryString(new Uint8Array(data.from))).to.equal(data.to) 78 | }) 79 | 80 | it('should throw error when convert invalid Uint8Array into binaryString', function () { 81 | [null, false, 42, new Date(), {}, 'Hello world', [4, 56]].forEach(function (value) { 82 | expect(() => Exonum.uint8ArrayToBinaryString(value)) 83 | .to.throw(TypeError, 'Wrong data type of array of 8-bit integers. Uint8Array is expected') 84 | }) 85 | }) 86 | }) 87 | 88 | describe('Check Exonum.binaryStringToHexadecimal', function () { 89 | it('should convert valid binaryString into hexadecimal', function () { 90 | const data = require('./data/convertors/binaryStringToHexadecimal.json') 91 | expect(Exonum.binaryStringToHexadecimal(data.from)).to.deep.equal(data.to) 92 | }) 93 | 94 | it('should throw error when convert binaryString of wrong type into hexadecimal', function () { 95 | expect(() => Exonum.binaryStringToHexadecimal('102')) 96 | .to.throw(TypeError, 'String of wrong type is passed. Binary string expected.'); 97 | 98 | [null, false, 42, new Date(), {}, []].forEach(function (value) { 99 | expect(() => Exonum.binaryStringToHexadecimal(value)) 100 | .to.throw(TypeError, 'Wrong data type passed to convertor. Binary string is expected') 101 | }) 102 | }) 103 | }) 104 | 105 | describe('Check Exonum.hexadecimalToBinaryString', function () { 106 | it('should convert valid hexadecimal into BinaryString', function () { 107 | const data = require('./data/convertors/hexadecimalToBinaryString.json') 108 | expect(Exonum.hexadecimalToBinaryString(data.from)).to.equal(data.to) 109 | }) 110 | 111 | it('should throw error when convert invalid hexadecimal into binaryString', function () { 112 | expect(() => Exonum.hexadecimalToBinaryString('az')) 113 | .to.throw(TypeError, 'String of wrong type is passed. Hexadecimal expected.'); 114 | 115 | [null, false, 42, new Date(), {}, []].forEach(function (value) { 116 | expect(() => Exonum.hexadecimalToBinaryString(value)) 117 | .to.throw(TypeError, 'Wrong data type passed to convertor. Hexadecimal string is expected') 118 | }) 119 | }) 120 | 121 | it('should convert hexadecimal to BinaryString and back', function () { 122 | const hex = '0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d' 123 | expect(Exonum.binaryStringToHexadecimal(Exonum.hexadecimalToBinaryString(hex))).to.equal(hex) 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /proto/google/protobuf/timestamp.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option cc_enable_arenas = true; 37 | option go_package = "github.com/golang/protobuf/ptypes/timestamp"; 38 | option java_package = "com.google.protobuf"; 39 | option java_outer_classname = "TimestampProto"; 40 | option java_multiple_files = true; 41 | option objc_class_prefix = "GPB"; 42 | 43 | // A Timestamp represents a point in time independent of any time zone 44 | // or calendar, represented as seconds and fractions of seconds at 45 | // nanosecond resolution in UTC Epoch time. It is encoded using the 46 | // Proleptic Gregorian Calendar which extends the Gregorian calendar 47 | // backwards to year one. It is encoded assuming all minutes are 60 48 | // seconds long, i.e. leap seconds are "smeared" so that no leap second 49 | // table is needed for interpretation. Range is from 50 | // 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. 51 | // By restricting to that range, we ensure that we can convert to 52 | // and from RFC 3339 date strings. 53 | // See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt). 54 | // 55 | // # Examples 56 | // 57 | // Example 1: Compute Timestamp from POSIX `time()`. 58 | // 59 | // Timestamp timestamp; 60 | // timestamp.set_seconds(time(NULL)); 61 | // timestamp.set_nanos(0); 62 | // 63 | // Example 2: Compute Timestamp from POSIX `gettimeofday()`. 64 | // 65 | // struct timeval tv; 66 | // gettimeofday(&tv, NULL); 67 | // 68 | // Timestamp timestamp; 69 | // timestamp.set_seconds(tv.tv_sec); 70 | // timestamp.set_nanos(tv.tv_usec * 1000); 71 | // 72 | // Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. 73 | // 74 | // FILETIME ft; 75 | // GetSystemTimeAsFileTime(&ft); 76 | // UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; 77 | // 78 | // // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z 79 | // // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. 80 | // Timestamp timestamp; 81 | // timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); 82 | // timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); 83 | // 84 | // Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. 85 | // 86 | // long millis = System.currentTimeMillis(); 87 | // 88 | // Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) 89 | // .setNanos((int) ((millis % 1000) * 1000000)).build(); 90 | // 91 | // 92 | // Example 5: Compute Timestamp from current time in Python. 93 | // 94 | // timestamp = Timestamp() 95 | // timestamp.GetCurrentTime() 96 | // 97 | // # JSON Mapping 98 | // 99 | // In JSON format, the Timestamp type is encoded as a string in the 100 | // [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the 101 | // format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 102 | // where {year} is always expressed using four digits while {month}, {day}, 103 | // {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional 104 | // seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), 105 | // are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone 106 | // is required, though only UTC (as indicated by "Z") is presently supported. 107 | // 108 | // For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 109 | // 01:30 UTC on January 15, 2017. 110 | // 111 | // In JavaScript, one can convert a Date object to this format using the 112 | // standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString] 113 | // method. In Python, a standard `datetime.datetime` object can be converted 114 | // to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) 115 | // with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one 116 | // can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( 117 | // http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime--) 118 | // to obtain a formatter capable of generating timestamps in this format. 119 | // 120 | // 121 | message Timestamp { 122 | 123 | // Represents seconds of UTC time since Unix epoch 124 | // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 125 | // 9999-12-31T23:59:59Z inclusive. 126 | int64 seconds = 1; 127 | 128 | // Non-negative fractions of a second at nanosecond resolution. Negative 129 | // second values with fractions must still have non-negative nanos values 130 | // that count forward in time. Must be from 0 to 999,999,999 131 | // inclusive. 132 | int32 nanos = 2; 133 | } 134 | -------------------------------------------------------------------------------- /src/blockchain/ProofPath.js: -------------------------------------------------------------------------------- 1 | import { 2 | binaryStringToUint8Array, 3 | uint8ArrayToHexadecimal, 4 | hexadecimalToBinaryString 5 | } from '../types/convert' 6 | 7 | /** 8 | * Length of a path to a terminal node in bits. 9 | * 10 | * @type {number} 11 | */ 12 | const BIT_LENGTH = 256 13 | 14 | export default class ProofPath { 15 | /** 16 | * Constructs a proof path from a binary string or a byte buffer. 17 | * 18 | * @param {string | Uint8Array} bits 19 | * @param {number} bitLength? 20 | */ 21 | constructor (bits, bitLength = BIT_LENGTH) { 22 | if (typeof bits === 'string') { 23 | this.key = binaryStringToUint8Array(padWithZeros(bits, BIT_LENGTH)) 24 | bitLength = bits.length 25 | } else if (bits instanceof Uint8Array && bits.length === BIT_LENGTH / 8) { 26 | this.key = bits.slice(0) 27 | } else { 28 | throw new TypeError('Invalid `bits` parameter') 29 | } 30 | 31 | this.bitLength = bitLength 32 | this.hexKey = uint8ArrayToHexadecimal(this.key) 33 | } 34 | 35 | /** 36 | * Checks if this path corresponds to the terminal node / leaf in the Merkle Patricia tree. 37 | * 38 | * @returns {boolean} 39 | */ 40 | isTerminal () { 41 | return this.bitLength === BIT_LENGTH 42 | } 43 | 44 | /** 45 | * Retrieves a bit at a specific position of this key. 46 | * 47 | * @param {number} pos 48 | * @returns {0 | 1 | void} 49 | */ 50 | bit (pos) { 51 | pos = +pos 52 | if (pos >= this.bitLength || pos < 0) { 53 | return undefined 54 | } 55 | 56 | return getBit(this.key, pos) 57 | } 58 | 59 | commonPrefixLength (other) { 60 | const intersectingBits = Math.min(this.bitLength, other.bitLength) 61 | 62 | // First, advance by a full byte while it is possible 63 | let pos 64 | for (pos = 0; 65 | pos < intersectingBits >> 3 && this.key[pos >> 3] === other.key[pos >> 3]; 66 | pos += 8) ; 67 | 68 | // Then, check individual bits 69 | for (; pos < intersectingBits && this.bit(pos) === other.bit(pos); pos++) ; 70 | 71 | return pos 72 | } 73 | 74 | /** 75 | * Computes a common prefix of this and another byte sequence. 76 | * 77 | * @param {ProofPath} other 78 | * @returns {ProofPath} 79 | */ 80 | commonPrefix (other) { 81 | const pos = this.commonPrefixLength(other) 82 | return this.truncate(pos) 83 | } 84 | 85 | /** 86 | * Checks if the path starts with the other specified path. 87 | * 88 | * @param {ProofPath} other 89 | * @returns {boolean} 90 | */ 91 | startsWith (other) { 92 | return this.commonPrefixLength(other) === other.bitLength 93 | } 94 | 95 | /** 96 | * Compares this proof path to another. 97 | * 98 | * @param {ProofPath} other 99 | * @returns {-1 | 0 | 1} 100 | */ 101 | compare (other) { 102 | const [thisLen, otherLen] = [this.bitLength, other.bitLength] 103 | const intersectingBits = Math.min(thisLen, otherLen) 104 | const pos = this.commonPrefixLength(other) 105 | 106 | if (pos === intersectingBits) { 107 | return Math.sign(thisLen - otherLen) 108 | } 109 | return this.bit(pos) - other.bit(pos) 110 | } 111 | 112 | /** 113 | * Truncates this bit sequence to a shorter one by removing some bits from the end. 114 | * 115 | * @param {number} bits 116 | * new length of the sequence 117 | * @returns {ProofPath} 118 | * truncated bit sequence 119 | */ 120 | truncate (bits) { 121 | bits = +bits 122 | if (bits > this.bitLength) { 123 | throw new TypeError('Cannot truncate bit slice to length more than current ' + 124 | `(current: ${this.bitLength}, requested: ${bits})`) 125 | } 126 | 127 | const bytes = new Uint8Array(BIT_LENGTH / 8) 128 | for (let i = 0; i < bits >> 3; i++) { 129 | bytes[i] = this.key[i] 130 | } 131 | for (let bit = 8 * (bits >> 3); bit < bits; bit++) { 132 | setBit(bytes, bit, this.bit(bit)) 133 | } 134 | 135 | return new ProofPath(bytes, bits) 136 | } 137 | 138 | /** 139 | * Serializes this path into a buffer. The serialization is performed according as follows: 140 | * 141 | * 1. Serialize number of bits in the path in LEB128 encoding. 142 | * 2. Serialize bits with zero padding to the right. 143 | * 144 | * @param {Array} buffer 145 | */ 146 | serialize (buffer) { 147 | if (this.bitLength < 128) { 148 | buffer.push(this.bitLength) 149 | } else { 150 | // The length is encoded as two bytes. 151 | // The first byte contains the lower 7 bits of the length, and has the highest bit set 152 | // as per LEB128. The second byte contains the upper bit of the length 153 | // (i.e., 1 or 2), thus, it always equals 1 or 2. 154 | buffer.push(128 + (this.bitLength % 128), this.bitLength >> 7) 155 | } 156 | 157 | // Copy the bits. 158 | for (let pos = 0; pos < (this.bitLength + 7) >> 3; pos++) { 159 | buffer.push(this.key[pos]) 160 | } 161 | } 162 | 163 | /** 164 | * Converts this path to its JSON presentation. 165 | * 166 | * @returns {string} 167 | * binary string representing the path 168 | */ 169 | toJSON () { 170 | const bits = hexadecimalToBinaryString(this.hexKey) 171 | return trimZeros(bits, this.bitLength) 172 | } 173 | 174 | toString () { 175 | let bits = hexadecimalToBinaryString(this.hexKey) 176 | bits = (this.bitLength > 8) 177 | ? trimZeros(bits, 8) + '...' 178 | : trimZeros(bits, this.bitLength) 179 | return `path(${bits})` 180 | } 181 | } 182 | 183 | /** 184 | * Expected length of byte buffers used to create `ProofPath`s. 185 | */ 186 | ProofPath.BYTE_LENGTH = BIT_LENGTH / 8 187 | 188 | function getBit (buffer, pos) { 189 | const byte = Math.floor(pos / 8) 190 | const bitPos = pos % 8 191 | 192 | return (buffer[byte] & (1 << bitPos)) >> bitPos 193 | } 194 | 195 | /** 196 | * Sets a specified bit in the byte buffer. 197 | * 198 | * @param {Uint8Array} buffer 199 | * @param {number} pos 0-based position in the buffer to set 200 | * @param {0 | 1} bit 201 | */ 202 | function setBit (buffer, pos, bit) { 203 | const byte = Math.floor(pos / 8) 204 | const bitPos = pos % 8 205 | 206 | if (bit === 0) { 207 | const mask = 255 - (1 << bitPos) 208 | buffer[byte] &= mask 209 | } else { 210 | const mask = (1 << bitPos) 211 | buffer[byte] |= mask 212 | } 213 | } 214 | 215 | const ZEROS = (() => { 216 | let str = '0' 217 | for (let i = 0; i < 8; i++) str = str + str 218 | return str 219 | })() 220 | 221 | function padWithZeros (str, desiredLength) { 222 | return str + ZEROS.substring(0, desiredLength - str.length) 223 | } 224 | 225 | function trimZeros (str, desiredLength) { 226 | /* istanbul ignore next: should never be triggered */ 227 | if (str.length < desiredLength) { 228 | throw new Error('Invariant broken: negative zero trimming requested') 229 | } 230 | return str.substring(0, desiredLength) 231 | } 232 | -------------------------------------------------------------------------------- /test/sources/protobuf-message.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | import * as $protobuf from 'protobufjs/light' 4 | import { hexadecimalToUint8Array } from '../../src/types' 5 | 6 | const expect = require('chai').expect 7 | const Exonum = require('../../src') 8 | const Root = $protobuf.Root 9 | const Type = $protobuf.Type 10 | const Field = $protobuf.Field 11 | 12 | const root = new Root() 13 | 14 | describe('Protobuf serialization', function () { 15 | it('should create data for transaction', function () { 16 | const keyPair = { 17 | publicKey: '84e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455', 18 | secretKey: '9aaa377f0880ae2aa6697ea45e6c26f164e923e73b31f52e6da0cf40798ca4c184e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455' 19 | } 20 | 21 | const CreateTransactionProtobuf = new Type('CreateTransaction') 22 | .add(new Field('pub_key', 1, 'bytes')) 23 | .add(new Field('name', 2, 'string')) 24 | .add(new Field('balance', 3, 'int64')) 25 | root.define('CreateTransactionProtobuf').add(CreateTransactionProtobuf) 26 | 27 | const CreateTransaction = new Exonum.Transaction({ 28 | serviceId: 130, 29 | methodId: 0, 30 | schema: CreateTransactionProtobuf 31 | }) 32 | 33 | const data = { 34 | pub_key: hexadecimalToUint8Array('f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36'), 35 | name: 'Smart wallet', 36 | balance: 359120 37 | } 38 | 39 | const buffer = CreateTransaction.create(data, keyPair).serialize() 40 | expect(buffer.length).to.satisfy(len => len > 32 + 64) 41 | const restored = CreateTransaction.deserialize(buffer) 42 | expect(restored.author).to.equal(keyPair.publicKey) 43 | expect(restored.payload.pub_key).to.deep.equal(data.pub_key) 44 | expect(+restored.payload.balance).to.equal(data.balance) 45 | }) 46 | 47 | it('should create data for small transactions', function () { 48 | const keyPair = Exonum.keyPair() 49 | 50 | const SmallTransactionProtobuf = new Type('SmallTransaction') 51 | SmallTransactionProtobuf.add(new Field('name', 1, 'string')) 52 | root.define('SmallTransaction').add(SmallTransactionProtobuf) 53 | 54 | const SmallTransaction = new Exonum.Transaction({ 55 | serviceId: 128, 56 | methodId: 2, 57 | schema: SmallTransactionProtobuf 58 | }) 59 | 60 | const verified = SmallTransaction.create({ name: 'test' }, keyPair) 61 | expect(verified).to.be.instanceOf(Exonum.Verified) 62 | const buffer = verified.serialize() 63 | expect(buffer).to.be.instanceOf(Uint8Array) 64 | 65 | const restored = SmallTransaction.deserialize(buffer) 66 | expect(restored.payload.name).to.equal('test') 67 | }) 68 | 69 | it('should create data for small type', function () { 70 | const SmallTypeProtobuf = new Type('SmallType') 71 | SmallTypeProtobuf.add(new Field('name', 1, 'string')) 72 | root.define('SmallType').add(SmallTypeProtobuf) 73 | 74 | const SmallType = Exonum.newType(SmallTypeProtobuf) 75 | const data = { 76 | data: { 77 | name: '' 78 | }, 79 | serialized: Uint8Array.from([]) 80 | } 81 | const buffer = SmallType.serialize(data.data) 82 | expect(buffer).to.deep.equal(data.serialized) 83 | }) 84 | 85 | it('should create data for new type', function () { 86 | const CreateTypeProtobuf = new Type('CreateType').add(new Field('pub_key', 1, 'bytes')) 87 | CreateTypeProtobuf.add(new Field('name', 2, 'string')) 88 | CreateTypeProtobuf.add(new Field('balance', 3, 'int64')) 89 | root.define('CreateTypeProtobuf').add(CreateTypeProtobuf) 90 | 91 | const CreateType = Exonum.newType(CreateTypeProtobuf) 92 | 93 | const data = { 94 | data: { 95 | pub_key: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 96 | name: 'Smart wallet', 97 | balance: 359120 98 | }, 99 | serialized: Uint8Array.from([ 100 | 10, 48, 127, 159, 58, 225, 166, 250, 107, 150, 182, 215, 221, 58, 101 | 235, 166, 248, 237, 206, 187, 233, 183, 31, 215, 150, 181, 127, 103, 244, 239, 189, 55, 115, 150, 220, 102 | 105, 246, 249, 239, 143, 90, 107, 189, 249, 113, 239, 27, 237, 205, 250, 18, 12, 83, 109, 97, 114, 116, 103 | 32, 119, 97, 108, 108, 101, 116, 24, 208, 245, 21 104 | ]) 105 | } 106 | 107 | const buffer = CreateType.serialize(data.data) 108 | 109 | expect(buffer).to.deep.equal(data.serialized) 110 | }) 111 | 112 | it('should create data for new type with zero int', function () { 113 | const CreateTypeProtobuf = new Type('CreateType').add(new Field('pub_key', 1, 'bytes')) 114 | CreateTypeProtobuf.add(new Field('name', 2, 'string')) 115 | CreateTypeProtobuf.add(new Field('balance', 3, 'int64')) 116 | root.define('CreateTypeProtobuf1').add(CreateTypeProtobuf) 117 | 118 | const CreateType = Exonum.newType(CreateTypeProtobuf) 119 | 120 | const data = { 121 | data: { 122 | pub_key: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 123 | name: 'Smart wallet', 124 | balance: 0 125 | }, 126 | serialized: Uint8Array.from([ 127 | 10, 48, 127, 159, 58, 225, 166, 250, 107, 150, 182, 215, 221, 58, 128 | 235, 166, 248, 237, 206, 187, 233, 183, 31, 215, 150, 181, 127, 103, 244, 239, 189, 55, 115, 150, 220, 129 | 105, 246, 249, 239, 143, 90, 107, 189, 249, 113, 239, 27, 237, 205, 250, 18, 12, 83, 109, 97, 114, 116, 130 | 32, 119, 97, 108, 108, 101, 116 131 | ]) 132 | } 133 | 134 | const buffer = CreateType.serialize(data.data) 135 | 136 | expect(buffer).to.deep.equal(data.serialized) 137 | }) 138 | 139 | it('should create data for new transaction with zero int', function () { 140 | const keyPair = { 141 | publicKey: '84e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455', 142 | secretKey: '9aaa377f0880ae2aa6697ea45e6c26f164e923e73b31f52e6da0cf40798ca4c184e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455' 143 | } 144 | const CreateTransactionProtobuf1 = new Type('CreateType1').add(new Field('pub_key', 1, 'bytes')) 145 | CreateTransactionProtobuf1.add(new Field('name', 2, 'string')) 146 | CreateTransactionProtobuf1.add(new Field('balance', 3, 'int64')) 147 | root.define('CreateTransactionProtobuf1').add(CreateTransactionProtobuf1) 148 | 149 | const CreateTransaction = new Exonum.Transaction({ 150 | serviceId: 130, 151 | methodId: 0, 152 | schema: CreateTransactionProtobuf1 153 | }) 154 | 155 | const data = { 156 | pub_key: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 157 | name: 'Smart wallet', 158 | balance: 0 159 | } 160 | 161 | const signed = CreateTransaction.create(data, keyPair) 162 | // Check that zero balance is not present in the serialization. 163 | const args = signed.payload.any_tx.arguments 164 | const argsEnd = args.slice(-'Smart wallet'.length) 165 | expect(argsEnd).to.deep.equal(Buffer.from('Smart wallet')) 166 | 167 | const restored = CreateTransaction.deserialize(signed.serialize()) 168 | expect(+restored.payload.balance).to.equal(0) 169 | expect(restored.payload.name).to.equal('Smart wallet') 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /test/sources/merkle-proof.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | const expect = require('chai').expect 5 | const Exonum = require('../../src') 6 | 7 | describe('Check proof of Merkle tree', () => { 8 | it('should work for a single-element tree', () => { 9 | const data = { 10 | proof: { 11 | proof: [], 12 | entries: [ 13 | [0, 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b'] 14 | ], 15 | length: 1 16 | }, 17 | trustedRoot: '11ab18c3230d71ee2272393ea31fadb1cf964a4bff0a4a57891ee506d32f26ca' 18 | } 19 | 20 | const proof = new Exonum.ListProof(data.proof, Exonum.Hash) 21 | expect(proof.merkleRoot).to.equal(data.trustedRoot) 22 | }) 23 | 24 | it('should work for small fully-exposed tree', () => { 25 | const data = require('./data/merkle-tree/small.json') 26 | const proof = new Exonum.ListProof(data.proof, Exonum.Hash) 27 | expect(proof.merkleRoot).to.equal(data.trustedRoot) 28 | expect(proof.length).to.equal(4) 29 | proof.entries.forEach(({ index, value }, i) => { 30 | expect(index).to.equal(i) 31 | expect(value).to.be.a('string') 32 | }) 33 | }) 34 | 35 | it('should work for small tree with single element', () => { 36 | const data = require('./data/merkle-tree/single-element.json') 37 | const proof = new Exonum.ListProof(data.proof, Exonum.Hash) 38 | expect(proof.merkleRoot).to.equal(data.trustedRoot) 39 | expect(proof.entries).to.have.lengthOf(1) 40 | expect(proof.entries[0].index).to.equal(2) 41 | }) 42 | 43 | it('should work for tree with several elements', () => { 44 | const data = require('./data/merkle-tree/several-elements.json') 45 | const proof = new Exonum.ListProof(data.proof, Exonum.Hash) 46 | expect(proof.merkleRoot).to.equal(data.trustedRoot) 47 | expect(proof.entries).to.have.lengthOf(3) 48 | const indexes = proof.entries.map(({ index }) => index) 49 | expect(indexes).to.deep.equal([3, 4, 5]) 50 | }) 51 | 52 | it('should error if value type is not specified', () => { 53 | const data = require('./data/merkle-tree/single-element.json') 54 | expect(() => new Exonum.ListProof(data)) 55 | .to.throw('No `serialize` method in the value type') 56 | expect(() => new Exonum.ListProof(data, {})) 57 | .to.throw('No `serialize` method in the value type') 58 | }) 59 | 60 | it('should error if proof part is malformed', () => { 61 | const malformedProof = { 62 | proof: [ 63 | { height: 0, index: 0, hash: 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b' } 64 | ], 65 | entries: [ 66 | [0, 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b'] 67 | ] 68 | } 69 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 70 | .to.throw('malformed `proof` part of the proof') 71 | 72 | malformedProof.proof[0].height = 100 73 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 74 | .to.throw('malformed `proof` part of the proof') 75 | 76 | malformedProof.proof[0] = { 77 | height: 1, 78 | index: 'aaa', 79 | hash: 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b' 80 | } 81 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 82 | .to.throw('malformed `proof` part of the proof') 83 | 84 | malformedProof.proof[0].index = 1 85 | malformedProof.proof[0].hash = 'd6daf4cabee9' 86 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 87 | .to.throw('malformed `proof` part of the proof') 88 | }) 89 | 90 | it('should error if entries are malformed', () => { 91 | const malformedProof = { 92 | proof: [], 93 | entries: [ 94 | ['aa', 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b'] 95 | ] 96 | } 97 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 98 | .to.throw('malformed `entries` in the proof') 99 | }) 100 | 101 | it('should error if `proof` entries are unordered', () => { 102 | const zeroHash = '00'.repeat(32) 103 | const malformedProof = { 104 | proof: [ 105 | { height: 1, index: 1, hash: zeroHash }, 106 | { height: 1, index: 0, hash: zeroHash } 107 | ], 108 | entries: [[0, zeroHash]] 109 | } 110 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 111 | .to.throw('malformed `proof` part of the proof') 112 | 113 | // Duplicate "coordinates of a proof entry" 114 | malformedProof.proof[1].index = 1 115 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 116 | .to.throw('malformed `proof` part of the proof') 117 | }) 118 | 119 | it('should error if entries are unordered', () => { 120 | const zeroHash = '00'.repeat(32) 121 | const malformedProof = { 122 | proof: [], 123 | entries: [[5, zeroHash], [3, zeroHash]], 124 | length: 10 125 | } 126 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 127 | .to.throw('malformed `entries` in the proof') 128 | 129 | malformedProof.entries[0][0] = 3 130 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 131 | .to.throw('malformed `entries` in the proof') 132 | }) 133 | 134 | it('should error on exceedingly large height of an entry', () => { 135 | const zeroHash = '00'.repeat(32) 136 | const malformedProof = { 137 | proof: [ 138 | { height: 10, index: 1, hash: zeroHash } 139 | ], 140 | entries: [[0, zeroHash]], 141 | length: 2 142 | } 143 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 144 | .to.throw('impossible according to list length') 145 | }) 146 | 147 | it('should error on exceedingly large index of an entry', () => { 148 | const zeroHash = '00'.repeat(32) 149 | const malformedProof = { 150 | proof: [ 151 | { height: 1, index: 3, hash: zeroHash } 152 | ], 153 | entries: [[0, zeroHash]], 154 | length: 2 155 | } 156 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 157 | .to.throw('impossible according to list length') 158 | 159 | // Make index adequate in the `proof`, but break it in `entries[0]`. 160 | malformedProof.proof[0].index = 1 161 | malformedProof.entries = [[2, zeroHash]] 162 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 163 | .to.throw('impossible according to list length') 164 | }) 165 | 166 | it('should error on missing hash', () => { 167 | const malformedProof = { 168 | proof: [], 169 | entries: [ 170 | [0, 'd6daf4cabee921faf9f9b2424b53bf49f7f1d8e813e4ed06d465d0ef5bcf2b4b'], 171 | [1, 'ef9c89edc71fdf62b1642aa13b5d6f6e9b09717b4e77c045dcbd24c1318a50e9'], 172 | [3, 'c808416e4ce59b474a9e6311d8619f5519bc72cea884b435215b52540912ca93'] 173 | ], 174 | length: 4 175 | } 176 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 177 | .to.throw('proof does not contain information to restore index hash') 178 | }) 179 | 180 | it('should error on missing hash if some hashes are available', () => { 181 | const malformedProof = { 182 | proof: [ 183 | { index: 3, height: 1, hash: '61119196a01db39f0b3a381579c366c8f85bf5186f53754531efe95f7e7a47c8' }, 184 | { index: 0, height: 2, hash: '7bfe099e406e9c23b06ef1c6d50268524941cfa19152720fe841a9268b9375dc' } 185 | ], 186 | entries: [ 187 | [2, '463249d0a1109e8469a2af46419655c9cd2a41f6ce5d2afc16597927467ab56c'] 188 | ], 189 | length: 6 190 | } 191 | expect(() => new Exonum.ListProof(malformedProof, Exonum.Hash)) 192 | .to.throw('proof does not contain information to restore index hash') 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /examples/map-proof.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example how to use `MapProof`s in client apps. 3 | */ 4 | 5 | const Exonum = require('..') 6 | const { MapProof, PublicKey } = Exonum 7 | const { expect } = require('chai') 8 | const { Type, Field } = require('protobufjs/light') 9 | 10 | // Declare the value type used in the proof. 11 | const PublicKeySchema = new Type('PublicKey') 12 | .add(new Field('data', 1, 'bytes')) 13 | const WalletSchema = new Type('Wallet') 14 | .add(PublicKeySchema) 15 | .add(new Field('pub_key', 1, 'PublicKey')) 16 | .add(new Field('name', 2, 'string')) 17 | .add(new Field('balance', 3, 'uint64')) 18 | .add(new Field('uniq_id', 4, 'string')) 19 | const Wallet = Exonum.newType(WalletSchema) 20 | 21 | // This proof JSON for 2 existing and 2 missing wallets is generated 22 | // with the help of the `/wallets/random` endpoint of the integration test server 23 | // (located in the `integration-tests` directory): 24 | // 25 | // cd integration-tests 26 | // cargo run & 27 | // curl http://localhost:8000/wallets/random?seed=1337&wallets=20&wallets_in_proof=2 28 | let proof = { 29 | entries: [ 30 | { 31 | missing: 'c6e167e6914c68fe9cb6d02e72ff4c8ecfe8fa1696625e4b1f89eb6597b1c16a' 32 | }, 33 | { 34 | key: 'c1608da6fe83023f95c1d9d31d070c4e70b93059f1a298b1a02e85eb4414c855', 35 | value: { 36 | pub_key: 'c1608da6fe83023f95c1d9d31d070c4e70b93059f1a298b1a02e85eb4414c855', 37 | name: 'c1608da6', 38 | balance: 3986358255, 39 | uniq_id: '0621b404-80c5-bf18-04c1-6c466054f28a' 40 | } 41 | }, 42 | { 43 | key: '049b1598a5b417fcd53e318bc90322b72c52bd771ff3febb3d53d36b77329ada', 44 | value: { 45 | pub_key: '049b1598a5b417fcd53e318bc90322b72c52bd771ff3febb3d53d36b77329ada', 46 | name: '049b1598', 47 | balance: 69049252, 48 | uniq_id: 'beb5bee7-ac51-c829-1b16-e8b77a12d371' 49 | } 50 | }, 51 | { 52 | missing: '69a5411f3ec4c2cd7e83bf61130e15654d69b76585dcca6e1dcc986a50b96cad' 53 | } 54 | ], 55 | proof: [ 56 | { 57 | path: '0000110110110010100111000100010110011110110000100010100001000101010000110001110001010110010001111011011101000111101100111010010000010110011110111011110111001111001110110000101111101100111101100001000110010101101101010001111000111101000111110111110011000101', 58 | hash: '42458b0cf398d03cefb02fe31919b4bbe6ee6c68a79c49bea7c80a0b0bef3936' 59 | }, 60 | { 61 | path: '001011', 62 | hash: 'dcaf092596dcd3e3a9a8405d8dfb3817df21657ecb1e5257a7779c0c3c0157f8' 63 | }, 64 | { 65 | path: '01', 66 | hash: 'f7e72241fb9ee2351c9ab54c3020554bd26af62a217072d4eb8eaaba0a13af21' 67 | }, 68 | { 69 | path: '10', 70 | hash: 'b69cd61a9c22f2467e1be1309a06671c7b2ab38b97a9bc47ce85e3646271b240' 71 | }, 72 | { 73 | path: '1100101011001101010001000111001000101010100001110101110010111010100011010010101011100110111111100100101001111111010111000010010010100110110110001101000100110101101011100100011010000010111011001000111110000110110111010011110100011100100101111110110001111100', 74 | hash: 'da18257a7f0b9c0c863468f2c15819cd93a67a87408ddd884db1142e657965b7' 75 | }, 76 | { 77 | path: '1101011001101000011110000011010100011010110001100100001110100110110111100010101000010100101100101011111011010011001000100110111111101111100010111100101110000100011011010101010000010000010111011011010000110011100011100100111010100110001101111101110100011101', 78 | hash: 'd156aba4c4e5c63124ac52295466e935ba5d2282a64082c85841e32bbf5b4c97' 79 | }, 80 | { 81 | path: '1111100001001101101101000111010010001010100111001101100110101011110100110001110110110110101101000100100011101000011010100111001110000011100110111111101110000010010001011101000011111011000010100110110010100000111111111001110100000110100011001001100011000011', 82 | hash: '3bb6a394e0f69404bf8594f5fc42a2a5275b1d98f86bb766e4091597f4df7803' 83 | } 84 | ] 85 | } 86 | 87 | // Temporary hack to convert `PublicKey`s into the form compatible with the Protobuf reader. 88 | function convertPublicKey (wallet) { 89 | if (wallet && wallet.pub_key) { 90 | wallet.pub_key = { 91 | data: Buffer.from(wallet.pub_key, 'hex') 92 | } 93 | } 94 | } 95 | 96 | proof.entries.forEach(({ value }) => convertPublicKey(value)) 97 | 98 | // Create a `MapProof` instance. The constructor will throw an error if 99 | // the supplied JSON is malformed. 100 | proof = new MapProof(proof, PublicKey, Wallet) 101 | expect(proof.entries.size).to.equal(2) 102 | expect(proof.missingKeys.size).to.equal(2) 103 | 104 | // Output elements asserted by the proof to exist or to be absent in the 105 | // underlying index. 106 | console.log('\nEntries:') 107 | for (const wallet of proof.entries.values()) { 108 | console.log(wallet) 109 | } 110 | console.log('\nMissing keys:') 111 | for (const missingKey of proof.missingKeys) { 112 | console.log(missingKey) 113 | } 114 | 115 | // The Merkle root of the proof is usually propagated further to the trust anchor 116 | // (usually, a `state_hash` field in the block header). Here, we just compare 117 | // it to the reference value for simplicity. 118 | expect(proof.merkleRoot).to.equal('22e3fa8457f2226546f8919ef22bba7853ddc55e221c95644daf21970e8ea8a6') 119 | 120 | // Perform the same task, but for raw (unhashed) keys. These keys may be used 121 | // with certain key types (e.g., `PublicKey`) to minimize the amount of hashing. 122 | // 123 | // This proof JSON is obtained using the following commands: 124 | // 125 | // cd integration-tests 126 | // cargo run & 127 | // curl http://localhost:8000/wallets/random-raw?seed=1337&wallets=20&wallets_in_proof=2 128 | proof = { 129 | entries: [ 130 | { 131 | key: '049b1598a5b417fcd53e318bc90322b72c52bd771ff3febb3d53d36b77329ada', 132 | value: { 133 | pub_key: '049b1598a5b417fcd53e318bc90322b72c52bd771ff3febb3d53d36b77329ada', 134 | name: '049b1598', 135 | balance: 69049252, 136 | uniq_id: 'beb5bee7-ac51-c829-1b16-e8b77a12d371' 137 | } 138 | }, 139 | { 140 | missing: 'c6e167e6914c68fe9cb6d02e72ff4c8ecfe8fa1696625e4b1f89eb6597b1c16a' 141 | }, 142 | { 143 | key: 'c1608da6fe83023f95c1d9d31d070c4e70b93059f1a298b1a02e85eb4414c855', 144 | value: { 145 | pub_key: 'c1608da6fe83023f95c1d9d31d070c4e70b93059f1a298b1a02e85eb4414c855', 146 | name: 'c1608da6', 147 | balance: 3986358255, 148 | uniq_id: '0621b404-80c5-bf18-04c1-6c466054f28a' 149 | } 150 | }, 151 | { 152 | missing: '69a5411f3ec4c2cd7e83bf61130e15654d69b76585dcca6e1dcc986a50b96cad' 153 | } 154 | ], 155 | proof: [ 156 | { 157 | path: '000', 158 | hash: '990c60568f8aaba6f42fa082c3465e46d6fe294396eb545ba4d3b0696e3f1f86' 159 | }, 160 | { 161 | path: '0011', 162 | hash: '89d4d997f806056d9d390a4e70ab4054d652a1eed345723b9af44e5fe87a309b' 163 | }, 164 | { 165 | path: '0100101001110111101001000111001011101100001110010000101001110001011100011110110010111111001001001001010110001000101110011001000100100000011101011001010110001010000001111010000011010011010010000111110111100111110100100111010110111011010010011001100101011010', 166 | hash: 'eecd7bf7dc58b367dd059c8f6c13c8b7674c244e2f22e03af91c7d45bc4e85fc' 167 | }, 168 | { 169 | path: '0110001100101011001000110110101110110111101100100101011011011111101010001010000100110011001000110011100111000100001001110110011100000101001110100011101110101010111101001110011011111111111001101000001100111000101011101010010000011000011000000110001111101011', 170 | hash: 'd156aba4c4e5c63124ac52295466e935ba5d2282a64082c85841e32bbf5b4c97' 171 | }, 172 | { 173 | path: '0110100101001001110100110101000110101000111000000011000100111111111111100010100001001101111111011010011111010001011011011100010001011100101010011111011011011110101011101010001101110100110001000101011110011101101001000101100011000011111110010011101010111100', 174 | hash: 'a22631cc982f32971febdb758163585fcba1fa60ac247cdca9d7def498ded678' 175 | }, 176 | { 177 | path: '0111101011101010010101110011000111011111000001000001111011110100110010101101111101110101001010110001001010111100001011001010011111111110001100111001001011001001101111110001010010101000100010111011101100110010000101110101111100101000110010101100100100001100', 178 | hash: '6843d354b2b761a346c5e341128b30beb8c5958783ac131de60d1352741d813c' 179 | }, 180 | { 181 | path: '1000011110011001110000000100011001011000011110011011000110110110011000111010111100001010010010001011001011111110110000100000000110110110110000101010100011111010111000010111101110010100010011011101000000011101011101011011011100100000111010000001010000001101', 182 | hash: 'dd0a44b99e08e3f7eeb17c2f14487ae5ec764adbc6245ef09a7e0c6bfd89b680' 183 | }, 184 | { 185 | path: '1001111101100101110010000101111110001110111001111101001001000001100111100101001110101101001100101101001001011101000100101101001101001110010100100011001010011100110011110001111101000110010011001100111011011100011110110101100001100100111011000010100100101010', 186 | hash: '3bb6a394e0f69404bf8594f5fc42a2a5275b1d98f86bb766e4091597f4df7803' 187 | }, 188 | { 189 | path: '11', 190 | hash: '32a61b662d8a1720622f524162323f2c01bb6e706163e03a0724cfaa94814ebf' 191 | } 192 | ] 193 | } 194 | 195 | proof.entries.forEach(({ value }) => convertPublicKey(value)) 196 | 197 | // Create a `MapProof` instance. Note the use of `MapProof.rawKey`. 198 | proof = new MapProof(proof, MapProof.rawKey(PublicKey), Wallet) 199 | expect(proof.entries.size).to.equal(2) 200 | expect(proof.missingKeys.size).to.equal(2) 201 | expect(proof.merkleRoot).to.equal('facb561c29cc1aeac8bdd9a101a6644f8e49fa24a23c835723e8039e00979949') 202 | -------------------------------------------------------------------------------- /src/blockchain/merkle.js: -------------------------------------------------------------------------------- 1 | import bigInt from 'big-integer' 2 | import binarySearch from 'binary-search' 3 | import { Hash } from '../types/hexadecimal' 4 | import { hexadecimalToUint8Array } from '../types/convert' 5 | import { hash, HASH_LENGTH } from '../crypto' 6 | import { BLOB_PREFIX, LIST_PREFIX, LIST_BRANCH_PREFIX } from './constants' 7 | 8 | /** 9 | * @typedef IEntry 10 | * @property {number} height Height of the entry 11 | * @property {number} index Index of the entry on the level 12 | * @property {string} hash SHA-256 digest of the entry 13 | */ 14 | 15 | // Maximum height of a valid Merkle tree 16 | const MAX_TREE_HEIGHT = 58 17 | 18 | export class ListProof { 19 | constructor ({ proof, entries, length }, valueType) { 20 | if (!valueType || typeof valueType.serialize !== 'function') { 21 | throw new TypeError('No `serialize` method in the value type') 22 | } 23 | this.valueType = valueType 24 | 25 | this.proof = parseProof(proof) 26 | this.entries = parseEntries(entries, valueType) 27 | this.length = length 28 | 29 | let rootHash 30 | if (this.length === 0) { 31 | if (this.proof.length === 0 && this.entries.length === 0) { 32 | rootHash = '0000000000000000000000000000000000000000000000000000000000000000' 33 | } else { 34 | throw new ListProofError('malformedEmptyProof') 35 | } 36 | } else { 37 | const completeProof = [...this.entries, ...this.proof] 38 | rootHash = collect(completeProof, this.length) 39 | } 40 | this.merkleRoot = hashList(rootHash, this.length) 41 | } 42 | } 43 | 44 | function parseProof (proof) { 45 | if (!Array.isArray(proof)) { 46 | throw new ListProofError('malformedProof') 47 | } 48 | 49 | const validEntries = proof.every(({ index, height, hash }) => { 50 | return /^[0-9a-f]{64}$/i.test(hash) && 51 | Number.isInteger(index) && 52 | Number.isInteger(height) && 53 | height > 0 && 54 | height <= MAX_TREE_HEIGHT 55 | }) 56 | if (!validEntries) { 57 | throw new ListProofError('malformedProof') 58 | } 59 | 60 | // Check ordering of proof entries. 61 | for (let i = 0; i + 1 < proof.length; i++) { 62 | const [prev, next] = [proof[i], proof[i + 1]] 63 | if (prev.height > next.height || (prev.height === next.height && prev.index >= next.index)) { 64 | throw new ListProofError('invalidProofOrdering') 65 | } 66 | } 67 | 68 | return proof 69 | } 70 | 71 | /** 72 | * Performs some preliminary checks on list values and computes their hashes. 73 | * 74 | * @param entries 75 | * @param valueType 76 | * @returns {IEntry[]} parsed entries 77 | */ 78 | function parseEntries (entries, valueType) { 79 | if (!Array.isArray(entries)) { 80 | throw new ListProofError('malformedEntries') 81 | } 82 | 83 | // Check ordering of values. 84 | for (let i = 0; i + 1 < entries.length; i++) { 85 | const [prev, next] = [entries[i], entries[i + 1]] 86 | if (prev[0] >= next[0]) { 87 | throw new ListProofError('invalidValuesOrdering') 88 | } 89 | } 90 | 91 | return entries.map(([index, value]) => { 92 | if (!Number.isInteger(index)) { 93 | throw new ListProofError('malformedEntries') 94 | } 95 | 96 | return { 97 | index, 98 | height: 0, 99 | value, 100 | hash: hash([BLOB_PREFIX, ...valueType.serialize(value, [], 0)]) 101 | } 102 | }) 103 | } 104 | 105 | /** 106 | * Collects entries into a single hash of the Merkle tree. 107 | * @param {IEntry[]} entries 108 | * @param {number} listLength 109 | * @returns {string} hash of the Merkle tree 110 | */ 111 | function collect (entries, listLength) { 112 | const treeHeight = calcHeight(listLength) 113 | 114 | // Check that height is appropriate. Since we've checked ordering of `entries`, 115 | // we only check that the height of the last entry does not exceed the expected 116 | // value. 117 | if (entries[entries.length - 1].height >= treeHeight) { 118 | throw new ListProofError('unexpectedHeight') 119 | } 120 | 121 | // Check that indexes of `entries` are appropriate. 122 | entries.forEach(({ height, index }) => { 123 | const divisor = (height === 0) ? 1 : (2 ** (height - 1)) 124 | const maxIndexOnLevel = Math.floor((listLength - 1) / divisor) 125 | if (index > maxIndexOnLevel) { 126 | throw new ListProofError('unexpectedIndex') 127 | } 128 | }) 129 | 130 | // Copy values to the first layer (we've calculated their hashes already). 131 | let layer = spliceLayer(entries, 0).map(({ index, hash }) => ({ 132 | height: 1, 133 | index, 134 | hash 135 | })) 136 | let lastIndex = listLength - 1 137 | for (let height = 1; height < treeHeight; height++) { 138 | // Merge with the next layer. 139 | const nextLayer = spliceLayer(entries, height) 140 | layer = mergeLayers(layer, nextLayer) 141 | // Zip the entries on the layer. 142 | hashLayer(layer, lastIndex) 143 | lastIndex = Math.floor(lastIndex / 2) 144 | } 145 | return layer[0].hash 146 | } 147 | 148 | /** 149 | * Splices entries with the specified height from the beginning of the array. 150 | * The entries are modified in place. 151 | * 152 | * @param {IEntry[]} entries 153 | * @param {number} height 154 | * @returns {IEntry[]} spliced entries 155 | */ 156 | function spliceLayer (entries, height) { 157 | const index = binarySearch(entries, height + 1, ({ height, index }, needleHeight) => { 158 | // Assume that all entries with `height === needleHeight` are larger than our needle. 159 | const x = (needleHeight !== height) ? (height - needleHeight) : 1 160 | return x 161 | }) 162 | /* istanbul ignore next: should never be triggered */ 163 | if (index >= 0) { 164 | throw new Error('Internal error while verifying list proof') 165 | } 166 | 167 | const greaterIndex = -index - 1 168 | return entries.splice(0, greaterIndex) 169 | } 170 | 171 | /** 172 | * Merges two sorted arrays together. 173 | * 174 | * @param {IEntry[]} xs 175 | * @param {IEntry[]} ys 176 | * @returns {IEntry[]} 177 | */ 178 | function mergeLayers (xs, ys) { 179 | let xIndex = 0 180 | let yIndex = 0 181 | const output = [] 182 | while (xIndex < xs.length || yIndex < ys.length) { 183 | const [x, y] = [xs[xIndex], ys[yIndex]] 184 | if (!x) { 185 | output.push(y) 186 | yIndex++ 187 | } else if (!y) { 188 | output.push(x) 189 | xIndex++ 190 | } else if (x.index < y.index) { 191 | output.push(x) 192 | xIndex++ 193 | } else if (x.index > y.index) { 194 | output.push(y) 195 | yIndex++ 196 | } else { 197 | // x.index === y.index 198 | throw new ListProofError('duplicateHash') 199 | } 200 | } 201 | return output 202 | } 203 | 204 | /** 205 | * Elevates the layer to the next level by zipping pairs of entries together. 206 | * 207 | * @param {IEntry[]} layer 208 | * @param {number} lastIndex 209 | */ 210 | function hashLayer (layer, lastIndex) { 211 | for (let i = 0; i < layer.length; i += 2) { 212 | const [left, right] = [layer[i], layer[i + 1]] 213 | let hash 214 | if (right) { 215 | // To be able to zip two hashes on the layer, they need to be adjacent to each other, 216 | // and the first of them needs to have an even index. 217 | if (left.index % 2 !== 0 || right.index !== left.index + 1) { 218 | throw new ListProofError('missingHash') 219 | } 220 | hash = hashNode(left.hash, right.hash) 221 | } else { 222 | // If there is an odd number of hashes on the layer, the solitary hash must have 223 | // the greatest possible index. 224 | if (lastIndex % 2 === 1 || left.index !== lastIndex) { 225 | throw new ListProofError('missingHash') 226 | } 227 | hash = hashNode(left.hash) 228 | } 229 | 230 | layer[i / 2] = { 231 | height: left.height + 1, 232 | index: left.index / 2, 233 | hash 234 | } 235 | } 236 | layer.length = Math.ceil(layer.length / 2) 237 | } 238 | 239 | function hashNode (leftHash, maybeRightHash) { 240 | const buffer = [LIST_BRANCH_PREFIX] 241 | Hash.serialize(leftHash, buffer, buffer.length) 242 | if (maybeRightHash) { 243 | Hash.serialize(maybeRightHash, buffer, buffer.length) 244 | } 245 | return hash(buffer) 246 | } 247 | 248 | /** 249 | * Computes hash of the `ProofListIndex` given its length and root hash. 250 | * @param {string} rootHash 251 | * @param {number} length 252 | * @returns {string} 253 | */ 254 | function hashList (rootHash, length) { 255 | const buffer = new Uint8Array(9 + HASH_LENGTH) 256 | buffer[0] = LIST_PREFIX 257 | // Set bytes 1..9 as little-endian list length 258 | let quotient = bigInt(length) 259 | for (let byte = 1; byte < 9; byte++) { 260 | let remainder 261 | ({ quotient, remainder } = quotient.divmod(256)) 262 | buffer[byte] = remainder 263 | } 264 | buffer.set(hexadecimalToUint8Array(rootHash), 9) 265 | return hash(buffer) 266 | } 267 | 268 | /** 269 | * Calculates height of a Merkle tree given its length. 270 | * @param {bigInt} count 271 | * @return {number} 272 | */ 273 | function calcHeight (count) { 274 | let i = 0 275 | while (bigInt(2).pow(i).lt(count)) { 276 | i++ 277 | } 278 | return i + 1 279 | } 280 | 281 | export class ListProofError extends Error { 282 | constructor (type) { 283 | switch (type) { 284 | case 'malformedProof': 285 | case 'invalidProofOrdering': 286 | super('malformed `proof` part of the proof') 287 | break 288 | case 'malformedEntries': 289 | case 'invalidValuesOrdering': 290 | super('malformed `entries` in the proof') 291 | break 292 | case 'unexpectedHeight': 293 | case 'unexpectedIndex': 294 | super('proof contains a branch where it is impossible according to list length') 295 | break 296 | case 'duplicateHash': 297 | super('proof contains redundant entries') 298 | break 299 | case 'missingHash': 300 | super('proof does not contain information to restore index hash') 301 | break 302 | default: 303 | super(type) 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/blockchain/merkle-patricia.js: -------------------------------------------------------------------------------- 1 | import binarySearch from 'binary-search' 2 | 3 | import { hash, HASH_LENGTH } from '../crypto' 4 | import { Hash } from '../types/hexadecimal' 5 | import { BLOB_PREFIX, MAP_PREFIX, MAP_BRANCH_PREFIX } from './constants' 6 | import ProofPath from './ProofPath' 7 | import { hexadecimalToUint8Array } from '../types' 8 | 9 | /** 10 | * Proof of existence and/or absence of certain elements from a Merkelized 11 | * map index. 12 | */ 13 | export class MapProof { 14 | /** 15 | * Converts a key type to a raw representation, in which keys are not hashed before 16 | * Merkle Patricia tree construction. 17 | * 18 | * @param keyType 19 | */ 20 | static rawKey (keyType) { 21 | if (!keyType || typeof keyType.serialize !== 'function') { 22 | throw new TypeError('Invalid key type; pass a type with a `serialize` function') 23 | } 24 | 25 | return { 26 | hash (data) { 27 | const bytes = keyType.serialize(data, [], 0) 28 | if (bytes.length !== HASH_LENGTH) { 29 | throw new Error(`Invalid raw key; raw keys should have ${HASH_LENGTH}-byte serialization`) 30 | } 31 | return bytes 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Creates a new instance of a proof. 38 | * 39 | * @param {Object} json 40 | * JSON object containing (untrusted) proof 41 | * @param {{serialize: (any) => Array}} keyType 42 | * Type of keys used in the underlying Merkelized map. Usually, `PublicKey` 43 | * or `Hash`. The keys must be serializable. 44 | * @param {{serialize: (any) => Array}} valueType 45 | * Type of values used in the underlying Merkelized map. Usually, it should 46 | * be a type created with the `newType` function. The type must be serializable. 47 | * @throws {MapProofError} 48 | * if the proof is malformed 49 | */ 50 | constructor (json, keyType, valueType) { 51 | this.proof = parseProof(json.proof) 52 | this.entries = parseEntries(json.entries, keyType, valueType) 53 | 54 | if (!keyType) { 55 | throw new TypeError('No key type provided') 56 | } 57 | if (typeof keyType.serialize !== 'function' && typeof keyType.hash !== 'function') { 58 | throw new TypeError('No `serialize` or `hash` method in the key type') 59 | } 60 | this.keyType = keyType 61 | 62 | if (!valueType || typeof valueType.serialize !== 'function') { 63 | throw new TypeError('No `serialize` method in the value type') 64 | } 65 | this.valueType = valueType 66 | 67 | precheckProof.call(this) 68 | 69 | const completeProof = this.proof 70 | .concat(this.entries) 71 | .sort(({ path: pathA }, { path: pathB }) => pathA.compare(pathB)) 72 | 73 | // This check is required as duplicate paths can be introduced by entries 74 | // (further, it's generally possible that two different entry keys lead 75 | // to the same `ProofPath`). 76 | for (let i = 1; i < completeProof.length; i++) { 77 | const [{ path: pathA }, { path: pathB }] = [ 78 | completeProof[i - 1], 79 | completeProof[i] 80 | ] 81 | 82 | if (pathA.compare(pathB) === 0) { 83 | throw new MapProofError('duplicatePath', pathA) 84 | } 85 | } 86 | 87 | const rootHash = hexadecimalToUint8Array(collect(completeProof.filter(({ hash }) => !!hash))) 88 | this.merkleRoot = hash([MAP_PREFIX, ...rootHash]) 89 | this.missingKeys = new Set( 90 | this.entries 91 | .filter(e => e.missing !== undefined) 92 | .map(({ missing }) => missing) 93 | ) 94 | this.entries = new Map( 95 | this.entries 96 | .filter(e => e.key !== undefined) 97 | .map(({ key, value }) => [key, value]) 98 | ) 99 | } 100 | } 101 | 102 | function parseProof (proof) { 103 | if (!Array.isArray(proof)) { 104 | throw new MapProofError('malformedProof') 105 | } 106 | 107 | const validEntries = proof.every(({ path, hash }) => { 108 | return /^[01]{1,256}$/.test(path) && 109 | /^[0-9a-f]{64}$/i.test(hash) 110 | }) 111 | if (!validEntries) { 112 | throw new MapProofError('malformedProof') 113 | } 114 | 115 | return proof.map(({ path, hash }) => ({ 116 | path: new ProofPath(path), 117 | hash 118 | })) 119 | } 120 | 121 | function parseEntries (entries, keyType, valueType) { 122 | function createPath (data) { 123 | const keyBytes = (typeof keyType.hash === 'function') 124 | ? keyType.hash(data) 125 | : hash(keyType.serialize(data, [], 0)) 126 | 127 | let bytes 128 | if (typeof keyBytes === 'string') { 129 | bytes = hexadecimalToUint8Array(keyBytes) 130 | } else { 131 | bytes = new Uint8Array(keyBytes) 132 | } 133 | return new ProofPath(bytes) 134 | } 135 | 136 | if (!Array.isArray(entries)) { 137 | throw new MapProofError('malformedEntries') 138 | } 139 | 140 | return entries.map(({ missing, key, value }) => { 141 | if (missing === undefined && (key === undefined || value === undefined)) { 142 | throw new MapProofError('unknownEntryType') 143 | } 144 | if (missing !== undefined && (key !== undefined || value !== undefined)) { 145 | throw new MapProofError('ambiguousEntryType') 146 | } 147 | 148 | if (missing !== undefined) { 149 | return { 150 | missing, 151 | path: createPath(missing) 152 | } 153 | } 154 | 155 | return { 156 | key, 157 | value, 158 | path: createPath(key), 159 | hash: hash([BLOB_PREFIX, ...valueType.serialize(value, [], 0)]) 160 | } 161 | }) 162 | } 163 | 164 | /** 165 | * @this {MapProof} 166 | */ 167 | function precheckProof () { 168 | // Check that entries in proof are in increasing order 169 | for (let i = 1; i < this.proof.length; i++) { 170 | const [{ path: prevPath }, { path }] = [this.proof[i - 1], this.proof[i]] 171 | 172 | switch (prevPath.compare(path)) { 173 | case -1: 174 | if (path.startsWith(prevPath)) { 175 | throw new MapProofError('embeddedPaths', prevPath, path) 176 | } 177 | break 178 | case 0: 179 | throw new MapProofError('duplicatePath', path) 180 | case 1: 181 | throw new MapProofError('invalidOrdering', prevPath, path) 182 | } 183 | } 184 | 185 | // Check that no entry has a prefix among the paths in the proof entries. 186 | // In order to do this, it suffices to locate the closest smaller path 187 | // in the proof entries and check only it. 188 | this.entries.forEach(({ path: keyPath }) => { 189 | const index = binarySearch(this.proof, keyPath, ({ path }, needle) => { 190 | return path.compare(needle) 191 | }) 192 | 193 | if (index >= 0) { 194 | throw new MapProofError('duplicatePath', keyPath) 195 | } 196 | 197 | const insertionIndex = -index - 1 198 | if (insertionIndex > 0) { 199 | const prevPath = this.proof[insertionIndex - 1].path 200 | if (keyPath.startsWith(prevPath)) { 201 | throw new MapProofError('embeddedPaths', prevPath, keyPath) 202 | } 203 | } 204 | }) 205 | } 206 | 207 | function serializeBranchNode (leftHash, rightHash, leftPath, rightPath) { 208 | const buffer = [MAP_BRANCH_PREFIX] 209 | Hash.serialize(leftHash, buffer, buffer.length) 210 | Hash.serialize(rightHash, buffer, buffer.length) 211 | leftPath.serialize(buffer) 212 | rightPath.serialize(buffer) 213 | return buffer 214 | } 215 | 216 | function serializeIsolatedNode (path, hash) { 217 | const buffer = [MAP_BRANCH_PREFIX] 218 | path.serialize(buffer) 219 | Hash.serialize(hash, buffer, buffer.length) 220 | return buffer 221 | } 222 | 223 | function collect (entries) { 224 | function hashIsolatedNode ({ path, hash: valueHash }) { 225 | const buffer = serializeIsolatedNode(path, valueHash) 226 | return hash(buffer) 227 | } 228 | 229 | function hashBranch (left, right) { 230 | const buffer = serializeBranchNode(left.hash, right.hash, left.path, right.path) 231 | return hash(buffer) 232 | } 233 | 234 | function fold (contour, lastPrefix) { 235 | const lastEntry = contour.pop() 236 | const penultimateEntry = contour.pop() 237 | 238 | contour.push({ 239 | path: lastPrefix, 240 | hash: hashBranch(penultimateEntry, lastEntry) 241 | }) 242 | 243 | return (contour.length > 1) 244 | ? lastPrefix.commonPrefix(contour[contour.length - 2].path) 245 | : null 246 | } 247 | 248 | switch (entries.length) { 249 | case 0: 250 | return '0000000000000000000000000000000000000000000000000000000000000000' 251 | 252 | case 1: 253 | if (!entries[0].path.isTerminal()) { 254 | throw new MapProofError('nonTerminalNode', entries[0].path) 255 | } 256 | return hashIsolatedNode(entries[0]) 257 | 258 | default: { 259 | const contour = [] 260 | 261 | // invariant: equal to the common prefix of the 2 last nodes in the contour 262 | let lastPrefix = entries[0].path.commonPrefix(entries[1].path) 263 | contour.push(entries[0], entries[1]) 264 | 265 | for (let i = 2; i < entries.length; i++) { 266 | const entry = entries[i] 267 | const newPrefix = entry.path.commonPrefix(contour[contour.length - 1].path) 268 | 269 | while (contour.length > 1 && newPrefix.bitLength < lastPrefix.bitLength) { 270 | const foldedPrefix = fold(contour, lastPrefix) 271 | if (foldedPrefix) { 272 | lastPrefix = foldedPrefix 273 | } 274 | } 275 | 276 | contour.push(entry) 277 | lastPrefix = newPrefix 278 | } 279 | 280 | while (contour.length > 1) { 281 | lastPrefix = fold(contour, lastPrefix) 282 | } 283 | return contour[0].hash 284 | } 285 | } 286 | } 287 | 288 | /** 289 | * Error indicating a malformed `MapProof`. 290 | */ 291 | export class MapProofError extends Error { 292 | constructor (type, ...args) { 293 | switch (type) { 294 | case 'malformedProof': 295 | super('malformed `proof` part of the proof') 296 | break 297 | case 'malformedEntries': 298 | case 'unknownEntryType': 299 | case 'ambiguousEntryType': 300 | super('malformed `entries` part of the proof') 301 | break 302 | case 'embeddedPaths': 303 | super(`embedded paths in proof: ${args[0]} is a prefix of ${args[1]}`) 304 | break 305 | case 'duplicatePath': 306 | super(`duplicate ${args[0]} in proof`) 307 | break 308 | case 'invalidOrdering': 309 | super('invalid path ordering') 310 | break 311 | case 'nonTerminalNode': 312 | super('non-terminal isolated node in proof') 313 | break 314 | default: 315 | super(type) 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.18.4 (January 11, 2021) 4 | 5 | * Update outdated dependencies 6 | 7 | ## 0.18.3 (February 7, 2020) 8 | 9 | * Support of Exonum core 1.0.0 10 | 11 | ## 0.18.1 (January 24, 2020) 12 | 13 | * Support of Exonum core 0.13 14 | * Support of `CallerAddress` 15 | * Support unhashed `MapProof` keys 16 | 17 | ## 0.16.9 (June 5, 2019) 18 | 19 | * Fix axios security alert. 20 | * Fix js-yaml security alert. 21 | 22 | ## 0.16.6 (Jan 11, 2019) 23 | 24 | * Fix links to Exonum documentation. ([#152][pr-152]) 25 | 26 | ## 0.16.5 (Jan 9, 2019) 27 | 28 | * Export compiled protobuf stubs as `protocol`. ([#151][pr-151]) 29 | 30 | ## 0.16.4 (Dec 13, 2018) 31 | 32 | * Re-build [package-lock.json](package-lock.json) file. ([#146][pr-146]) 33 | 34 | ## 0.16.3 (Dec 13, 2018) 35 | 36 | * Fix issue with serialization of empty strings and zero-valued numbers. ([#145][pr-145]) 37 | 38 | ## 0.16.2 (Dec 12, 2018) 39 | 40 | * Add proto stubs into npm package. ([#143][pr-143]) 41 | 42 | ## 0.16.1 (Dec 12, 2018) 43 | 44 | * Hot fix for broken package install through npm. ([#142][pr-142]) 45 | 46 | ## 0.16.0 (Dec 12, 2018) 47 | 48 | * Use protobuf serialization format instead of Exonum serialization format. ([#141][pr-141]) 49 | * Rework `newType` method syntax. ([#141][pr-141]) 50 | * Rework `newTransaction` method syntax. ([#141][pr-141]) 51 | * Rework `verifyBlock` method syntax to return Promise. ([#140][pr-140]) 52 | 53 | ## 0.15.0 (Oct 9, 2018) 54 | 55 | * Remove `cutSignature` field from `serialize` method. ([#139][pr-139]) 56 | * Rename `newMessage` method into `newTransaction`. ([#139][pr-139]) 57 | * Rename `isInstanceofOfNewMessage` method into `isTransaction`. ([#139][pr-139]) 58 | * Rename `isInstanceofOfNewArray` method into `isNewArray`. ([#139][pr-139]) 59 | 60 | ## 0.14.0 (Oct 9, 2018) 61 | 62 | * Update `Message` and `Precommit` serialization format. Change `newMessage` method syntax. ([#136][pr-136]) 63 | Serialization format is changed in next release of the Exonum core. 64 | * Change `send` and `sendQueue` methods syntax. ([#136][pr-136]) 65 | 66 | ## 0.13.0 (Oct 5, 2018) 67 | 68 | * Add a new `verifyTable` method to verify table existence in the root tree. ([#138][pr-138]) 69 | 70 | ## 0.12.5 (Oct 5, 2018) 71 | 72 | * Change package used to replace library version via Grunt. ([#137][pr-137]) 73 | 74 | ## 0.12.4 (Aug 22, 2018) 75 | 76 | * Fix `send` method when `attempts` to be `0`. ([#133][pr-133]) 77 | 78 | ## 0.12.3 (Jul 31, 2018) 79 | 80 | * Fix broken `MapProof` in Internet Explorer. ([#126][pr-126]) 81 | 82 | ## 0.12.2 (Jul 25, 2018) 83 | 84 | * Rework `send` method to ignore wrong and error responses from blockchain node. 85 | Swap `timeout` and `attempts` parameters int the `send` method. 86 | Allow `attempts` to be `0`. ([#122][pr-122]) 87 | 88 | ## 0.12.1 (Jul 20, 2018) 89 | 90 | * Add `timeout` and `attempts` parameters to the `send` method. ([#116][pr-116]) 91 | 92 | ## 0.11.1 (Jul 10, 2018) 93 | 94 | * Updated explorer API. ([#114][pr-114]) 95 | New web API based on `actix-web` implemented in Exonum core [0.9][release-0.9]. 96 | 97 | ## 0.11.0 (Jul 9, 2018) 98 | 99 | * Remove field `schema_version` from `Block` structure. ([#115][pr-115]) 100 | Field is removed in Exonum core [0.9][release-0.9]. 101 | 102 | ## 0.10.2 (Jun 29, 2018) 103 | 104 | * Rework version import to fix library babelify using webpack. ([#112][pr-112]) 105 | 106 | ## 0.10.1 (Jun 25, 2018) 107 | 108 | * Fix serialization of custom data types of 2 and more nesting level. ([#110][pr-110]) 109 | 110 | ## 0.10.0 (Jun 20, 2018) 111 | 112 | * Add serialization support of decimal type (`Decimal`). ([#108][pr-108]) 113 | Decimal type is added into Exonum core in [0.8][release-0.8]. 114 | 115 | ## 0.9.0 (Jun 18, 2018) 116 | 117 | * Add support of `UUID` serialization. ([#97][pr-97]) 118 | 119 | ## 0.8.2 (May 23, 2018) 120 | 121 | * Refactor `send` method to remove dependency onto service response format during pushing the transaction. ([#103][pr-103]) 122 | 123 | ## 0.8.1 (May 21, 2018) 124 | 125 | * Add a new `version` property to check library version. ([#101][pr-101]) 126 | * Cover the case when the blockchain explorer down or return the unexpected response. ([#102][pr-102]) 127 | 128 | ## 0.8.0 (May 15, 2018) 129 | 130 | * Add a new `send` method to send transaction to the blockchain. ([#98][pr-98]) 131 | * Add a new `sendQueue` method to send multiple transactions to the blockchain. ([#98][pr-98]) 132 | 133 | ## 0.7.3 (Apr 30, 2018) 134 | 135 | * Update third-party dependencies to fix potential security vulnerabilities. ([#96][pr-96]) 136 | 137 | ## 0.7.2 (Apr 11, 2018) 138 | 139 | * Add static `hash` method to `Exonum.Hash` primitive type. ([#94][pr-94]) 140 | 141 | ## 0.7.1 (Apr 11, 2018) 142 | 143 | * Fix missed `MapProof` method. ([#93][pr-93]) 144 | 145 | ## 0.7.0 (Apr 11, 2018) 146 | 147 | * Proofs of existence in Merkle Patricia tree have been replaced with Map proof. ([#85][pr-85]) 148 | Method is replaced in Exonum core in [0.7][release-0.7]. 149 | * `network_id` attribute has been removed from custom data types, transactions and proofs. ([#90][pr-90]) 150 | Attribute is removed in Exonum core in [0.7][release-0.7]. 151 | 152 | ## 0.6.1 (Apr 4, 2018) 153 | 154 | * Add Uint8Array to Binary String convertor (`uint8ArrayToBinaryString` method). ([#88][pr-88]) 155 | 156 | ## 0.6.0 (Mar 24, 2018) 157 | 158 | * Custom data type and transaction no longer require manual `size`, `from` and `to` specification. 159 | This feature is added into Exonum core in [0.5][release-0.5]. ([#84][pr-84]) 160 | 161 | ## 0.5.0 (Mar 6, 2018) 162 | 163 | * Add serialization support of floating point types (`Float32` and `Float64`). ([#83][pr-83]) 164 | Floating point types are added into Exonum core in [0.5][release-0.5]. 165 | * Add [package-lock.json](package-lock.json). ([#81][pr-81]) 166 | 167 | ## 0.4.1 (Feb 23, 2018) 168 | 169 | * Fix issue with converting of Binary String to Uint8Array (`binaryStringToUint8Array` method). 170 | This problem also affected the validation of the Merkle Patricia tree. ([#80][pr-80]) 171 | 172 | ## 0.4.0 (Feb 9, 2018) 173 | 174 | * Change order of bytes and bits in the `DBKey` keys of Merkle Patricia. ([#78][pr-78]) 175 | Order is changed in Exonum core in [0.5][release-0.5]. 176 | * Extend usage examples and move them into separate files. ([#77][pr-77]) 177 | * Improve tests readability. ([#75][pr-75]) ([#76][pr-76]) 178 | 179 | ## 0.3.0 (Nov 20, 2017) 180 | 181 | * Remove `FixedBuffer` type because it is not supported by core by default. ([#71][pr-71]) 182 | 183 | ## 0.2.3 (Sep 27, 2017) 184 | 185 | * Fix issue with serialization of transactions. ([#70][pr-70]) 186 | 187 | ## 0.2.2 (Sep 26, 2017) 188 | 189 | * Fix issue with serialization of transactions. ([#69][pr-69]) 190 | 191 | ## 0.2.1 (Sep 20, 2017) 192 | 193 | * Add serialization support of array type (`newArray`). ([#63][pr-63]) 194 | * Change the way of `Array` and `String` serialization. ([#58][pr-58]) 195 | * Use `standard` lint rules. ([#64][pr-64]) ([#65][pr-65]) 196 | 197 | ## 0.2.0 (Aug 1, 2017) 198 | 199 | * Fix issue with Merkle Patricia Tree processing (`merklePatriciaProof` method). ([#53][pr-53]) 200 | 201 | ## 0.1.1 (Jul 21, 2017) 202 | 203 | * Add automatic publishing of new releases into npm via Travis CI. ([#54][pr-54]) 204 | 205 | ## 0.1.0 (Jul 18, 2017) 206 | 207 | The first release of JavaScript client for Exonum blockchain, 208 | matching [release 0.1][release-0.1] of the Exonum core repository. 209 | 210 | [release-0.9]: https://github.com/exonum/exonum/blob/master/CHANGELOG.md#090---2018-07-19 211 | [release-0.8]: https://github.com/exonum/exonum/blob/master/CHANGELOG.md#08---2018-05-31 212 | [release-0.7]: https://github.com/exonum/exonum/blob/master/CHANGELOG.md#07---2018-04-11 213 | [release-0.5]: https://github.com/exonum/exonum/blob/master/CHANGELOG.md#05---2018-01-30 214 | [release-0.1]: https://github.com/exonum/exonum/releases/tag/v0.1 215 | [pr-152]: https://github.com/exonum/exonum-client/pull/152 216 | [pr-151]: https://github.com/exonum/exonum-client/pull/151 217 | [pr-146]: https://github.com/exonum/exonum-client/pull/146 218 | [pr-145]: https://github.com/exonum/exonum-client/pull/145 219 | [pr-143]: https://github.com/exonum/exonum-client/pull/143 220 | [pr-142]: https://github.com/exonum/exonum-client/pull/142 221 | [pr-141]: https://github.com/exonum/exonum-client/pull/141 222 | [pr-140]: https://github.com/exonum/exonum-client/pull/140 223 | [pr-139]: https://github.com/exonum/exonum-client/pull/139 224 | [pr-138]: https://github.com/exonum/exonum-client/pull/138 225 | [pr-137]: https://github.com/exonum/exonum-client/pull/137 226 | [pr-136]: https://github.com/exonum/exonum-client/pull/136 227 | [pr-133]: https://github.com/exonum/exonum-client/pull/133 228 | [pr-126]: https://github.com/exonum/exonum-client/pull/126 229 | [pr-122]: https://github.com/exonum/exonum-client/pull/122 230 | [pr-116]: https://github.com/exonum/exonum-client/pull/116 231 | [pr-115]: https://github.com/exonum/exonum-client/pull/115 232 | [pr-114]: https://github.com/exonum/exonum-client/pull/114 233 | [pr-112]: https://github.com/exonum/exonum-client/pull/112 234 | [pr-110]: https://github.com/exonum/exonum-client/pull/110 235 | [pr-108]: https://github.com/exonum/exonum-client/pull/108 236 | [pr-103]: https://github.com/exonum/exonum-client/pull/103 237 | [pr-102]: https://github.com/exonum/exonum-client/pull/102 238 | [pr-101]: https://github.com/exonum/exonum-client/pull/101 239 | [pr-98]: https://github.com/exonum/exonum-client/pull/98 240 | [pr-97]: https://github.com/exonum/exonum-client/pull/97 241 | [pr-96]: https://github.com/exonum/exonum-client/pull/96 242 | [pr-94]: https://github.com/exonum/exonum-client/pull/94 243 | [pr-93]: https://github.com/exonum/exonum-client/pull/93 244 | [pr-90]: https://github.com/exonum/exonum-client/pull/90 245 | [pr-88]: https://github.com/exonum/exonum-client/pull/88 246 | [pr-85]: https://github.com/exonum/exonum-client/pull/85 247 | [pr-84]: https://github.com/exonum/exonum-client/pull/84 248 | [pr-83]: https://github.com/exonum/exonum-client/pull/83 249 | [pr-81]: https://github.com/exonum/exonum-client/pull/81 250 | [pr-80]: https://github.com/exonum/exonum-client/pull/80 251 | [pr-78]: https://github.com/exonum/exonum-client/pull/78 252 | [pr-77]: https://github.com/exonum/exonum-client/pull/77 253 | [pr-76]: https://github.com/exonum/exonum-client/pull/76 254 | [pr-75]: https://github.com/exonum/exonum-client/pull/75 255 | [pr-71]: https://github.com/exonum/exonum-client/pull/71 256 | [pr-70]: https://github.com/exonum/exonum-client/pull/70 257 | [pr-69]: https://github.com/exonum/exonum-client/pull/69 258 | [pr-65]: https://github.com/exonum/exonum-client/pull/65 259 | [pr-64]: https://github.com/exonum/exonum-client/pull/64 260 | [pr-63]: https://github.com/exonum/exonum-client/pull/63 261 | [pr-58]: https://github.com/exonum/exonum-client/pull/58 262 | [pr-54]: https://github.com/exonum/exonum-client/pull/54 263 | [pr-53]: https://github.com/exonum/exonum-client/pull/53 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /test/sources/data/map-proof.json: -------------------------------------------------------------------------------- 1 | { 2 | "valid-hash-value-short": { 3 | "expected": { 4 | "valueType": "UniqueHash", 5 | "merkleRoot": "cdfd14d957582f1f1bd357d59822ff61252b03907558a4bb2ceac26a2c62cc6d", 6 | "entries": [ 7 | [ 8 | "0dc615156ad345b7448e2e89152fc658529bf1e35f1b36735e99e8d79548845f", "7acfd59d96d77ed99169fdc0826a6ef8a6b45d87ac00536373882dfa7ba70925" 9 | ] 10 | ] 11 | }, 12 | "data": { 13 | "proof": [ 14 | { 15 | "path": "1111111001111110101100100110110100100001111110111001000000110110001101110100111101110000101010010111000001110110010110010101100101110011000111101111001100101011110001000000100111101001110100111001100011000100101100110100111110001010111100100011100100100001", 16 | "hash": "8af7733978ade756ee3e8adc56b12dc3d1958194a79ecabb5c10996caa621efc" 17 | } 18 | ], 19 | "entries": [ 20 | { 21 | "key": "0dc615156ad345b7448e2e89152fc658529bf1e35f1b36735e99e8d79548845f", 22 | "value": "7acfd59d96d77ed99169fdc0826a6ef8a6b45d87ac00536373882dfa7ba70925" 23 | } 24 | ] 25 | } 26 | }, 27 | "valid-empty": { 28 | "expected": { 29 | "valueType": "Uint16", 30 | "merkleRoot": "7324b5c72b51bb5d4c180f1109cfd347b60473882145841c39f3e584576296f9", 31 | "missingKeys": [ 32 | "f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4" 33 | ] 34 | }, 35 | "data": { 36 | "proof": [], 37 | "entries": [ 38 | { 39 | "missing": "f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4" 40 | } 41 | ] 42 | } 43 | }, 44 | "valid-hash-value": { 45 | "expected": { 46 | "valueType": "UniqueHash", 47 | "merkleRoot": "5532a80e0b8041df3297e785e8a1c46d9b18c229ecb9f991365f108b7174af35", 48 | "entries": [ 49 | [ 50 | "50c8ba3a6170f0a2fb6736ece8a603576ef6309a35e810911599bc6211b554a9", "2cc250e3c24b9d91f26b162d79b5031684607bf1c406581281953a14dc149c70" 51 | ] 52 | ] 53 | }, 54 | "data": { 55 | "proof": [ 56 | { 57 | "path": "0000000101010001101111011001000011001001001010100001110011110011110100101101010010101000110111001010011110100010000001010110110011101111110011011100010000100110111100101110110001101000111100011011011100011111111011100101011111001111001101100011010011110101", 58 | "hash": "9e6c7cc123f8f25d8368449688e61449162d152946fbe2cc71717812e6cedd50" 59 | }, { 60 | "path": "0000101010101110110000001010110110011000000001100011001110110111000101011001101100100100000010011111001000011101110010101110111001111111101111101110100011111110000111011111101111110011011010100100110101110010101000101110101000100110011100100010101101100001", 61 | "hash": "0000000000000000000000000000000000000000000000000000000000000000" 62 | }, { 63 | "path": "111", 64 | "hash": "cef524f9dc548e7813dba2d49b03056b128e3460d9996e793f2509b211de39f9" 65 | } 66 | ], 67 | "entries": [ 68 | { 69 | "key": "50c8ba3a6170f0a2fb6736ece8a603576ef6309a35e810911599bc6211b554a9", 70 | "value": "2cc250e3c24b9d91f26b162d79b5031684607bf1c406581281953a14dc149c70" 71 | } 72 | ] 73 | } 74 | }, 75 | "valid-not-found": { 76 | "expected": { 77 | "valueType": "Uint16", 78 | "merkleRoot": "c784568a8b3f2143624aeacd5d9613faad4e43addbfc539b0a52a69484f8517f", 79 | "missingKeys": [ 80 | "1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c" 81 | ] 82 | }, 83 | "data": { 84 | "entries": [ 85 | { 86 | "missing": "1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c" 87 | } 88 | ], 89 | "proof": [ 90 | { 91 | "path": "000", 92 | "hash": "9073cb8a4fde44a83524109229bfadf8e971e4f2bcc30801810f6accf1b5a648" 93 | }, { 94 | "path": "0010011", 95 | "hash": "dcd2c45a8e71602612bf37765b6b1f8dc063b455ad46280691b67624bf8046ff" 96 | }, { 97 | "path": "0011011111110101110000100100111101010110010111100011010100000100111111110101011011000011000000000100001000100000101101101100010101110110101101010001100000101110100011101101111100100101110100110010011011100011111011010110010100011111011010011000101111011010", 98 | "hash": "9be1fdaa5e58640e6c17dba7e734c56ec7ccab77f823933301661e3514284dd7" 99 | }, { 100 | "path": "0011110001011101100110100001110110011011011000001010011000100111110101001001010001011101000011101111000100001000001011110001000110101101100010100001111011011100001110001011101101000001111110111100001000101111111000001101110100101110010100111101101001010000", 101 | "hash": "a02b43536eb5fa7fd7251a4a24091d6d8a0e8ca73c1f2f0d508a0c76af983136" 102 | }, { 103 | "path": "01", 104 | "hash": "ba6ec397eb9ffb7d6e27404e8e904c8841f4552623b56d5ae8e5cd4e47448e46" 105 | }, { 106 | "path": "1", 107 | "hash": "5fca0d4856f189a812c35162fd392db3ab767e74421baa4e890e0144d7100229" 108 | } 109 | ] 110 | } 111 | }, 112 | "valid-single-wallet": { 113 | "expected": { 114 | "valueType": "Wallet", 115 | "merkleRoot": "b662e771af954695cd8ab0df69ad66a1e064939e547fdd209a28217055f9a34a", 116 | "entries": [ 117 | [ 118 | "097bae292e571a32d86dc5e31a19b04f086b51b5d27b0455ad514931e8a7261d", { 119 | "pub_key": { 120 | "data": [ 121 | 9, 123, 174, 41, 46, 87, 26, 50, 216, 109, 197, 227, 26, 25, 176, 79, 8, 107, 81, 181, 210, 123, 4, 85, 173, 81, 73, 49, 232, 167, 38, 29 122 | ] 123 | }, 124 | "name": "Ivan", 125 | "balance": 100, 126 | "history_len": 1, 127 | "history_hash": { 128 | "data": [ 129 | 0, 28, 144, 85, 19, 166, 108, 226, 176, 113, 101, 55, 10, 248, 221, 157, 110, 102, 246, 118, 221, 233, 225, 177, 85, 113, 106, 42, 128, 174, 189, 203 130 | ] 131 | } 132 | } 133 | ] 134 | ] 135 | }, 136 | "data": { 137 | "entries": [ 138 | { 139 | "key": "097bae292e571a32d86dc5e31a19b04f086b51b5d27b0455ad514931e8a7261d", 140 | "value": { 141 | "pub_key": { 142 | "data": [ 143 | 9, 123, 174, 41, 46, 87, 26, 50, 216, 109, 197, 227, 26, 25, 176, 79, 8, 107, 81, 181, 210, 123, 4, 85, 173, 81, 73, 49, 232, 167, 38, 29 144 | ] 145 | }, 146 | "name": "Ivan", 147 | "balance": 100, 148 | "history_len": 1, 149 | "history_hash": { 150 | "data": [ 151 | 0, 28, 144, 85, 19, 166, 108, 226, 176, 113, 101, 55, 10, 248, 221, 157, 110, 102, 246, 118, 221, 233, 225, 177, 85, 113, 106, 42, 128, 174, 189, 203 152 | ] 153 | } 154 | } 155 | } 156 | ], 157 | "proof": [] 158 | } 159 | }, 160 | "valid-timestamp": { 161 | "expected": { 162 | "valueType": "TimestampEntry", 163 | "merkleRoot": "a644c58d0f2d75a0fbfed5f3252fe820b2322e90c9af74729d30d5034a684c27", 164 | "entries": [ 165 | [ 166 | "a8434476db9b23f226a9e83fc4c1091861caad2014da1af0138d9bb041e7cd43", { 167 | "timestamp": { 168 | "content_hash": { 169 | "data": [ 170 | 168, 67, 68, 118, 219, 155, 35, 242, 38, 169, 232, 63, 196, 193, 9, 24, 97, 202, 173, 32, 20, 218, 26, 240, 19, 141, 155, 176, 65, 231, 205, 67 171 | ] 172 | }, 173 | "metadata": "test" 174 | }, 175 | "tx_hash": { 176 | "data": [ 177 | 159, 29, 223, 82, 178, 79, 21, 212, 45, 197, 66, 41, 14, 126, 189, 123, 26, 159, 84, 87, 173, 72, 216, 224, 117, 177, 245, 13, 123, 176, 0, 76 178 | ] 179 | }, 180 | "time": { 181 | "seconds": 1565078024, 182 | "nanos": 171304000 183 | } 184 | } 185 | ] 186 | ] 187 | }, 188 | "data": { 189 | "entries": [ 190 | { 191 | "key": "a8434476db9b23f226a9e83fc4c1091861caad2014da1af0138d9bb041e7cd43", 192 | "value": { 193 | "timestamp": { 194 | "content_hash": { 195 | "data": [ 196 | 168, 67, 68, 118, 219, 155, 35, 242, 38, 169, 232, 63, 196, 193, 9, 24, 97, 202, 173, 32, 20, 218, 26, 240, 19, 141, 155, 176, 65, 231, 205, 67 197 | ] 198 | }, 199 | "metadata": "test" 200 | }, 201 | "tx_hash": { 202 | "data": [ 203 | 159, 29, 223, 82, 178, 79, 21, 212, 45, 197, 66, 41, 14, 126, 189, 123, 26, 159, 84, 87, 173, 72, 216, 224, 117, 177, 245, 13, 123, 176, 0, 76 204 | ] 205 | }, 206 | "time": { 207 | "seconds": 1565078024, 208 | "nanos": 171304000 209 | } 210 | } 211 | } 212 | ], 213 | "proof": [ 214 | { 215 | "path": "1111000111010100001110110101010011111010010010111010111110011100101010111011110010011011000010010000011100101011011100010110010000110100010000110011101010001101100100000001110000110011001010001110111010011010010011110001111110010111000000100000010101000011", 216 | "hash": "56c4fbc350291cccdf09cc7ed20f5ea36188f55bc27a475ba528849df3ad41b9" 217 | } 218 | ] 219 | } 220 | }, 221 | "valid-timestamp-empty-metadata": { 222 | "expected": { 223 | "valueType": "TimestampEntry", 224 | "merkleRoot": "a03785d67c7012766ea7071ec6a01baae65155c3d600caecd983e7d226fd1949", 225 | "entries": [ 226 | [ 227 | "67fa96da3465c77befabd75b53027e39b35c79d0ed69a175fff5141516353cc3", { 228 | "timestamp": { 229 | "content_hash": { 230 | "data": [103, 250, 150, 218, 52, 101, 199, 123, 239, 171, 215, 91, 83, 2, 126, 57, 179, 92, 121, 208, 237, 105, 161, 117, 255, 245, 20, 21, 22, 53, 60, 195] 231 | }, 232 | "metadata": "" 233 | }, 234 | "tx_hash": { 235 | "data": [1, 82, 174, 121, 249, 59, 210, 153, 99, 239, 207, 182, 238, 120, 110, 82, 147, 209, 155, 149, 215, 25, 48, 250, 165, 9, 154, 4, 177, 170, 188, 208] 236 | }, 237 | "time": { 238 | "seconds": 1565078316, 239 | "nanos": 344791000 240 | } 241 | } 242 | ] 243 | ] 244 | }, 245 | "data": { 246 | "entries": [ 247 | { 248 | "key": "67fa96da3465c77befabd75b53027e39b35c79d0ed69a175fff5141516353cc3", 249 | "value": { 250 | "timestamp": { 251 | "content_hash": { 252 | "data": [103, 250, 150, 218, 52, 101, 199, 123, 239, 171, 215, 91, 83, 2, 126, 57, 179, 92, 121, 208, 237, 105, 161, 117, 255, 245, 20, 21, 22, 53, 60, 195] 253 | }, 254 | "metadata": "" 255 | }, 256 | "tx_hash": { 257 | "data": [1, 82, 174, 121, 249, 59, 210, 153, 99, 239, 207, 182, 238, 120, 110, 82, 147, 209, 155, 149, 215, 25, 48, 250, 165, 9, 154, 4, 177, 170, 188, 208] 258 | }, 259 | "time": { 260 | "seconds": 1565078316, 261 | "nanos": 344791000 262 | } 263 | } 264 | } 265 | ], 266 | "proof": [ 267 | { 268 | "path": "0001010111000010001000100110111011011011110110011100010001001111011001001001010100010111111111000010001110000011100100000001100010000110010100111011010100000100001010000101101101011000000011111100100010110001110110010000110110000010111001111011001111000010", 269 | "hash": "c7cb6bd43e2546acf3758bb4eadd7fadecdc2f3d2ef4e1cf1297f9a14bfce1db" 270 | }, { 271 | "path": "1111000111010100001110110101010011111010010010111010111110011100101010111011110010011011000010010000011100101011011100010110010000110100010000110011101010001101100100000001110000110011001010001110111010011010010011110001111110010111000000100000010101000011", 272 | "hash": "56c4fbc350291cccdf09cc7ed20f5ea36188f55bc27a475ba528849df3ad41b9" 273 | } 274 | ] 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /test/sources/cryptography.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | const $protobuf = require('protobufjs/light') 5 | const expect = require('chai').expect 6 | const Exonum = require('../../src') 7 | const proto = require('./proto/stubs') 8 | 9 | const Root = $protobuf.Root 10 | const Type = $protobuf.Type 11 | const Field = $protobuf.Field 12 | 13 | const keyPair = { 14 | publicKey: '84e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455', 15 | secretKey: '9aaa377f0880ae2aa6697ea45e6c26f164e923e73b31f52e6da0cf40798ca4c184e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455' 16 | } 17 | 18 | const root = new Root() 19 | const CreateTypeProtobuf = new Type('CreateType') 20 | .add(new Field('pub_key', 1, 'bytes')) 21 | .add(new Field('name', 2, 'string')) 22 | .add(new Field('balance', 3, 'int64')) 23 | root.define('CreateTypeProtobuf').add(CreateTypeProtobuf) 24 | 25 | const CreateType = Exonum.newType(CreateTypeProtobuf) 26 | const TYPE_DATA = { 27 | data: { 28 | pub_key: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 29 | name: 'Smart wallet', 30 | balance: 359120 31 | }, 32 | hash: 'b6effedef97bd9bfee70bfa0007029d33d4526fa932c3d0d58ffca9c6a135246', 33 | signed: 'e0b074a33c370142ed7728782f579dd8701f55b2730f82ad5174c174fdcb597db2a8f9e2e4a4bcfbae8960ab47ddf9a5de741dba69785302649b5affcac1bb07' 34 | } 35 | 36 | const CreateTransactionProtobuf = new Type('CreateTransaction') 37 | .add(new Field('pub_key', 1, 'bytes')) 38 | .add(new Field('name', 2, 'string')) 39 | .add(new Field('balance', 3, 'int64')) 40 | root.define('CreateTransactionProtobuf').add(CreateTransactionProtobuf) 41 | const CreateTransaction = new Exonum.Transaction({ 42 | serviceId: 130, 43 | methodId: 0, 44 | schema: CreateTransactionProtobuf 45 | }) 46 | 47 | const TX_DATA = { 48 | data: { 49 | pub_key: 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36', 50 | name: 'Smart wallet', 51 | balance: 359120 52 | }, 53 | hash: 'b765a4f6f2a08f6c61876a090a18e9cfead4c80f6bab0f9e1b18a14433a94ff1', 54 | signature: '1224254b30ed5fa2f6f3e61be3e8b0400669eec29b1e295c75a37090fada3b79' + 55 | 'e7e67f77f6175a16e3a50e8343d2b98a8a432e57667a41b6e706bfabaff4570b' 56 | } 57 | 58 | describe('Check cryptography', function () { 59 | describe('Get SHA256 hash', function () { 60 | it('should return hash of data of newType type', function () { 61 | const hash = Exonum.hash(TYPE_DATA.data, CreateType) 62 | expect(hash).to.equal(TYPE_DATA.hash) 63 | }) 64 | 65 | it('should return key pair from seed', function () { 66 | const keyPair1 = Exonum.fromSeed(Uint8Array.from([ 67 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 68 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 69 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 70 | 31, 32])) 71 | 72 | expect(keyPair1.publicKey).to.not.equal(null) 73 | expect(keyPair1.secretKey).to.not.equal(null) 74 | }) 75 | 76 | const expectedErrorMessage = 'unexpected type, use Uint8Array' 77 | 78 | it('should throw if anything different is passed instead of Uint8Array as a seed', function () { 79 | expect(() => Exonum.fromSeed(undefined)) 80 | .to.throw(TypeError, expectedErrorMessage) 81 | 82 | expect(() => Exonum.fromSeed(null)) 83 | .to.throw(TypeError, expectedErrorMessage) 84 | 85 | expect(() => Exonum.fromSeed()) 86 | .to.throw(TypeError, expectedErrorMessage) 87 | 88 | expect(() => Exonum.fromSeed('123')) 89 | .to.throw(TypeError, expectedErrorMessage) 90 | }) 91 | 92 | it('should throw bad size of Uint8Array is passed as a seed', function () { 93 | expect(() => Exonum.fromSeed(Uint8Array.from([1, 2, 3]))) 94 | .to.throw(Error, 'bad seed size') 95 | }) 96 | 97 | it('should return unique key pair from different seeds', function () { 98 | const keyPair1 = Exonum.fromSeed(Uint8Array.from([ 99 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 101 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 102 | 31, 32])) 103 | 104 | const keyPair2 = Exonum.fromSeed(Uint8Array.from([ 105 | 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 106 | 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 107 | 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 108 | 31, 32])) 109 | 110 | expect(keyPair1.publicKey).to.not.equal(null) 111 | expect(keyPair1.secretKey).to.not.equal(null) 112 | expect(keyPair2.publicKey).to.not.equal(null) 113 | expect(keyPair2.secretKey).to.not.equal(null) 114 | 115 | expect(keyPair1.publicKey).to.not.equal(keyPair2.publicKey) 116 | expect(keyPair1.secretKey).to.not.equal(keyPair2.secretKey) 117 | }) 118 | 119 | it('should create entity of newType', () => { 120 | const Wallet = Exonum.newType(proto.exonum.examples.cryptocurrency_advanced.Wallet) 121 | const data = { 122 | balance: 160, 123 | history_hash: { data: Exonum.hexadecimalToUint8Array('e6a4349d4c0f2e07c44f145cf765318ce14a0c869ca458e12bdae4724df853d4') }, 124 | history_len: 3, 125 | name: 'Jane Doe', 126 | pub_key: { data: Exonum.hexadecimalToUint8Array('e2ad49552fdf95302a725890139f9d1af69c49670d466366bd3af214da086dc4') } 127 | } 128 | 129 | const buffer = Wallet.serialize(data) 130 | 131 | expect('d7923cc44dafaad4d89a1ed46bbb2390cfd0b2c9c5a18ba1ceb15955750ff455').to.equal(Exonum.hash(buffer)) 132 | }) 133 | 134 | it('should create entity of newType with zero int', () => { 135 | const Issue = Exonum.newType(proto.exonum.examples.cryptocurrency_advanced.Issue) 136 | const data = { 137 | amount: 0, 138 | seed: 1 139 | } 140 | 141 | const buffer = Issue.serialize(data) 142 | 143 | expect('27c24fcb8474773e2af799d0848495ff053272d33c432dc26277993df45c9276') 144 | .to.equal(Exonum.hash(buffer)) 145 | }) 146 | 147 | it('should return hash of data of newType type using built-in method', function () { 148 | const hash = CreateType.hash(TYPE_DATA.data) 149 | expect(hash).to.equal(TYPE_DATA.hash) 150 | }) 151 | 152 | it('should return hash of data of Transaction type using built-in method', function () { 153 | const hash = CreateTransaction.create(TX_DATA.data, keyPair).hash() 154 | expect(hash).to.equal(TX_DATA.hash) 155 | }) 156 | 157 | it('should return hash of the array of 8-bit integers', function () { 158 | const buffer = CreateType.serialize(TYPE_DATA.data) 159 | const hash = Exonum.hash(buffer) 160 | expect(hash).to.equal(TYPE_DATA.hash) 161 | }) 162 | }) 163 | 164 | describe('Get ED25519 signature', function () { 165 | it('should return signature of the data of NewType type', function () { 166 | const signature = Exonum.sign(keyPair.secretKey, TYPE_DATA.data, CreateType) 167 | expect(signature).to.equal(TYPE_DATA.signed) 168 | }) 169 | 170 | it('should return signature of the data of NewType type using built-in method', function () { 171 | const signature = CreateType.sign(keyPair.secretKey, TYPE_DATA.data) 172 | expect(signature).to.equal(TYPE_DATA.signed) 173 | }) 174 | 175 | it('should return signature of the data of Transaction type', function () { 176 | const signature = Exonum.sign(keyPair.secretKey, TX_DATA.data, CreateTransaction) 177 | expect(signature).to.equal(TX_DATA.signature) 178 | }) 179 | 180 | it('should return signature of the data of Transaction type using built-in method', function () { 181 | const { signature } = CreateTransaction.create(TX_DATA.data, keyPair) 182 | expect(signature).to.equal(TX_DATA.signature) 183 | }) 184 | 185 | it('should throw error when the type parameter of invalid type', function () { 186 | const secretKey = '6752be882314f5bbbc9a6af2ae634fc07038584a4a77510ea5eced45f54dc030f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 187 | const User = { 188 | alpha: 5 189 | } 190 | const userData = { 191 | firstName: 'John', 192 | lastName: 'Doe' 193 | } 194 | 195 | expect(() => Exonum.sign(secretKey, userData, User)) 196 | .to.throw(TypeError, 'Wrong type of data.') 197 | }) 198 | 199 | it('should throw error when the secretKey parameter of wrong length', function () { 200 | const buffer = CreateType.serialize(TX_DATA.data) 201 | expect(() => Exonum.sign('1', buffer)) 202 | .to.throw(TypeError, 'secretKey of wrong type is passed. Hexadecimal expected.') 203 | }) 204 | 205 | it('should throw error when wrong secretKey parameter', function () { 206 | const buffer = CreateType.serialize(TX_DATA.data) 207 | expect(() => Exonum.sign(123, buffer)) 208 | .to.throw(TypeError, 'secretKey of wrong type is passed. Hexadecimal expected.') 209 | }) 210 | 211 | it('should throw error when the secretKey parameter of invalid type', function () { 212 | const buffer = CreateType.serialize(TX_DATA.data); 213 | 214 | [true, null, undefined, [], {}, 51, new Date()].forEach(function (secretKey) { 215 | expect(() => Exonum.sign(secretKey, buffer)) 216 | .to.throw(TypeError, 'secretKey of wrong type is passed. Hexadecimal expected.') 217 | }) 218 | }) 219 | }) 220 | 221 | describe('Verify signature', function () { 222 | it('should verify signature of the data of NewType type and return true', function () { 223 | const signature = Exonum.sign(keyPair.secretKey, TYPE_DATA.data, CreateType) 224 | expect(Exonum.verifySignature(signature, keyPair.publicKey, TYPE_DATA.data, CreateType)).to.be.true 225 | }) 226 | 227 | it('should verify signature of the data of NewType type using built-in method and return true', function () { 228 | const signature = CreateType.sign(keyPair.secretKey, TYPE_DATA.data) 229 | expect(CreateType.verifySignature(signature, keyPair.publicKey, TYPE_DATA.data)).to.be.true 230 | }) 231 | 232 | it('should verify signature of the data of Transaction type and return true', function () { 233 | const signature = Exonum.sign(keyPair.secretKey, TX_DATA.data, CreateTransaction) 234 | expect(Exonum.verifySignature(signature, keyPair.publicKey, TX_DATA.data, CreateTransaction)).to.be.true 235 | }) 236 | 237 | it('should verify signature of the data in Verified', function () { 238 | const signed = CreateTransaction.create(TX_DATA.data, keyPair) 239 | expect(signed.author).to.equal(keyPair.publicKey) 240 | expect(Exonum.verifySignature(signed.signature, signed.author, signed.payload, CreateTransaction)).to.be.true 241 | }) 242 | 243 | it('should verify signature of the array of 8-bit integers', function () { 244 | const buffer = CreateType.serialize(TYPE_DATA.data) 245 | expect(Exonum.verifySignature(TYPE_DATA.signed, keyPair.publicKey, buffer)).to.be.true 246 | }) 247 | 248 | it('should verify signature of the array of 8-bit integers', function () { 249 | const buffer = CreateType.serialize(TYPE_DATA.data) 250 | expect(Exonum.verifySignature(TYPE_DATA.signed, keyPair.publicKey, buffer)).to.be.true 251 | }) 252 | 253 | it('should throw error when the signature parameter is of wrong length', function () { 254 | const publicKey = 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 255 | const signature = 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 256 | const buffer = CreateType.serialize(TYPE_DATA.data) 257 | 258 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 259 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 260 | }) 261 | 262 | it('should throw error when the signature parameter is invalid', function () { 263 | const publicKey = 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 264 | const signature = '6752be882314f5bbbc9a6af2ae634fc07038584a4a77510ea5eced45f54dc030f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7z' 265 | const buffer = CreateType.serialize(TYPE_DATA.data) 266 | 267 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 268 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 269 | }) 270 | 271 | it('should throw error when the signature parameter is of wrong type', function () { 272 | const publicKey = 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36' 273 | const buffer = CreateType.serialize(TYPE_DATA.data); 274 | 275 | [true, null, undefined, [], {}, 51, new Date()].forEach(signature => { 276 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 277 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 278 | }) 279 | }) 280 | 281 | it('should throw error when the publicKey parameter is of wrong length', function () { 282 | const publicKey = '6752BE882314F5BBBC9A6AF2AE634FC07038584A4A77510EA5ECED45F54DC030F5864AB6A5A2190666B47C676BCF15A1F2F07703C5BCAFB5749AA735CE8B7C' 283 | const signature = '6752BE882314F5BBBC9A6AF2AE634FC07038584A4A77510EA5ECED45F54DC030F5864AB6A5A2190666B47C676BCF15A1F2F07703C5BCAFB5749AA735CE8B7C' 284 | const buffer = CreateType.serialize(TYPE_DATA.data) 285 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 286 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 287 | }) 288 | 289 | it('should throw error when the publicKey parameter is invalid', function () { 290 | const publicKey = 'F5864AB6A5A2190666B47C676BCF15A1F2F07703C5BCAFB5749AA735CE8B7C3Z' 291 | const signature = '6752BE882314F5BBBC9A6AF2AE634FC07038584A4A77510EA5ECED45F54DC030F5864AB6A5A2190666B47C676BCF15A1F2F07703C5BCAFB5749AA735CE8B7C' 292 | const buffer = CreateType.serialize(TYPE_DATA.data) 293 | 294 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 295 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 296 | }) 297 | 298 | it('should throw error when the publicKey parameter is of wrong type', function () { 299 | const signature = '6752BE882314F5BBBC9A6AF2AE634FC07038584A4A77510EA5ECED45F54DC030F5864AB6A5A2190666B47C676BCF15A1F2F07703C5BCAFB5749AA735CE8B7C' 300 | const buffer = CreateType.serialize(TYPE_DATA.data); 301 | [true, null, undefined, [], {}, 51, new Date()].forEach(function (publicKey) { 302 | expect(() => Exonum.verifySignature(signature, publicKey, buffer)) 303 | .to.throw(TypeError, 'Signature of wrong type is passed. Hexadecimal expected.') 304 | }) 305 | }) 306 | }) 307 | 308 | /* eslint-disable quote-props */ 309 | describe('publicKeyToAddress', () => { 310 | const referencePairs = { 311 | '0000000000000000000000000000000000000000000000000000000000000000': 312 | '13d0470e90875c1ac973e699573b24557ca9e255edcedcbec43cc820429852b9', 313 | '0000000000000000000000000000000000000000000000000000000000000001': 314 | '711f87668175afae5f158a4f16ec705e558b8f7c9e494f12534759453bbfa004', 315 | '84e0d4ae17ceefd457da118729539d121c9f5586f82338d895d1744652ce4455': 316 | '069714edac1fdb7f932f7f0af657f19982b6e72318781e042d25cea12311086c', 317 | 'f5864ab6a5a2190666b47c676bcf15a1f2f07703c5bcafb5749aa735ce8b7c36': 318 | '4d711ad2af3dead1e8562806f665d203a4174eb961dc19c54eff31053c4c449d' 319 | } 320 | 321 | Object.entries(referencePairs).forEach(([key, address]) => { 322 | it(`should work on key ${key}`, () => { 323 | expect(Exonum.publicKeyToAddress(key)).to.equal(address) 324 | }) 325 | }) 326 | }) 327 | }) 328 | -------------------------------------------------------------------------------- /test/sources/blockchain.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | 4 | import { uint8ArrayToHexadecimal } from '../../src/types' 5 | 6 | const MockAdapter = require('axios-mock-adapter') 7 | const axios = require('axios') 8 | const chai = require('chai') 9 | const chaiAsPromised = require('chai-as-promised') 10 | const { expect } = chai 11 | const Exonum = require('../../src') 12 | 13 | const proto = require('./proto/stubs') 14 | const { cryptocurrency_advanced: cryptocurrency } = proto.exonum.examples 15 | 16 | chai.use(chaiAsPromised) 17 | const mock = new MockAdapter(axios) 18 | 19 | describe('Verify block of precommits', function () { 20 | const validators = [ 21 | '55c48a5ccf060c4ab644277e4a98c6c4f4c480c115477e81523662948fa75a55' 22 | ] 23 | const validatorSecretKey = 24 | 'a822fd6e7e8265fbc00f8401696a5bdc34f5a6d2ff3f922f58a28c18576b71e5' + 25 | '55c48a5ccf060c4ab644277e4a98c6c4f4c480c115477e81523662948fa75a55' 26 | 27 | const validBlockProof = { 28 | block: { 29 | height: 123, 30 | tx_count: 5, 31 | prev_hash: '4f319987a786107dc63b2b70115b3734cb9880b099b70c463c5e1b05521ab764', 32 | tx_hash: '0a2c6ea0370d1d49c411a6e2396695fcd4eab03d96e9e7a8a3ec1ec312d9ab38', 33 | state_hash: '0000000000000000000000000000000000000000000000000000000000000000', 34 | error_hash: '0000000000000000000000000000000000000000000000000000000000000000', 35 | additional_headers: { 36 | headers: { 37 | proposer_id: [0, 0] 38 | } 39 | } 40 | }, 41 | precommits: [ 42 | '0a5c125a107b180122220a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8552a2' + 43 | '20a201e4d7209d7a1a74b6c51921017324fc926ccf25e6721e158dffd89ff53608b15320c08ccb395f10510f4b5' + 44 | 'b9cb0212220a2055c48a5ccf060c4ab644277e4a98c6c4f4c480c115477e81523662948fa75a551a420a406bb74' + 45 | 'b0ea8362164f73520844a509b5cac80b6852791687a4571b218f9739429ac089be4dc4581d2331ee1e415676903' + 46 | 'e47116e9c69234549f72e6cca1bccf0f' 47 | ] 48 | } 49 | 50 | const secondValidator = 'd519b0219afdf43556c4972c6efc37d971c8675ec8d0257b7f8e7f8206fb4d9e' 51 | const secondPrecommit = 52 | '0a5e125c0801107b180122220a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85' + 53 | '52a220a201e4d7209d7a1a74b6c51921017324fc926ccf25e6721e158dffd89ff53608b15320c08d5ba95f10510' + 54 | 'd49cfcb70112220a20d519b0219afdf43556c4972c6efc37d971c8675ec8d0257b7f8e7f8206fb4d9e1a420a402' + 55 | '77e69dc4050db11814a787f84e2b79318b7e47f8f09c45d8dcd0f26a9a45d08916c542606e8c331c1da6356a089' + 56 | '4c7b5a07e95b120e34d50d1823d47c045e01' 57 | 58 | it('should work when block with precommits is valid', () => { 59 | Exonum.verifyBlock(validBlockProof, validators) 60 | }) 61 | 62 | it('should throw error when precommit height is unexpected', () => { 63 | const invalidBlockProof = { 64 | block: Object.assign({}, validBlockProof.block), 65 | precommits: validBlockProof.precommits 66 | } 67 | invalidBlockProof.block.height -= 1 68 | expect(() => Exonum.verifyBlock(invalidBlockProof, validators)) 69 | .to.throw('Precommit height does not match block height') 70 | }) 71 | 72 | it('should throw error when block hash is unexpected', () => { 73 | const invalidBlockProof = { 74 | block: Object.assign({}, validBlockProof.block), 75 | precommits: validBlockProof.precommits 76 | } 77 | invalidBlockProof.block.tx_count += 1 78 | expect(() => Exonum.verifyBlock(invalidBlockProof, validators)) 79 | .to.throw('Precommit block hash does not match calculated block hash') 80 | }) 81 | 82 | it('should throw error when public key is unexpected', () => { 83 | const invalidBlockProof = { 84 | block: validBlockProof.block, 85 | precommits: [secondPrecommit] 86 | } 87 | expect(() => Exonum.verifyBlock(invalidBlockProof, validators)) 88 | .to.throw('Precommit public key does not match') 89 | }) 90 | 91 | it('should throw error when signature is unexpected', () => { 92 | const precommit = validBlockProof.precommits[0] 93 | // The last bytes of the precommit correspond to the signature 94 | const mangledPrecommit = precommit.substring(0, precommit.length - 1) + 'a' 95 | expect(mangledPrecommit).to.not.equal(precommit) 96 | 97 | const invalidBlockProof = { 98 | block: validBlockProof.block, 99 | precommits: [mangledPrecommit] 100 | } 101 | expect(() => Exonum.verifyBlock(invalidBlockProof, validators)) 102 | .to.throw('Precommit signature is wrong') 103 | }) 104 | 105 | it('should work with 2 validators', () => { 106 | const allValidators = [validators[0], secondValidator] 107 | const newBlockProof = { 108 | block: validBlockProof.block, 109 | precommits: [validBlockProof.precommits[0], secondPrecommit] 110 | } 111 | Exonum.verifyBlock(newBlockProof, allValidators) 112 | 113 | newBlockProof.precommits = [secondPrecommit, validBlockProof.precommits[0]] 114 | Exonum.verifyBlock(newBlockProof, allValidators) 115 | }) 116 | 117 | it('should throw when number of precommits is insufficient', () => { 118 | const allValidators = [validators[0], secondValidator] 119 | expect(() => Exonum.verifyBlock(validBlockProof, allValidators)) 120 | .to.throw('Insufficient number of precommits') 121 | }) 122 | 123 | it('should throw error when transaction is used instead of precommit', () => { 124 | const keyPair = { publicKey: validators[0], secretKey: validatorSecretKey } 125 | const sendFunds = new Exonum.Transaction({ 126 | schema: cryptocurrency.Transfer, 127 | serviceId: 128, 128 | methodId: 1 129 | }) 130 | const transactionData = { 131 | to: { 132 | data: Exonum.hexadecimalToUint8Array('278663010ebe1136011618ad5be1b9d6f51edc5b6c6b51b5450ffc72f54a57df') 133 | }, 134 | amount: '25', 135 | seed: '7743941227375415562' 136 | } 137 | const transaction = sendFunds.create(transactionData, keyPair).serialize() 138 | const invalidBlockProof = { 139 | block: validBlockProof.block, 140 | precommits: [uint8ArrayToHexadecimal(transaction)] 141 | } 142 | expect(() => Exonum.verifyBlock(invalidBlockProof, validators)) 143 | .to.throw('Invalid message type') 144 | }) 145 | 146 | it('should throw error on double endorsement', () => { 147 | const allValidators = [validators[0], secondValidator] 148 | const invalidBlockProof = { 149 | block: validBlockProof.block, 150 | precommits: [secondPrecommit, secondPrecommit] 151 | } 152 | expect(() => Exonum.verifyBlock(invalidBlockProof, allValidators)) 153 | .to.throw('Double endorsement from a validator') 154 | }) 155 | }) 156 | 157 | describe('Verify table existence', () => { 158 | const proof = { 159 | entries: [ 160 | { 161 | key: 'prefixed.list', 162 | value: '247d2cdfa9deeb9502080c4385e5ae341a55bf6d3115ed16170e444f4c8c9e87' 163 | } 164 | ], 165 | proof: [ 166 | { 167 | path: '0', 168 | hash: 'c43f42f7976721b7061ea0d5ba925c613196d4eda97f211bd7faeccf79ecd97c' 169 | }, 170 | { 171 | path: '1100010100001100100111000011101000000011101001010101110010001011000001001110111001101100001010100101011011111111001011111100111010011010000000101100100110101110001001101101110111101111001110101000011110110111101101010000001000011110101100001000100110011100', 172 | hash: 'd89d7c6b2955f99fd1113bd48f32fde18aa73e97695cdd9528ef921d1a772fbe' 173 | } 174 | ] 175 | } 176 | const stateHash = '681dccac8804d54bd5929425f34b9ef7628759cab7a954f0baaa8b572b19d3f5' 177 | const rootHash = '247d2cdfa9deeb9502080c4385e5ae341a55bf6d3115ed16170e444f4c8c9e87' 178 | 179 | it('should return root hash with valid proof', () => { 180 | expect(Exonum.verifyTable(proof, stateHash, 'prefixed.list')).to.equal(rootHash) 181 | }) 182 | 183 | it('should fail with wrong table name', () => { 184 | expect(() => Exonum.verifyTable(proof, stateHash, 'other.table')) 185 | .to.throw('Table not found in the root tree') 186 | }) 187 | 188 | it('should throw error when `stateHash` is invalid', () => { 189 | expect(() => Exonum.verifyTable(proof, '00'.repeat(32), 'token.wallets')) 190 | .to.throw(Error, 'Table proof is corrupted') 191 | }) 192 | }) 193 | 194 | describe('Send transaction to the blockchain', () => { 195 | const sendFunds = new Exonum.Transaction({ 196 | schema: cryptocurrency.Transfer, 197 | serviceId: 128, 198 | methodId: 1 199 | }) 200 | 201 | const keyPair = { 202 | publicKey: '78cf8b5e5c020696319eb32a1408e6c65e7d97733d34528fbdce08438a0243e8', 203 | secretKey: 'b5b3ccf6ca4475b7ff3d910d5ab31e4723098490a3e341dd9d2896b42ebc9f8978cf8b5e5c020696319eb32a1408e6c65e7d97733d34528fbdce08438a0243e8' 204 | } 205 | const data = { 206 | to: { 207 | data: Exonum.hexadecimalToUint8Array('278663010ebe1136011618ad5be1b9d6f51edc5b6c6b51b5450ffc72f54a57df') 208 | }, 209 | amount: '25', 210 | seed: '7743941227375415562' 211 | } 212 | const transaction = sendFunds.create(data, keyPair).serialize() 213 | 214 | const txHash = 'b4f78eab1d9b0b04a82f77f30ac0656e3a41765a4fccb513f8f6e4571a1f4003' 215 | const explorerBasePath = '/api/explorer/v1/transactions' 216 | const transactionPath = `${explorerBasePath}?hash=${txHash}` 217 | 218 | describe('Valid transaction has been sent', () => { 219 | before(() => { 220 | mock 221 | .onPost(explorerBasePath) 222 | .replyOnce(200, { tx_hash: txHash }) 223 | 224 | mock 225 | .onGet(transactionPath) 226 | .replyOnce(200, { type: 'in_pool' }) 227 | .onGet(transactionPath) 228 | .replyOnce(200, { type: 'committed' }) 229 | }) 230 | 231 | after(() => mock.reset()) 232 | 233 | it('should return fulfilled Promise state when transaction has accepted to the blockchain', async () => { 234 | const response = await Exonum.send(explorerBasePath, transaction) 235 | expect(response).to.deep.equal(txHash) 236 | }) 237 | }) 238 | 239 | describe('Valid transaction has been sent but node processes it very slow', () => { 240 | before(() => { 241 | mock 242 | .onPost(explorerBasePath) 243 | .replyOnce(200, { tx_hash: txHash }) 244 | 245 | mock 246 | .onGet(transactionPath) 247 | .replyOnce(404) 248 | .onGet(transactionPath) 249 | .replyOnce(404) 250 | .onGet(transactionPath) 251 | .replyOnce(404) 252 | .onGet(transactionPath) 253 | .replyOnce(200, { type: 'in_pool' }) 254 | .onGet(transactionPath) 255 | .replyOnce(200, { type: 'committed' }) 256 | }) 257 | 258 | after(() => mock.reset()) 259 | 260 | it('should return fulfilled Promise state when transaction has accepted to the blockchain', async function () { 261 | this.timeout(5000) 262 | const response = await Exonum.send(explorerBasePath, transaction) 263 | expect(response).to.deep.equal(txHash) 264 | }) 265 | }) 266 | 267 | describe('Valid transaction has been sent with custom attempts and timeout number', function () { 268 | before(() => { 269 | mock 270 | .onPost(explorerBasePath) 271 | .replyOnce(200, { tx_hash: txHash }) 272 | 273 | mock 274 | .onGet(transactionPath) 275 | .replyOnce(200, { type: 'in_pool' }) 276 | .onGet(transactionPath) 277 | .replyOnce(200, { type: 'in_pool' }) 278 | .onGet(transactionPath) 279 | .replyOnce(200, { type: 'in_pool' }) 280 | .onGet(transactionPath) 281 | .replyOnce(200, { type: 'in_pool' }) 282 | .onGet(transactionPath) 283 | .replyOnce(200, { type: 'in_pool' }) 284 | .onGet(transactionPath) 285 | .replyOnce(200, { type: 'in_pool' }) 286 | .onGet(transactionPath) 287 | .replyOnce(200, { type: 'committed' }) 288 | }) 289 | 290 | after(() => mock.reset()) 291 | 292 | it('should return fulfilled Promise state when transaction has accepted to the blockchain', async function () { 293 | this.timeout(5000) 294 | const response = await Exonum.send(explorerBasePath, transaction, 100, 7) 295 | expect(response).to.deep.equal(txHash) 296 | }) 297 | }) 298 | 299 | describe('Invalid data has been passed', function () { 300 | it('should throw error when wrong explorer base path is passed', async () => { 301 | const paths = [null, false, 42, new Date(), {}, []] 302 | 303 | for (const value of paths) { 304 | await expect(Exonum.send(value, transaction)) 305 | .to.be.rejectedWith('Explorer base path endpoint of wrong data type is passed.') 306 | } 307 | }) 308 | }) 309 | 310 | describe('Unexpected node behavior', function () { 311 | describe('Stay suspended in pool', function () { 312 | before(() => { 313 | mock 314 | .onPost(explorerBasePath) 315 | .reply(200, { tx_hash: txHash }) 316 | 317 | mock 318 | .onGet(transactionPath) 319 | .reply(200, { type: 'in_pool' }) 320 | }) 321 | 322 | after(() => mock.reset()) 323 | 324 | it('should return rejected Promise state', async () => { 325 | await expect(Exonum.send(explorerBasePath, transaction, 3, 100)) 326 | .to.be.rejectedWith('The transaction was not accepted to the block for the expected period.') 327 | }) 328 | }) 329 | 330 | describe('Node responded in unknown format', () => { 331 | before(() => { 332 | mock 333 | .onPost(explorerBasePath) 334 | .reply(200, { tx_hash: txHash }) 335 | 336 | mock 337 | .onGet(transactionPath) 338 | .reply(200) 339 | }) 340 | 341 | after(() => mock.reset()) 342 | 343 | it('should return rejected Promise state', async () => { 344 | await expect(Exonum.send(explorerBasePath, transaction, 3, 100)) 345 | .to.be.rejectedWith('The request failed or the blockchain node did not respond.') 346 | }) 347 | }) 348 | 349 | describe('Node responded with error', () => { 350 | before(() => { 351 | mock 352 | .onPost(explorerBasePath) 353 | .reply(200, { tx_hash: txHash }) 354 | 355 | mock 356 | .onGet(transactionPath) 357 | .reply(404) 358 | }) 359 | 360 | after(() => mock.reset()) 361 | 362 | it('should return rejected Promise state', async () => { 363 | await expect(Exonum.send(explorerBasePath, transaction, 3, 100)) 364 | .to.be.rejectedWith('The request failed or the blockchain node did not respond.') 365 | }) 366 | }) 367 | }) 368 | }) 369 | 370 | describe('Send multiple transactions to the blockchain', function () { 371 | const keyPair = { 372 | publicKey: '78cf8b5e5c020696319eb32a1408e6c65e7d97733d34528fbdce08438a0243e8', 373 | secretKey: 'b5b3ccf6ca4475b7ff3d910d5ab31e4723098490a3e341dd9d2896b42ebc9f8978cf8b5e5c020696319eb32a1408e6c65e7d97733d34528fbdce08438a0243e8' 374 | } 375 | const sendFunds = new Exonum.Transaction({ 376 | schema: cryptocurrency.Transfer, 377 | serviceId: 128, 378 | methodId: 0 379 | }) 380 | const explorerBasePath = '/api/explorer/v1/transactions' 381 | const transactionData = [ 382 | { 383 | data: { 384 | to: { data: Exonum.hexadecimalToUint8Array('278663010ebe1136011618ad5be1b9d6f51edc5b6c6b51b5450ffc72f54a57df') }, 385 | amount: '20', 386 | seed: '8452680960415703000' 387 | }, 388 | type: sendFunds 389 | }, 390 | { 391 | data: { 392 | to: { data: Exonum.hexadecimalToUint8Array('278663010ebe1136011618ad5be1b9d6f51edc5b6c6b51b5450ffc72f54a57df') }, 393 | amount: '25', 394 | seed: '7743941227375415562' 395 | }, 396 | type: sendFunds 397 | } 398 | ] 399 | const transactions = transactionData.map(data => sendFunds.create(data, keyPair)) 400 | const transactionHashes = [ 401 | 'dcea2d6b9dd46ae6f4da47f1e3d79f5fa0348b3b156be714d2d9471d7f476482', 402 | 'b4f78eab1d9b0b04a82f77f30ac0656e3a41765a4fccb513f8f6e4571a1f4003' 403 | ] 404 | 405 | describe('Queue of valid transactions has been sent', () => { 406 | before(() => { 407 | let txIndex = 0 408 | mock 409 | .onPost(explorerBasePath) 410 | .reply(() => [200, { tx_hash: transactionHashes[txIndex++] }]) 411 | 412 | mock 413 | .onGet(`${explorerBasePath}?hash=${transactionHashes[0]}`) 414 | .replyOnce(200, { type: 'in_pool' }) 415 | .onGet(`${explorerBasePath}?hash=${transactionHashes[0]}`) 416 | .replyOnce(200, { type: 'committed' }) 417 | .onGet(`${explorerBasePath}?hash=${transactionHashes[1]}`) 418 | .replyOnce(200, { type: 'in_pool' }) 419 | .onGet(`${explorerBasePath}?hash=${transactionHashes[1]}`) 420 | .replyOnce(200, { type: 'committed' }) 421 | }) 422 | 423 | after(() => mock.reset()) 424 | 425 | it('should return fulfilled Promise', async () => { 426 | const response = await Exonum.sendQueue(explorerBasePath, transactions) 427 | expect(response).to.deep.equal(transactionHashes) 428 | }) 429 | }) 430 | }) 431 | 432 | describe('Export proto stubs', () => { 433 | it('should export blockchain stubs', () => { 434 | expect(Exonum.protocol.exonum.Block).to.be.a('function') 435 | expect(Exonum.protocol.exonum.TxLocation).to.be.a('function') 436 | }) 437 | 438 | it('should export helpers stubs', () => { 439 | expect(Exonum.protocol.exonum.crypto.Hash).to.be.a('function') 440 | expect(Exonum.protocol.exonum.crypto.PublicKey).to.be.a('function') 441 | }) 442 | }) 443 | --------------------------------------------------------------------------------