o2-client
Version:
Light Client for O2 Blockchain
352 lines (280 loc) • 11.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MapProofError = exports.MapProof = undefined;
var _binarySearch = require('binary-search');
var _binarySearch2 = _interopRequireDefault(_binarySearch);
var _crypto = require('../crypto');
var _generic = require('../types/generic');
var _hexadecimal = require('../types/hexadecimal');
var _ProofPath = require('./ProofPath');
var _ProofPath2 = _interopRequireDefault(_ProofPath);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* 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 serialize to exactly 32 bytes.
* @param {{hash?: (any) => string, 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 should possess
* one of `hash` or `serialize` methods.
* @throws {MapProofError}
* if the proof is malformed
*/
exports.MapProof = function MapProof(json, keyType, valueType) {
_classCallCheck(this, MapProof);
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;
if (!valueType || typeof valueType.serialize !== 'function' && typeof valueType.hash !== 'function') {
throw new TypeError('No `hash` or `serialize` method in the value type');
}
this.valueType = valueType;
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);
}
}
this.merkleRoot = collect(completeProof.filter(function (_ref4) {
var hash = _ref4.hash;
return !!hash;
}));
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];
}));
};
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 _ProofPath2.default(path),
hash: hash
};
});
}
function parseEntries(entries, keyType, valueType) {
function createPath(data) {
var bytes = keyType.serialize(data, [], 0);
if (bytes.length !== _ProofPath2.default.BYTE_LENGTH) {
throw new TypeError('invalid key type; keys should serialize to ' + (_ProofPath2.default.BYTE_LENGTH + '-byte buffers'));
}
return new _ProofPath2.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: typeof valueType.hash === 'function' ? valueType.hash(value) // `newType`s
: (0, _crypto.hash)(valueType.serialize(value, [], 0)) // "primitive" types
};
});
}
/**
* @param 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, _binarySearch2.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);
}
}
});
}
var IsolatedNode = (0, _generic.newType)({
fields: [{ name: 'path', type: _ProofPath2.default.TYPE }, { name: 'hash', type: _hexadecimal.Hash }]
});
var BranchNode = (0, _generic.newType)({
fields: [{ name: 'left_hash', type: _hexadecimal.Hash }, { name: 'right_hash', type: _hexadecimal.Hash }, { name: 'left_path', type: _ProofPath2.default.TYPE }, { name: 'right_path', type: _ProofPath2.default.TYPE }]
});
function collect(entries) {
function hashIsolatedNode(_ref13) {
var path = _ref13.path,
valueHash = _ref13.hash;
return (0, _crypto.hash)({ hash: valueHash, path: path }, IsolatedNode);
}
function hashBranch(left, right) {
var branch = {
left_hash: left.hash,
right_hash: right.hash,
left_path: left.path,
right_path: right.path
};
return (0, _crypto.hash)(branch, BranchNode);
}
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 = exports.MapProofError = function (_Error) {
_inherits(MapProofError, _Error);
function MapProofError(type) {
_classCallCheck(this, MapProofError);
switch (type) {
case 'malformedProof':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'malformed `proof` part of the proof'));
break;
case 'malformedEntries':
case 'unknownEntryType':
case 'ambiguousEntryType':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'malformed `entries` part of the proof'));
break;
case 'embeddedPaths':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'embedded paths in proof: ' + (arguments.length <= 1 ? undefined : arguments[1]) + ' is a prefix of ' + (arguments.length <= 2 ? undefined : arguments[2])));
break;
case 'duplicatePath':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'duplicate ' + (arguments.length <= 1 ? undefined : arguments[1]) + ' in proof'));
break;
case 'invalidOrdering':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'invalid path ordering'));
break;
case 'nonTerminalNode':
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, 'non-terminal isolated node in proof'));
break;
default:
var _this2 = _possibleConstructorReturn(this, (MapProofError.__proto__ || Object.getPrototypeOf(MapProofError)).call(this, type));
}
return _this2;
}
return MapProofError;
}(Error);