exonum-client-cis
Version:
Light Client for Exonum CIS Blockchain
409 lines (317 loc) • 11.7 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ListProofError = exports.ListProof = void 0;
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _bigInteger = _interopRequireDefault(require("big-integer"));
var _binarySearch = _interopRequireDefault(require("binary-search"));
var _hexadecimal = require("../types/hexadecimal");
var _convert = require("../types/convert");
var _crypto = require("../crypto");
var _constants = require("./constants");
/**
* @typedef IEntry
* @property {number} height Height of the entry
* @property {number} index Index of the entry on the level
* @property {string} hash SHA-256 digest of the entry
*/
// Maximum height of a valid Merkle tree
var MAX_TREE_HEIGHT = 58;
var ListProof = function ListProof(_ref, valueType) {
var proof = _ref.proof,
entries = _ref.entries,
length = _ref.length;
(0, _classCallCheck2["default"])(this, ListProof);
if (!valueType || typeof valueType.serialize !== 'function') {
throw new TypeError('No `serialize` method in the value type');
}
this.valueType = valueType;
this.proof = parseProof(proof);
this.entries = parseEntries(entries, valueType);
this.length = length;
var rootHash;
if (this.length === 0) {
if (this.proof.length === 0 && this.entries.length === 0) {
rootHash = '0000000000000000000000000000000000000000000000000000000000000000';
} else {
throw new ListProofError('malformedEmptyProof');
}
} else {
var completeProof = [].concat((0, _toConsumableArray2["default"])(this.entries), (0, _toConsumableArray2["default"])(this.proof));
rootHash = collect(completeProof, this.length);
}
this.merkleRoot = hashList(rootHash, this.length);
};
exports.ListProof = ListProof;
function parseProof(proof) {
if (!Array.isArray(proof)) {
throw new ListProofError('malformedProof');
}
var validEntries = proof.every(function (_ref2) {
var index = _ref2.index,
height = _ref2.height,
hash = _ref2.hash;
return /^[0-9a-f]{64}$/i.test(hash) && Number.isInteger(index) && Number.isInteger(height) && height > 0 && height <= MAX_TREE_HEIGHT;
});
if (!validEntries) {
throw new ListProofError('malformedProof');
} // Check ordering of proof entries.
for (var i = 0; i + 1 < proof.length; i++) {
var _ref3 = [proof[i], proof[i + 1]],
prev = _ref3[0],
next = _ref3[1];
if (prev.height > next.height || prev.height === next.height && prev.index >= next.index) {
throw new ListProofError('invalidProofOrdering');
}
}
return proof;
}
/**
* Performs some preliminary checks on list values and computes their hashes.
*
* @param entries
* @param valueType
* @returns {IEntry[]} parsed entries
*/
function parseEntries(entries, valueType) {
if (!Array.isArray(entries)) {
throw new ListProofError('malformedEntries');
} // Check ordering of values.
for (var i = 0; i + 1 < entries.length; i++) {
var _ref4 = [entries[i], entries[i + 1]],
prev = _ref4[0],
next = _ref4[1];
if (prev[0] >= next[0]) {
throw new ListProofError('invalidValuesOrdering');
}
}
return entries.map(function (_ref5) {
var _ref6 = (0, _slicedToArray2["default"])(_ref5, 2),
index = _ref6[0],
value = _ref6[1];
if (!Number.isInteger(index)) {
throw new ListProofError('malformedEntries');
}
return {
index: index,
height: 0,
value: value,
hash: (0, _crypto.hash)([_constants.BLOB_PREFIX].concat((0, _toConsumableArray2["default"])(valueType.serialize(value, [], 0))))
};
});
}
/**
* Collects entries into a single hash of the Merkle tree.
* @param {IEntry[]} entries
* @param {number} listLength
* @returns {string} hash of the Merkle tree
*/
function collect(entries, listLength) {
var treeHeight = calcHeight(listLength); // Check that height is appropriate. Since we've checked ordering of `entries`,
// we only check that the height of the last entry does not exceed the expected
// value.
if (entries[entries.length - 1].height >= treeHeight) {
throw new ListProofError('unexpectedHeight');
} // Check that indexes of `entries` are appropriate.
entries.forEach(function (_ref7) {
var height = _ref7.height,
index = _ref7.index;
var divisor = height === 0 ? 1 : Math.pow(2, height - 1);
var maxIndexOnLevel = Math.floor((listLength - 1) / divisor);
if (index > maxIndexOnLevel) {
throw new ListProofError('unexpectedIndex');
}
}); // Copy values to the first layer (we've calculated their hashes already).
var layer = spliceLayer(entries, 0).map(function (_ref8) {
var index = _ref8.index,
hash = _ref8.hash;
return {
height: 1,
index: index,
hash: hash
};
});
var lastIndex = listLength - 1;
for (var height = 1; height < treeHeight; height++) {
// Merge with the next layer.
var nextLayer = spliceLayer(entries, height);
layer = mergeLayers(layer, nextLayer); // Zip the entries on the layer.
hashLayer(layer, lastIndex);
lastIndex = Math.floor(lastIndex / 2);
}
return layer[0].hash;
}
/**
* Splices entries with the specified height from the beginning of the array.
* The entries are modified in place.
*
* @param {IEntry[]} entries
* @param {number} height
* @returns {IEntry[]} spliced entries
*/
function spliceLayer(entries, height) {
var index = (0, _binarySearch["default"])(entries, height + 1, function (_ref9, needleHeight) {
var height = _ref9.height,
index = _ref9.index;
// Assume that all entries with `height === needleHeight` are larger than our needle.
var x = needleHeight !== height ? height - needleHeight : 1;
return x;
});
/* istanbul ignore next: should never be triggered */
if (index >= 0) {
throw new Error('Internal error while verifying list proof');
}
var greaterIndex = -index - 1;
return entries.splice(0, greaterIndex);
}
/**
* Merges two sorted arrays together.
*
* @param {IEntry[]} xs
* @param {IEntry[]} ys
* @returns {IEntry[]}
*/
function mergeLayers(xs, ys) {
var xIndex = 0;
var yIndex = 0;
var output = [];
while (xIndex < xs.length || yIndex < ys.length) {
var _ref10 = [xs[xIndex], ys[yIndex]],
x = _ref10[0],
y = _ref10[1];
if (!x) {
output.push(y);
yIndex++;
} else if (!y) {
output.push(x);
xIndex++;
} else if (x.index < y.index) {
output.push(x);
xIndex++;
} else if (x.index > y.index) {
output.push(y);
yIndex++;
} else {
// x.index === y.index
throw new ListProofError('duplicateHash');
}
}
return output;
}
/**
* Elevates the layer to the next level by zipping pairs of entries together.
*
* @param {IEntry[]} layer
* @param {number} lastIndex
*/
function hashLayer(layer, lastIndex) {
for (var i = 0; i < layer.length; i += 2) {
var _ref11 = [layer[i], layer[i + 1]],
left = _ref11[0],
right = _ref11[1];
var _hash = void 0;
if (right) {
// To be able to zip two hashes on the layer, they need to be adjacent to each other,
// and the first of them needs to have an even index.
if (left.index % 2 !== 0 || right.index !== left.index + 1) {
throw new ListProofError('missingHash');
}
_hash = hashNode(left.hash, right.hash);
} else {
// If there is an odd number of hashes on the layer, the solitary hash must have
// the greatest possible index.
if (lastIndex % 2 === 1 || left.index !== lastIndex) {
throw new ListProofError('missingHash');
}
_hash = hashNode(left.hash);
}
layer[i / 2] = {
height: left.height + 1,
index: left.index / 2,
hash: _hash
};
}
layer.length = Math.ceil(layer.length / 2);
}
function hashNode(leftHash, maybeRightHash) {
var buffer = [_constants.LIST_BRANCH_PREFIX];
_hexadecimal.Hash.serialize(leftHash, buffer, buffer.length);
if (maybeRightHash) {
_hexadecimal.Hash.serialize(maybeRightHash, buffer, buffer.length);
}
return (0, _crypto.hash)(buffer);
}
/**
* Computes hash of the `ProofListIndex` given its length and root hash.
* @param {string} rootHash
* @param {number} length
* @returns {string}
*/
function hashList(rootHash, length) {
var buffer = new Uint8Array(9 + _crypto.HASH_LENGTH);
buffer[0] = _constants.LIST_PREFIX; // Set bytes 1..9 as little-endian list length
var quotient = (0, _bigInteger["default"])(length);
for (var _byte = 1; _byte < 9; _byte++) {
var remainder = void 0;
var _quotient$divmod = quotient.divmod(256);
quotient = _quotient$divmod.quotient;
remainder = _quotient$divmod.remainder;
buffer[_byte] = remainder;
}
buffer.set((0, _convert.hexadecimalToUint8Array)(rootHash), 9);
return (0, _crypto.hash)(buffer);
}
/**
* Calculates height of a Merkle tree given its length.
* @param {bigInt} count
* @return {number}
*/
function calcHeight(count) {
var i = 0;
while ((0, _bigInteger["default"])(2).pow(i).lt(count)) {
i++;
}
return i + 1;
}
var ListProofError =
/*#__PURE__*/
function (_Error) {
(0, _inherits2["default"])(ListProofError, _Error);
function ListProofError(type) {
var _this;
(0, _classCallCheck2["default"])(this, ListProofError);
switch (type) {
case 'malformedProof':
case 'invalidProofOrdering':
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, 'malformed `proof` part of the proof'));
break;
case 'malformedEntries':
case 'invalidValuesOrdering':
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, 'malformed `entries` in the proof'));
break;
case 'unexpectedHeight':
case 'unexpectedIndex':
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, 'proof contains a branch where it is impossible according to list length'));
break;
case 'duplicateHash':
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, 'proof contains redundant entries'));
break;
case 'missingHash':
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, 'proof does not contain information to restore index hash'));
break;
default:
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(ListProofError).call(this, type));
}
return _this;
}
return ListProofError;
}((0, _wrapNativeSuper2["default"])(Error));
exports.ListProofError = ListProofError;