types/threshold-sha256.js

'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