diff --git a/.flowconfig b/.flowconfig index 55835ff53..abe6b5cef 100644 --- a/.flowconfig +++ b/.flowconfig @@ -12,5 +12,5 @@ all=warn [options] include_warnings=true -module.name_mapper='^@polkadot/util-\(crypto\)\(.*\)$' -> '/packages/util-\1/src\2' +module.name_mapper='^@polkadot/util-\(crypto\|triehash\)\(.*\)$' -> '/packages/util-\1/src\2' module.name_mapper='^@polkadot/util\(.*\)$' -> '/packages/util/src\1' diff --git a/README.md b/README.md index 87758066f..59ac64ee8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ It is split up into a number of internal packages - - [packages/util](packages/util/) General utilities - [packages/util-crypto](packages/util-crypto/) Crypto and hashing utilities +- [packages/util-triehash](packages/util-triehash/) Calculation of trie hashes ## Contributing diff --git a/jest.config.js b/jest.config.js index cc71e2e26..f4c4d2a86 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,7 @@ const config = require('@polkadot/dev/config/jest'); module.exports = Object.assign({}, config, { moduleNameMapper: { - '@polkadot/util-(crypto)(.*)$': '/packages/util-$1/src/$2', + '@polkadot/util-(crypto|triehash)(.*)$': '/packages/util-$1/src/$2', '@polkadot/util(.*)$': '/packages/util/src/$1' } }); diff --git a/lerna.json b/lerna.json index cd44cced2..f61f5aa63 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "packages": [ "packages/*" ], - "version": "0.13.9" + "version": "0.14.0" } diff --git a/packages/util-crypto/README.md b/packages/util-crypto/README.md index 92d98f8aa..8da59f2ea 100644 --- a/packages/util-crypto/README.md +++ b/packages/util-crypto/README.md @@ -6,7 +6,7 @@ [![maintainability](https://img.shields.io/codeclimate/maintainability/polkadot-js/util.svg?style=flat-square)](https://codeclimate.com/github/polkadot-js/util/maintainability) [![coverage](https://img.shields.io/coveralls/polkadot-js/util.svg?style=flat-square)](https://coveralls.io/github/polkadot-js/util?branch=master) [![dependency](https://david-dm.org/polkadot-js/util.svg?style=flat-square&path=packages/util)](https://david-dm.org/polkadot-js/util?path=packages/util-crypto) -[![devDependency](https://david-dm.org/polkadot-js/util/dev-status.svg?style=flat-square&path=packages/util-crypto)](https://david-dm.org/polkadot-js/util?path=packages/util#info=devDependencies) +[![devDependency](https://david-dm.org/polkadot-js/util/dev-status.svg?style=flat-square&path=packages/util-crypto)](https://david-dm.org/polkadot-js/util?path=packages/util-crypto#info=devDependencies) # @polkadot/util-crypto diff --git a/packages/util-crypto/package.json b/packages/util-crypto/package.json index 29d2973e9..b7d280a78 100644 --- a/packages/util-crypto/package.json +++ b/packages/util-crypto/package.json @@ -1,6 +1,6 @@ { "name": "@polkadot/util-crypto", - "version": "0.13.9", + "version": "0.14.0", "description": "A collection of useful crypto utilities for @polkadot", "main": "index.js", "keywords": [ diff --git a/packages/util-crypto/src/nacl/keypairFromSeed.spec.js b/packages/util-crypto/src/nacl/keypairFromSeed.spec.js index a583d24d0..cbd19dc3c 100644 --- a/packages/util-crypto/src/nacl/keypairFromSeed.spec.js +++ b/packages/util-crypto/src/nacl/keypairFromSeed.spec.js @@ -3,31 +3,51 @@ // of the ISC license. See the LICENSE file for details. const u8aFromString = require('@polkadot/util/u8a/fromString'); +const u8aToHex = require('@polkadot/util/u8a/toHex'); const { naclKeypairFromSeed } = require('./index'); describe('naclKeypairFromSeed', () => { // NOTE: Aligned with Rust test, b"12345678901234567890123456789012" - it('generates a valid publicKey/secretKey pair', () => { + const TEST = '12345678901234567890123456789012'; + const RESULT = { + publicKey: new Uint8Array([ + 0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51, + 0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e, + 0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65, + 0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee + ]), + secretKey: new Uint8Array([ + 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, + // public part + 0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51, + 0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e, + 0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65, + 0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee + ]) + }; + + it('generates a valid publicKey/secretKey pair (string)', () => { + expect( + naclKeypairFromSeed(TEST) + ).toEqual(RESULT); + }); + + it('generates a valid publicKey/secretKey pair (u8a)', () => { expect( naclKeypairFromSeed( - u8aFromString('12345678901234567890123456789012') + u8aFromString(TEST) ) - ).toEqual({ - publicKey: new Uint8Array([ - 0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51, - 0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e, - 0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65, - 0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee - ]), - secretKey: new Uint8Array([ - 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, - // public part - 0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51, - 0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e, - 0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65, - 0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee - ]) - }); + ).toEqual(RESULT); + }); + + it('generates a valid publicKey/secretKey pair (hex)', () => { + expect( + naclKeypairFromSeed( + u8aToHex( + u8aFromString(TEST) + ) + ) + ).toEqual(RESULT); }); }); diff --git a/packages/util-triehash/LICENSE b/packages/util-triehash/LICENSE new file mode 100644 index 000000000..3d150a620 --- /dev/null +++ b/packages/util-triehash/LICENSE @@ -0,0 +1,15 @@ +ISC License (ISC) + +Copyright 2017-2018 Jaco Greeff + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/util-triehash/README.md b/packages/util-triehash/README.md new file mode 100644 index 000000000..7358fff54 --- /dev/null +++ b/packages/util-triehash/README.md @@ -0,0 +1,17 @@ +[![polkadotjs](https://img.shields.io/badge/polkadot-js-orange.svg?style=flat-square)](https://polkadot.js.org) +![isc](https://img.shields.io/badge/license-ISC-lightgrey.svg?style=flat-square) +[![style](https://img.shields.io/badge/code%20style-semistandard-lightgrey.svg?style=flat-square)](https://github.com/Flet/semistandard) +[![npm](https://img.shields.io/npm/v/@polkadot/util-triehash.svg?style=flat-square)](https://www.npmjs.com/package/@polkadot/util-triehash) +[![travis](https://img.shields.io/travis/polkadot-js/util.svg?style=flat-square)](https://travis-ci.org/polkadot-js/util) +[![maintainability](https://img.shields.io/codeclimate/maintainability/polkadot-js/util.svg?style=flat-square)](https://codeclimate.com/github/polkadot-js/util/maintainability) +[![coverage](https://img.shields.io/coveralls/polkadot-js/util.svg?style=flat-square)](https://coveralls.io/github/polkadot-js/util?branch=master) +[![dependency](https://david-dm.org/polkadot-js/util.svg?style=flat-square&path=packages/util)](https://david-dm.org/polkadot-js/util?path=packages/util-triehash) +[![devDependency](https://david-dm.org/polkadot-js/util/dev-status.svg?style=flat-square&path=packages/util-triehash)](https://david-dm.org/polkadot-js/util?path=packages/util-triehash#info=devDependencies) + +# @polkadot/util-triehash + +Calculate [Ethereum Trie hashes](https://github.com/ethereum/wiki/wiki/Patricia-Tree) from inputs. + +## Available Utilities + +For a list of currently exposed methods, see the [library documentation](docs/README.md). diff --git a/packages/util-triehash/docs/README.md b/packages/util-triehash/docs/README.md new file mode 100644 index 000000000..5a81b31ba --- /dev/null +++ b/packages/util-triehash/docs/README.md @@ -0,0 +1,49 @@ +# Available interfaces + +Utility methods to create [Ethereum Trie hashes](https://github.com/ethereum/wiki/wiki/Patricia-Tree) + +- [util](util.md) Internal utilities used along with hash generation + +# Available methods + +## trieRoot + +Creates a trie hash from the supplied pairs. + +```js +trieRoot (pairs: Trie$Pairs): Uint8Array +``` + + +From an `Array<{k: Uint8Array, v: Uint8Array}>` input, calculate the triehash and return the result as a `Uint8Array`. + +```js +import { u8aFromString } from '@polkadot/util'; +import { trieRoot } from '@polkadot/util-triehash'; + +trieRoot([{ +k: u8aFromString('A'), +v: u8aFromString('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +}]) // => 0xd23786fb4a010da3ce639d66d5e904a11dbc02746d1ce25029e53290cabf28ab +``` + +## trieRootOrdered + +Creates a trie hash from the supplied values. + +```js +trieRootOrdered (pvalues: Array): Uint8Array +``` + + +From an `Array` input, calculate the ordered triehash and return the result as a `Uint8Array`. + +```js +import { u8aFromString } from '@polkadot/util'; +import { trieRootOrdered } from '@polkadot/util-triehash'; + +trieRootOrdered([ +u8aFromString('doe'), +u8aFromString('reindeer') +]) // => 0xe766d5d51b89dc39d981b41bda63248d7abce4f0225eefd023792a540bcffee3 +``` \ No newline at end of file diff --git a/packages/util-triehash/docs/util.md b/packages/util-triehash/docs/util.md new file mode 100644 index 000000000..5d97e62e5 --- /dev/null +++ b/packages/util-triehash/docs/util.md @@ -0,0 +1,22 @@ +# util + +Internal utilities used along with hash generation + +- [asNibbles](#asnibbles) Converts the input to Nibbles. + +## asNibbles + +Converts the input to Nibbles. + +```js +asNibbles (pairs: Trie$Pairs): Uint8Array +``` + + +From an `Uint8Array` input, calculate and return a list of nibbles that makes up the input. + +```js +import { asNibbles } from '@polkadot/util-triehash/util'; + +asNibbles(new Uint8Array([0x41, 0x20]) // => Uint8Array([4, 1, 2, 0]) +``` \ No newline at end of file diff --git a/packages/util-triehash/package.json b/packages/util-triehash/package.json new file mode 100644 index 000000000..5316408a8 --- /dev/null +++ b/packages/util-triehash/package.json @@ -0,0 +1,37 @@ +{ + "name": "@polkadot/util-triehash", + "version": "0.14.0", + "description": "Creation of triehash values", + "main": "index.js", + "keywords": [ + "Polkadot" + ], + "author": "Jaco Greeff ", + "license": "ISC", + "engines": { + "node": ">=6.4" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/polkadot-js/util.git" + }, + "bugs": { + "url": "https://github.com/polkadot-js/util/issues" + }, + "homepage": "https://github.com/polkadot-js/util/tree/master/packages/util-triehash#readme", + "scripts": { + "build": "polkadot-dev-build-babel && polkadot-dev-build-docs", + "check": "eslint src && flow check", + "test": "echo \"Tests only available from root wrapper\"" + }, + "dependencies": { + "@polkadot/util": "^0.14.0", + "@polkadot/util-crypto": "^0.14.0", + "babel-runtime": "^6.26.0", + "rlp": "jacogr/rlp" + } +} diff --git a/packages/util-triehash/src/index.js b/packages/util-triehash/src/index.js new file mode 100644 index 000000000..9d3fa16ed --- /dev/null +++ b/packages/util-triehash/src/index.js @@ -0,0 +1,15 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +const trieRoot = require('./root'); +const trieRootOrdered = require('./rootOrdered'); + +/** + @summary Utility methods to create [Ethereum Trie hashes](https://github.com/ethereum/wiki/wiki/Patricia-Tree) +*/ +module.exports = { + trieRoot, + trieRootOrdered +}; diff --git a/packages/util-triehash/src/root.js b/packages/util-triehash/src/root.js new file mode 100644 index 000000000..f84340dbe --- /dev/null +++ b/packages/util-triehash/src/root.js @@ -0,0 +1,34 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +import type { Trie$Pairs } from './types'; + +const asNibbles = require('./util/asNibbles'); +const genRoot = require('./util/genRoot'); +const pairsUniq = require('./util/pairsUniq'); + +/** + @name trieRoot + @signature trieRoot (pairs: Trie$Pairs): Uint8Array + @summary Creates a trie hash from the supplied pairs. + @description + From an `Array<{k: Uint8Array, v: Uint8Array}>` input, calculate the triehash and return the result as a `Uint8Array`. + @example + import { u8aFromString } from '@polkadot/util'; + import { trieRoot } from '@polkadot/util-triehash'; + + trieRoot([{ + k: u8aFromString('A'), + v: u8aFromString('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + }]) // => 0xd23786fb4a010da3ce639d66d5e904a11dbc02746d1ce25029e53290cabf28ab +*/ +module.exports = function trieRoot (pairs: Trie$Pairs): Uint8Array { + return genRoot( + pairsUniq(pairs).map(({ k, v }) => ({ + k: asNibbles(k), + v + })) + ); +}; diff --git a/packages/util-triehash/src/root.spec.js b/packages/util-triehash/src/root.spec.js new file mode 100644 index 000000000..0a9bf9eeb --- /dev/null +++ b/packages/util-triehash/src/root.spec.js @@ -0,0 +1,88 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const u8aFromString = require('@polkadot/util/u8a/fromString'); +const hexToU8a = require('@polkadot/util/hex/toU8a'); + +const { trieRoot } = require('./index'); + +describe('trieRoot', () => { + it('encodes empty k/v pairs', () => { + expect( + trieRoot([]) + ).toEqual( + hexToU8a( + '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347' + ) + ); + }); + + // https://github.com/paritytech/parity/blob/e95b09348386d01b71901365785c5fa3aa2f7a6d/util/triehash/src/lib.rs#L331 + it('encodes a simple k/v pair', () => { + expect( + trieRoot([{ + k: u8aFromString('A'), + v: u8aFromString('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + }]) + ).toEqual( + hexToU8a( + '0xd23786fb4a010da3ce639d66d5e904a11dbc02746d1ce25029e53290cabf28ab' + ) + ); + }); + + it('encodes two simple k/v pairs', () => { + expect( + trieRoot([ + { k: u8aFromString('A'), v: u8aFromString('aaaa') }, + { k: u8aFromString('B'), v: u8aFromString('bbbb') } + ]) + ).toEqual( + hexToU8a( + '0x59262fe132edbc936eeeec3450ab442c81dcbc6168fb558c61e7a8cc883108e3' + ) + ); + }); + + // https://github.com/paritytech/parity/blob/e95b09348386d01b71901365785c5fa3aa2f7a6d/util/triehash/src/lib.rs#L73 + it('encodes an array of k/v pairs', () => { + expect( + trieRoot([ + { + k: u8aFromString('doe'), + v: u8aFromString('reindeer') + }, + { + k: u8aFromString('dog'), + v: u8aFromString('puppy') + }, + { + k: u8aFromString('dogglesworth'), + v: u8aFromString('cat') + } + ]) + ).toEqual( + hexToU8a( + '0x8aad789dff2f538bca5d8ea56e8abe10f4c7ba3a5dea95fea4cd6e7c3a1168d3' + ) + ); + }); + + // https://github.com/paritytech/parity/blob/e95b09348386d01b71901365785c5fa3aa2f7a6d/util/triehash/src/lib.rs#L338 + it('matches out-of-order k/v pairs', () => { + expect( + trieRoot([ + { k: new Uint8Array([0x01]), v: new Uint8Array([0x23]) }, + { k: new Uint8Array([0x81]), v: new Uint8Array([0x23]) }, + { k: new Uint8Array([0xf1]), v: new Uint8Array([0x23]) } + ]) + ).toEqual( + trieRoot([ + { k: new Uint8Array([0x01]), v: new Uint8Array([0x23]) }, + { k: new Uint8Array([0xf1]), v: new Uint8Array([0x23]) }, + { k: new Uint8Array([0x81]), v: new Uint8Array([0x23]) } + ]) + ); + }); +}); diff --git a/packages/util-triehash/src/rootOrdered.js b/packages/util-triehash/src/rootOrdered.js new file mode 100644 index 000000000..131e69a66 --- /dev/null +++ b/packages/util-triehash/src/rootOrdered.js @@ -0,0 +1,41 @@ +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +const bufferToU8a = require('@polkadot/util/buffer/toU8a'); +const rlp = require('rlp'); + +const asNibbles = require('./util/asNibbles'); +const genRoot = require('./util/genRoot'); +const pairsUniq = require('./util/pairsUniq'); + +/** + @name trieRootOrdered + @signature trieRootOrdered (pvalues: Array): Uint8Array + @summary Creates a trie hash from the supplied values. + @description + From an `Array` input, calculate the ordered triehash and return the result as a `Uint8Array`. + @example + import { u8aFromString } from '@polkadot/util'; + import { trieRootOrdered } from '@polkadot/util-triehash'; + + trieRootOrdered([ + u8aFromString('doe'), + u8aFromString('reindeer') + ]) // => 0xe766d5d51b89dc39d981b41bda63248d7abce4f0225eefd023792a540bcffee3 +*/ +module.exports = function trieRootOrdered (values: Array): Uint8Array { + return genRoot( + pairsUniq( + values.map((v, index) => ({ + k: asNibbles( + bufferToU8a( + // $FlowFixMe rlp type definition is lacking + rlp.encode(index) + ) + ), + v + })) + ) + ); +}; diff --git a/packages/util-triehash/src/rootOrdered.spec.js b/packages/util-triehash/src/rootOrdered.spec.js new file mode 100644 index 000000000..c43fa9e70 --- /dev/null +++ b/packages/util-triehash/src/rootOrdered.spec.js @@ -0,0 +1,24 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const hexToU8a = require('@polkadot/util/hex/toU8a'); +const u8aFromString = require('@polkadot/util/u8a/fromString'); + +const { trieRootOrdered } = require('./index'); + +describe('trieRootOrdered', () => { + // https://github.com/paritytech/parity/blob/e95b09348386d01b71901365785c5fa3aa2f7a6d/util/triehash/src/lib.rs#L42 + it('encodes a values', () => { + expect( + trieRootOrdered([ + u8aFromString('doe'), + u8aFromString('reindeer') + ]) + ).toEqual( + hexToU8a( + '0xe766d5d51b89dc39d981b41bda63248d7abce4f0225eefd023792a540bcffee3' + ) + ); + }); +}); diff --git a/packages/util-triehash/src/types.js b/packages/util-triehash/src/types.js new file mode 100644 index 000000000..662fc6984 --- /dev/null +++ b/packages/util-triehash/src/types.js @@ -0,0 +1,11 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +export type Trie$Pair = { + k: Uint8Array, + v: Uint8Array +}; + +export type Trie$Pairs = Array; diff --git a/packages/util-triehash/src/util/asNibbles.js b/packages/util-triehash/src/util/asNibbles.js new file mode 100644 index 000000000..bd5c3f33c --- /dev/null +++ b/packages/util-triehash/src/util/asNibbles.js @@ -0,0 +1,26 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +/** + @name asNibbles + @signature asNibbles (pairs: Trie$Pairs): Uint8Array + @summary Converts the input to Nibbles. + @description + From an `Uint8Array` input, calculate and return a list of nibbles that makes up the input. + @example + import { asNibbles } from '@polkadot/util-triehash/util'; + + asNibbles(new Uint8Array([0x41, 0x20]) // => Uint8Array([4, 1, 2, 0]) +*/ +module.exports = function asNibbles (bytes: Uint8Array | Array): Uint8Array { + return bytes.reduce((result, byte, index) => { + result.set( + [byte >> 4, byte & 0b1111], + index * 2 + ); + + return result; + }, new Uint8Array(bytes.length * 2)); +}; diff --git a/packages/util-triehash/src/util/asNibbles.spec.js b/packages/util-triehash/src/util/asNibbles.spec.js new file mode 100644 index 000000000..a2fb22b69 --- /dev/null +++ b/packages/util-triehash/src/util/asNibbles.spec.js @@ -0,0 +1,29 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const u8aFromString = require('@polkadot/util/u8a/fromString'); + +const asNibbles = require('./asNibbles'); + +describe('asNibbles', () => { + it('converts an array', () => { + expect( + asNibbles([ + 0x31, 0x23, 0x45 + ]) + ).toEqual( + new Uint8Array([3, 1, 2, 3, 4, 5]) + ); + }); + + it('converts a single', () => { + expect( + asNibbles( + u8aFromString('A') + ) + ).toEqual( + new Uint8Array([4, 1]) + ); + }); +}); diff --git a/packages/util-triehash/src/util/encode.js b/packages/util-triehash/src/util/encode.js new file mode 100644 index 000000000..c9c7cfa2e --- /dev/null +++ b/packages/util-triehash/src/util/encode.js @@ -0,0 +1,94 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +import type { Trie$Pairs } from '../types'; + +const keccakAsU8a = require('@polkadot/util-crypto/keccak/asU8a'); +const rlp = require('rlp'); + +const encodeHexPrefix = require('./encodeHexPrefix'); +const sharedPrefixLength = require('./sharedPrefixLength'); + +// flowlint-next-line unclear-type:off +function encodeAux (pairs: Trie$Pairs, preLength: number): any { + const encoded = encode(pairs, preLength); + // flowlint-next-line unclear-type:off + const rlped = rlp.encode((encoded: any)); + + if (rlped.length <= 31) { + return encoded; + } + + return keccakAsU8a(rlped); +} + +// flowlint-next-line unclear-type:off +function encode (pairs: Trie$Pairs, preLength: number): Array { + if (pairs.length === 0) { + return []; + } + + const firstKey = pairs[0].k; + + if (pairs.length === 1) { + return [ + encodeHexPrefix( + firstKey.slice(preLength), + true + ), + pairs[0].v + ]; + } + + const sharedLength = sharedPrefixLength(pairs); + + if (sharedLength > preLength) { + return [ + encodeHexPrefix( + firstKey.slice(preLength, sharedLength), + false + ), + encodeAux(pairs, sharedLength) + ]; + } + + const result = []; + let begin = preLength === firstKey.length + ? 1 + : 0; + + for (let index = 0; index < 16; index++) { + let length = 0; + + for (let j = begin; j < pairs.length; j++) { + if (pairs[j].k[preLength] === index) { + length++; + } else { + j = pairs.length; + } + } + + result.push( + length === 0 + ? '' + : encodeAux( + pairs.slice(begin, begin + length), + preLength + 1 + ) + ); + + begin += length; + } + + result.push( + preLength !== firstKey.length + ? '' + : pairs[0].v + ); + + return result; +} + +module.exports = encode; diff --git a/packages/util-triehash/src/util/encodeHexPrefix.js b/packages/util-triehash/src/util/encodeHexPrefix.js new file mode 100644 index 000000000..c14bf5bfe --- /dev/null +++ b/packages/util-triehash/src/util/encodeHexPrefix.js @@ -0,0 +1,19 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +module.exports = function encodeHexPrefix (nibbles: Uint8Array | Array, isLeaf: boolean): Uint8Array { + const bits = ((nibbles.length & 1) + (2 * (isLeaf ? 1 : 0))) << 4; + const result = []; + let offset = nibbles.length % 2; + + result.push(bits + (offset ? nibbles[0] : 0)); + + while (offset < nibbles.length) { + result.push((nibbles[offset] << 4) + nibbles[offset + 1]); + offset += 2; + } + + return new Uint8Array(result); +}; diff --git a/packages/util-triehash/src/util/encodeHexPrefix.spec.js b/packages/util-triehash/src/util/encodeHexPrefix.spec.js new file mode 100644 index 000000000..fa8aaff39 --- /dev/null +++ b/packages/util-triehash/src/util/encodeHexPrefix.spec.js @@ -0,0 +1,73 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const encodeHexPrefix = require('./encodeHexPrefix'); + +describe('encodeHexPrefix', () => { + it('encodes [0, 0, 1, 2, 3, 4, 5], false', () => { + expect( + encodeHexPrefix( + [0, 0, 1, 2, 3, 4, 5], + false + ) + ).toEqual( + new Uint8Array([0x10, 0x01, 0x23, 0x45]) + ); + }); + + it('encodes [0, 1, 2, 3, 4, 5], false', () => { + expect( + encodeHexPrefix( + [0, 1, 2, 3, 4, 5], + false + ) + ).toEqual( + new Uint8Array([0x00, 0x01, 0x23, 0x45]) + ); + }); + + it('encodes [0, 1, 2, 3, 4, 5], true', () => { + expect( + encodeHexPrefix( + [0, 1, 2, 3, 4, 5], + true + ) + ).toEqual( + new Uint8Array([0x20, 0x01, 0x23, 0x45]) + ); + }); + + it('encodes [1, 2, 3, 4, 5], true', () => { + expect( + encodeHexPrefix( + [1, 2, 3, 4, 5], + true + ) + ).toEqual( + new Uint8Array([0x31, 0x23, 0x45]) + ); + }); + + it('encodes [1, 2, 3, 4], false', () => { + expect( + encodeHexPrefix( + [1, 2, 3, 4], + false + ) + ).toEqual( + new Uint8Array([0x00, 0x12, 0x34]) + ); + }); + + it('encodes [4, 1], true', () => { + expect( + encodeHexPrefix( + [4, 1], + true + ) + ).toEqual( + new Uint8Array([0x20, 0x41]) + ); + }); +}); diff --git a/packages/util-triehash/src/util/genRoot.js b/packages/util-triehash/src/util/genRoot.js new file mode 100644 index 000000000..d3ee5baaa --- /dev/null +++ b/packages/util-triehash/src/util/genRoot.js @@ -0,0 +1,20 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +import type { Trie$Pairs } from '../types'; + +const keccakAsU8a = require('@polkadot/util-crypto/keccak/asU8a'); +const rlp = require('rlp'); + +const encode = require('./encode'); + +module.exports = function genRoot (pairs: Trie$Pairs): Uint8Array { + return keccakAsU8a( + rlp.encode( + // $FlowFixMe rlp definition is not 100% + encode(pairs, 0) + ) + ); +}; diff --git a/packages/util-triehash/src/util/index.js b/packages/util-triehash/src/util/index.js new file mode 100644 index 000000000..0a8363f51 --- /dev/null +++ b/packages/util-triehash/src/util/index.js @@ -0,0 +1,9 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +/** + @summary Internal utilities used along with hash generation +*/ +module.exports = {}; diff --git a/packages/util-triehash/src/util/pairsUniq.js b/packages/util-triehash/src/util/pairsUniq.js new file mode 100644 index 000000000..45da37093 --- /dev/null +++ b/packages/util-triehash/src/util/pairsUniq.js @@ -0,0 +1,25 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +import type { Trie$Pair, Trie$Pairs } from '../types'; + +type PairMap = { + [string]: Trie$Pair +}; + +const u8aToHex = require('@polkadot/util/u8a/toHex'); + +module.exports = function pairsUniq (pairs: Trie$Pairs): Trie$Pairs { + const map: PairMap = pairs.reduce((result, pair) => { + result[u8aToHex(pair.k)] = pair; + + return result; + }, {}); + + return Object + .keys(map) + .sort() + .map((key) => map[key]); +}; diff --git a/packages/util-triehash/src/util/pairsUniq.spec.js b/packages/util-triehash/src/util/pairsUniq.spec.js new file mode 100644 index 000000000..415571908 --- /dev/null +++ b/packages/util-triehash/src/util/pairsUniq.spec.js @@ -0,0 +1,44 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const pairsUniq = require('./pairsUniq'); + +describe('pairsUniq', () => { + it('returns non-duplicated unchanged', () => { + expect( + pairsUniq([ + { k: new Uint8Array([0x1]), v: 'A' }, + { k: new Uint8Array([0x2]), v: 'B' } + ]) + ).toEqual([ + { k: new Uint8Array([0x1]), v: 'A' }, + { k: new Uint8Array([0x2]), v: 'B' } + ]); + }); + + it('removes duplicated pairs', () => { + expect( + pairsUniq([ + { k: new Uint8Array([0x1]), v: 'A' }, + { k: new Uint8Array([0x2]), v: 'B' }, + { k: new Uint8Array([0x1]), v: 'C' } + ]) + ).toEqual([ + { k: new Uint8Array([0x1]), v: 'C' }, + { k: new Uint8Array([0x2]), v: 'B' } + ]); + }); + + it('sorts pairs as returned', () => { + expect( + pairsUniq([ + { k: new Uint8Array([0x2]), v: 'B' }, + { k: new Uint8Array([0x1]), v: 'A' } + ]) + ).toEqual([ + { k: new Uint8Array([0x1]), v: 'A' }, + { k: new Uint8Array([0x2]), v: 'B' } + ]); + }); +}); diff --git a/packages/util-triehash/src/util/sharedPrefixLength.js b/packages/util-triehash/src/util/sharedPrefixLength.js new file mode 100644 index 000000000..14813e953 --- /dev/null +++ b/packages/util-triehash/src/util/sharedPrefixLength.js @@ -0,0 +1,31 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. +// @flow + +import type { Trie$Pairs } from '../types'; + +function getSharedLength (a: Uint8Array, b: Uint8Array): number { + const count = Math.min(a.length, b.length); + + for (let index = 0; index < count; index++) { + if (a[index] !== b[index]) { + return index; + } + } + + return count; +} + +module.exports = function sharedPrefixLength (pairs: Trie$Pairs): number { + return pairs.reduce((length, { k }, index) => { + if (index === 0) { + return k.length; + } + + return Math.min( + getSharedLength(pairs[0].k, k), + length + ); + }, 0); +}; diff --git a/packages/util-triehash/src/util/sharedPrefixLength.spec.js b/packages/util-triehash/src/util/sharedPrefixLength.spec.js new file mode 100644 index 000000000..4ad28fee8 --- /dev/null +++ b/packages/util-triehash/src/util/sharedPrefixLength.spec.js @@ -0,0 +1,34 @@ +// Copyright 2017-2018 Jaco Greeff +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +const sharedPrefixLength = require('./sharedPrefixLength'); + +describe('sharedPrefixLength', () => { + it('returns 0 where no matches found', () => { + expect( + sharedPrefixLength([ + { k: new Uint8Array([1, 2, 3, 4, 5, 6]) }, + { k: new Uint8Array([4, 2, 3, 4, 5, 6]) } + ]) + ).toEqual(0); + }); + + it('returns min length where match found', () => { + expect( + sharedPrefixLength([ + { k: new Uint8Array([1, 2, 3, 3, 5]) }, + { k: new Uint8Array([1, 2, 3]) } + ]) + ).toEqual(3); + }); + + it('returns max length where match found', () => { + expect( + sharedPrefixLength([ + { k: new Uint8Array([1, 2, 3, 4, 5, 6]) }, + { k: new Uint8Array([1, 2, 3, 4, 5, 6]) } + ]) + ).toEqual(6); + }); +}); diff --git a/packages/util/package.json b/packages/util/package.json index 4e888b438..beb5ef68d 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@polkadot/util", - "version": "0.13.9", + "version": "0.14.0", "description": "A collection of useful utilities for @polkadot", "main": "index.js", "keywords": [ diff --git a/yarn.lock b/yarn.lock index c72948a8f..eb45eac43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4574,6 +4574,10 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2. dependencies: glob "^7.0.5" +rlp@jacogr/rlp: + version "2.0.0" + resolved "https://codeload.github.com/jacogr/rlp/tar.gz/ca33b296fc43d7a3543d8a8f172adc9b66826f5f" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"