exonum-client-cis
Version:
Light Client for Exonum CIS Blockchain
375 lines (291 loc) • 11.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MapProofError = exports.MapProof = 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 _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _binarySearch = _interopRequireDefault(require("binary-search"));
var _crypto = require("../crypto");
var _hexadecimal = require("../types/hexadecimal");
var _constants = require("./constants");
var _ProofPath = _interopRequireDefault(require("./ProofPath"));
var _types = require("../types");
/**
* Proof of existence and/or absence of certain elements from a Merkelized
* map index.
*/
var MapProof =
/**
* Creates a new instance of a proof.
*
* @param {Object} json
* JSON object containing (untrusted) proof
* @param {{serialize: (any) => Array<number>}} keyType
* Type of keys used in the underlying Merkelized map. Usually, `PublicKey`
* or `Hash`. The keys must be serializable.
* @param {{ serialize: (any) => Array<number>}} valueType
* Type of values used in the underlying Merkelized map. Usually, it should
* be a type created with the `newType` function. The type must be serializable.
* @throws {MapProofError}
* if the proof is malformed
*/
function MapProof(json, keyType, valueType) {
(0, _classCallCheck2["default"])(this, MapProof);
if (!valueType) {
throw new TypeError('Invalid value type');
}
if (typeof valueType.encode === 'function' && typeof valueType.serialize !== 'function') {
valueType.serialize = function (data, _buff, _offset) {
return this.encode(data).finish();
};
} else if (typeof valueType.serialize !== 'function') {
throw new TypeError('No `hash` or `serialize` method in the value type');
}
this.valueType = valueType;
this.proof = parseProof(json.proof);
this.entries = parseEntries(json.entries, keyType, valueType);
if (!keyType || typeof keyType.serialize !== 'function') {
throw new TypeError('No `serialize` method in the key type');
}
this.keyType = keyType;
precheckProof.call(this);
var completeProof = this.proof.concat(this.entries).sort(function (_ref, _ref2) {
var pathA = _ref.path;
var pathB = _ref2.path;
return pathA.compare(pathB);
}); // This check is required as duplicate paths can be introduced by entries
// (further, it's generally possible that two different entry keys lead
// to the same `ProofPath`).
for (var i = 1; i < completeProof.length; i++) {
var _ref3 = [completeProof[i - 1], completeProof[i]],
pathA = _ref3[0].path,
pathB = _ref3[1].path;
if (pathA.compare(pathB) === 0) {
throw new MapProofError('duplicatePath', pathA);
}
}
var rootHash = (0, _types.hexadecimalToUint8Array)(collect(completeProof.filter(function (_ref4) {
var hash = _ref4.hash;
return !!hash;
})));
this.merkleRoot = (0, _crypto.hash)([_constants.MAP_PREFIX].concat((0, _toConsumableArray2["default"])(rootHash)));
this.missingKeys = new Set(this.entries.filter(function (e) {
return e.missing !== undefined;
}).map(function (_ref5) {
var missing = _ref5.missing;
return missing;
}));
this.entries = new Map(this.entries.filter(function (e) {
return e.key !== undefined;
}).map(function (_ref6) {
var key = _ref6.key,
value = _ref6.value;
return [key, value];
}));
};
exports.MapProof = MapProof;
function parseProof(proof) {
if (!Array.isArray(proof)) {
throw new MapProofError('malformedProof');
}
var validEntries = proof.every(function (_ref7) {
var path = _ref7.path,
hash = _ref7.hash;
return /^[01]{1,256}$/.test(path) && /^[0-9a-f]{64}$/i.test(hash);
});
if (!validEntries) {
throw new MapProofError('malformedProof');
}
return proof.map(function (_ref8) {
var path = _ref8.path,
hash = _ref8.hash;
return {
path: new _ProofPath["default"](path),
hash: hash
};
});
}
function parseEntries(entries, keyType, valueType) {
function createPath(data) {
var keyBytes = typeof keyType.hash === 'function' ? keyType.hash(data) : (0, _crypto.hash)(keyType.serialize(data, [], 0));
var bytes = (0, _types.hexadecimalToUint8Array)(keyBytes);
return new _ProofPath["default"](new Uint8Array(bytes));
}
if (!Array.isArray(entries)) {
throw new MapProofError('malformedEntries');
}
return entries.map(function (_ref9) {
var missing = _ref9.missing,
key = _ref9.key,
value = _ref9.value;
if (missing === undefined && (key === undefined || value === undefined)) {
throw new MapProofError('unknownEntryType');
}
if (missing !== undefined && (key !== undefined || value !== undefined)) {
throw new MapProofError('ambiguousEntryType');
}
if (missing !== undefined) {
return {
missing: missing,
path: createPath(missing)
};
}
return {
key: key,
value: value,
path: createPath(key),
hash: (0, _crypto.hash)([_constants.BLOB_PREFIX].concat((0, _toConsumableArray2["default"])(valueType.serialize(value, [], 0))))
};
});
}
/**
* @this {MapProof}
*/
function precheckProof() {
var _this = this;
// Check that entries in proof are in increasing order
for (var i = 1; i < this.proof.length; i++) {
var _ref10 = [this.proof[i - 1], this.proof[i]],
prevPath = _ref10[0].path,
path = _ref10[1].path;
switch (prevPath.compare(path)) {
case -1:
if (path.startsWith(prevPath)) {
throw new MapProofError('embeddedPaths', prevPath, path);
}
break;
case 0:
throw new MapProofError('duplicatePath', path);
case 1:
throw new MapProofError('invalidOrdering', prevPath, path);
}
} // Check that no entry has a prefix among the paths in the proof entries.
// In order to do this, it suffices to locate the closest smaller path
// in the proof entries and check only it.
this.entries.forEach(function (_ref11) {
var keyPath = _ref11.path;
var index = (0, _binarySearch["default"])(_this.proof, keyPath, function (_ref12, needle) {
var path = _ref12.path;
return path.compare(needle);
});
if (index >= 0) {
throw new MapProofError('duplicatePath', keyPath);
}
var insertionIndex = -index - 1;
if (insertionIndex > 0) {
var _prevPath = _this.proof[insertionIndex - 1].path;
if (keyPath.startsWith(_prevPath)) {
throw new MapProofError('embeddedPaths', _prevPath, keyPath);
}
}
});
}
function serializeBranchNode(leftHash, rightHash, leftPath, rightPath) {
var buffer = [_constants.MAP_BRANCH_PREFIX];
_hexadecimal.Hash.serialize(leftHash, buffer, buffer.length);
_hexadecimal.Hash.serialize(rightHash, buffer, buffer.length);
leftPath.serialize(buffer);
rightPath.serialize(buffer);
return buffer;
}
function serializeIsolatedNode(path, hash) {
// `1, ...path.key, 0` is the old `ProofPath` serialization for terminal paths.
var buffer = [_constants.MAP_BRANCH_PREFIX, 1].concat((0, _toConsumableArray2["default"])(path.key), [0]);
_hexadecimal.Hash.serialize(hash, buffer, buffer.length);
return buffer;
}
function collect(entries) {
function hashIsolatedNode(_ref13) {
var path = _ref13.path,
valueHash = _ref13.hash;
var buffer = serializeIsolatedNode(path, valueHash);
return (0, _crypto.hash)(buffer);
}
function hashBranch(left, right) {
var buffer = serializeBranchNode(left.hash, right.hash, left.path, right.path);
return (0, _crypto.hash)(buffer);
}
function fold(contour, lastPrefix) {
var lastEntry = contour.pop();
var penultimateEntry = contour.pop();
contour.push({
path: lastPrefix,
hash: hashBranch(penultimateEntry, lastEntry)
});
return contour.length > 1 ? lastPrefix.commonPrefix(contour[contour.length - 2].path) : null;
}
switch (entries.length) {
case 0:
return '0000000000000000000000000000000000000000000000000000000000000000';
case 1:
if (!entries[0].path.isTerminal()) {
throw new MapProofError('nonTerminalNode', entries[0].path);
}
return hashIsolatedNode(entries[0]);
default:
var contour = []; // invariant: equal to the common prefix of the 2 last nodes in the contour
var lastPrefix = entries[0].path.commonPrefix(entries[1].path);
contour.push(entries[0], entries[1]);
for (var i = 2; i < entries.length; i++) {
var entry = entries[i];
var newPrefix = entry.path.commonPrefix(contour[contour.length - 1].path);
while (contour.length > 1 && newPrefix.bitLength < lastPrefix.bitLength) {
var foldedPrefix = fold(contour, lastPrefix);
if (foldedPrefix) {
lastPrefix = foldedPrefix;
}
}
contour.push(entry);
lastPrefix = newPrefix;
}
while (contour.length > 1) {
lastPrefix = fold(contour, lastPrefix);
}
return contour[0].hash;
}
}
/**
* Error indicating a malformed `MapProof`.
*/
var MapProofError =
/*#__PURE__*/
function (_Error) {
(0, _inherits2["default"])(MapProofError, _Error);
function MapProofError(type) {
var _this2;
(0, _classCallCheck2["default"])(this, MapProofError);
switch (type) {
case 'malformedProof':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, 'malformed `proof` part of the proof'));
break;
case 'malformedEntries':
case 'unknownEntryType':
case 'ambiguousEntryType':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, 'malformed `entries` part of the proof'));
break;
case 'embeddedPaths':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, "embedded paths in proof: ".concat(arguments.length <= 1 ? undefined : arguments[1], " is a prefix of ").concat(arguments.length <= 2 ? undefined : arguments[2])));
break;
case 'duplicatePath':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, "duplicate ".concat(arguments.length <= 1 ? undefined : arguments[1], " in proof")));
break;
case 'invalidOrdering':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, 'invalid path ordering'));
break;
case 'nonTerminalNode':
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, 'non-terminal isolated node in proof'));
break;
default:
_this2 = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(MapProofError).call(this, type));
}
return _this2;
}
return MapProofError;
}((0, _wrapNativeSuper2["default"])(Error));
exports.MapProofError = MapProofError;