UNPKG

five-bells-condition

Version:
355 lines (313 loc) 11.4 kB
'use strict' /** * @module types */ const Condition = require('../lib/condition') const Fulfillment = require('../lib/fulfillment') const BaseSha256 = require('./base-sha256') const MissingDataError = require('../errors/missing-data-error') const isInteger = require('../util/is-integer') const Asn1ThresholdFingerprintContents = require('../schemas/fingerprint').ThresholdFingerprintContents const CONDITION = 'condition' const FULFILLMENT = 'fulfillment' /** * THRESHOLD-SHA-256: Threshold gate condition using SHA-256. * * Threshold conditions can be used to create m-of-n multi-signature groups. * * Threshold conditions can represent the AND operator by setting the threshold * to equal the number of subconditions (n-of-n) or the OR operator by setting * the thresold to one (1-of-n). * * Since threshold conditions operate on conditions, they can be nested as well * which allows the creation of deep threshold trees of public keys. * * By using Merkle trees, threshold fulfillments do not need to to provide the * structure of unfulfilled subtrees. That means only the public keys that are * actually used in a fulfillment, will actually appear in the fulfillment, * saving space. * * One way to formally interpret a threshold condition is as a booleanthreshold * gate. A tree of threshold conditions forms a boolean threshold circuit. * * THRESHOLD-SHA-256 is assigned the type ID 2. It relies on the SHA-256 and * THRESHOLD feature suites which corresponds to a feature bitmask of 0x09. */ class ThresholdSha256 extends BaseSha256 { constructor () { super() this.threshold = null this.subconditions = [] } /** * Add a subcondition (unfulfilled). * * This can be used to generate a new threshold condition from a set of * subconditions or to provide a non-fulfilled subcondition when creating a * threshold fulfillment. * * @param {Condition|String} subcondition Condition object or URI string * representing a new subcondition to be added. */ addSubcondition (subcondition) { if (typeof subcondition === 'string') { subcondition = Condition.fromUri(subcondition) } else if (!(subcondition instanceof Condition)) { throw new Error('Subconditions must be URIs or objects of type Condition') } this.subconditions.push({ type: CONDITION, body: subcondition }) } /** * Add a fulfilled subcondition. * * When constructing a threshold fulfillment, this method allows you to * provide a fulfillment for one of the subconditions. * * Note that you do **not** have to add the subcondition if you're adding the * fulfillment. The condition can be calculated from the fulfillment and will * be added automatically. * * @param {Fulfillment|String} subfulfillment Fulfillment object or URI string * representing a new subfulfillment to be added. */ addSubfulfillment (subfulfillment) { if (typeof subfulfillment === 'string') { subfulfillment = Fulfillment.fromUri(subfulfillment) } else if (!(subfulfillment instanceof Fulfillment)) { throw new Error('Subfulfillments must be URIs or objects of type Fulfillment') } this.subconditions.push({ type: FULFILLMENT, body: subfulfillment }) } /** * Set the threshold. * * Determines the threshold that is used to consider this condition fulfilled. * If the number of valid subfulfillments is greater or equal to this number, * the threshold condition is considered to be fulfilled. * * @param {Number} threshold Integer threshold */ setThreshold (threshold) { if (!isInteger(threshold) || threshold < 1) { throw new TypeError('Threshold must be a integer greater than zero, was: ' + threshold) } this.threshold = threshold } /** * Get set of used type names. * * This is a type of condition that can contain subconditions. A complete * set of subtypes must contain all types that must be supported in order to * validate this fulfillment. Therefore, we need to join the type of this * fulfillment with all of the sets of subtypes for each of the subconditions. * * @return {Number} Complete set of types for this fulfillment. */ getSubtypes () { const typeSets = this.subconditions .map(x => Array.from(x.body.getSubtypes()).concat(x.body.getTypeName())) const subtypes = new Set(Array.prototype.concat.apply([], typeSets)) // Never include our own type as a subtype. The reason is that we already // know that the validating implementation knows how to interpret this type, // otherwise it wouldn't be able to verify this fulfillment to begin with. subtypes.delete(this.constructor.TYPE_NAME) return subtypes } /** * Comparison function used to pre-sort conditions due to lack of sorting * support in our current DER encoder of choice. * * See: https://github.com/indutny/asn1.js/issues/80 * * @param {Condition} a First condition to compare * @param {Condition} b Second condition to compare * * @private */ static compareConditions (a, b) { return Buffer.compare(a.serializeBinary(), b.serializeBinary()) } /** * Produce the contents of the condition hash. * * This function is called internally by the `getCondition` method. * * @return {Buffer} Encoded contents of fingerprint hash. * * @private */ getFingerprintContents () { return Asn1ThresholdFingerprintContents.encode({ threshold: this.threshold, subconditions: this.subconditions .map(x => ( x.body instanceof Condition ? x.body : x.body.getCondition() )) .sort(ThresholdSha256.compareConditions) .map(x => x.getAsn1Json()) }) } /** * Calculate the cost of fulfilling this condition. * * In a threshold condition, the cost consists of the t most expensive * subconditions plus n times 32 where t is the threshold and n is the * total number of subconditions. * * @return {Number} Expected maximum cost to fulfill this condition * @private */ calculateCost () { // Calculate length of longest fulfillments const subconditions = this.subconditions .map(this.constructor.getSubconditionCost) const worstCaseFulfillmentsCost = this.constructor.calculateWorstCaseLength( this.threshold, subconditions ) if (worstCaseFulfillmentsCost === -Infinity) { throw new MissingDataError('Insufficient number of subconditions to meet the threshold') } return worstCaseFulfillmentsCost + 1024 * subconditions.length } static getSubconditionCost (cond) { return cond.type === FULFILLMENT ? cond.body.getCondition().getCost() : cond.body.getCost() } /** * Calculate the worst case cost of a set of subconditions. * * Given a set of costs C and a threshold t, it returns the sum of the largest * t elements in C. * * @param {Number} threshold Threshold that the remaining subconditions have * to meet. * @param {Number[]} subconditionCosts Set of subconditions. * @return {Number} Maximum cost of a valid, minimal set of fulfillments or * -Infinity if there is no valid set. * * @private */ static calculateWorstCaseLength (threshold, subconditionCosts) { if (subconditionCosts.length < threshold) { return -Infinity } return subconditionCosts .sort((a, b) => a - b) .slice(-threshold) .reduce((total, size) => total + size, 0) } parseJson (json) { this.setThreshold(json.threshold) if (json.subfulfillments) { for (let fulfillmentJson of json.subfulfillments) { this.addSubfulfillment(Fulfillment.fromJson(fulfillmentJson)) } } if (json.subconditions) { for (let conditionJson of json.subconditions) { this.addSubcondition(Condition.fromJson(conditionJson)) } } } parseAsn1JsonPayload (json) { this.setThreshold(json.subfulfillments.length) if (json.subfulfillments) { for (let fulfillmentJson of json.subfulfillments) { this.addSubfulfillment(Fulfillment.fromAsn1Json(fulfillmentJson)) } } if (json.subconditions) { for (let conditionJson of json.subconditions) { this.addSubcondition(Condition.fromAsn1Json(conditionJson)) } } } getAsn1JsonPayload () { const fulfillments = this.subconditions.filter(x => x.type === FULFILLMENT) .sort((a, b) => a.body.getCondition().getCost() - b.body.getCondition().getCost()) const conditions = this.subconditions.filter(x => x.type === CONDITION) if (fulfillments.length < this.threshold) { throw new Error('Not enough fulfillments') } const minimalFulfillments = fulfillments .slice(0, this.threshold) const remainingConditions = conditions .map(x => x.body) .concat( fulfillments .slice(this.threshold) .map(x => x.body.getCondition()) ) return { subfulfillments: minimalFulfillments .map(x => x.body) .sort(ThresholdSha256.compareConditions) .map(x => x.getAsn1Json()), subconditions: remainingConditions .sort(ThresholdSha256.compareConditions) .map(x => x.getAsn1Json()) } } /** * Select the smallest valid set of fulfillments. * * From a set of fulfillments, selects the smallest combination of * fulfillments which meets the given threshold. * * @param {Number} threshold (Remaining) threshold that must be met. * @param {Object[]} fulfillments Set of fulfillments * @return {Object[]} Minimal set of fulfillments. * * @private */ static calculateSmallestValidFulfillmentSet (threshold, fulfillments) { fulfillments.sort((a, b) => b.size - a.size) return fulfillments.slice(0, threshold) } /** * Check whether this fulfillment meets all validation criteria. * * This will validate the subfulfillments and verify that there are enough * subfulfillments to meet the threshold. * * @param {Buffer} message Message to validate against. * @return {Boolean} Whether this fulfillment is valid. */ validate (message) { const fulfillments = this.subconditions.filter((cond) => cond.type === FULFILLMENT) // Number of fulfilled conditions must meet the threshold if (fulfillments.length < this.threshold) { throw new Error('Threshold not met') } // But the set must be minimal, there mustn't be any fulfillments // we could take out if (fulfillments.length > this.threshold) { throw new Error('Fulfillment is not minimal') } // Ensure all subfulfillments are valid return fulfillments.every((f) => f.body.validate(message)) } } ThresholdSha256.TYPE_ID = 2 ThresholdSha256.TYPE_NAME = 'threshold-sha-256' ThresholdSha256.TYPE_ASN1_CONDITION = 'thresholdSha256Condition' ThresholdSha256.TYPE_ASN1_FULFILLMENT = 'thresholdSha256Fulfillment' ThresholdSha256.TYPE_CATEGORY = 'compound' // DEPRECATED ThresholdSha256.prototype.addSubconditionUri = ThresholdSha256.prototype.addSubcondition ThresholdSha256.prototype.addSubfulfillmentUri = ThresholdSha256.prototype.addSubfulfillment module.exports = ThresholdSha256