lib_tree.js

/**
 * A hash tree or Merkle tree is a tree in which every "leaf" node is labelled 
 * with the cryptographic hash of a data block, and every node that is not a 
 * leaf (called a branch, inner node, or inode) is labelled with the 
 * cryptographic hash of the labels of its child nodes. A hash tree allows 
 * efficient and secure verification of the contents of a large data structure. 
 * A hash tree is a generalization of a hash list and a hash chain. 
 * @module merked/tree
 */

'use strict';

const assert = require('node:assert');
const crypto = require('node:crypto');


class MerkleTree {

  /**
   * Implements a merkle hash tree
   * @constructor
   * @param {string[]|Buffer[]} leaves - Initial tree input
   * @param {Function} hasher - Hash function for building tree
   */
  constructor(leaves, hasher) {
    this._hasher = hasher || MerkleTree.DEFAULT_HASH_FUNC;
    this._leaves = [];
    this._depth = 0;
    this._rows = [];
    this._count = 0;

    assert(Array.isArray(leaves), 'Invalid leaves array supplied');
    assert(typeof this._hasher === 'function', 
      'Invalid hash function supplied');

    for (var i = 0; i < leaves.length; i++) {
      this._feed(leaves[i]);
    }

    this._compute();
  }

  /**
   * Computes the tree depth
   * @returns {Number}
   */
  depth() {
    if (!this._depth) {
      var pow = 0;

      while (Math.pow(2, pow) < this._leaves.length){
        pow++;
      }

      this._depth = pow;
    }

    return this._depth;
  }

  /**
   * Returns the number of levels in the tree
   * @returns {Number}
   */
  levels() {
    return this.depth() + 1;
  }

  /**
   * Returns the number of nodes in the tree
   * @returns {Number}
   */
  nodes() {
    return this._count;
  }

  /**
   * Returns the merkle root of the tree
   * @returns {String}
   */
  root() {
    return this._rows[0][0];
  }

  /**
   * Returns the leaves at the given level
   * @param {Number} level
   * @returns {String}
   */
  level(level) {
    return this._rows[level];
  }

  /**
   * Inserts leaf into bottom of tree
   * @private
   * @param {String} data
   * @returns {MerkleTree}
   */
  _feed(data) {
    this._leaves.push(data);

    return this;
  }

  /**
   * Builds the tree from the leaves
   * @private
   */
  _compute() {
    var depth = this.depth();

    if (this._rows.length === 0) {
      for (var i = 0; i < depth; i++) {
        this._rows.push([]);
      }

      this._rows[depth] = this._leaves;

      for (var j = depth - 1; j >= 0; j--) {
        this._rows[j] = this._getNodes(this._rows[j + 1]);
        this._count += this._rows[j].length;
      }
    }
  }

  /**
   * Returns the nodes derived from the given leaves
   * @private
   * @param {Array} leaves
   * @returns {Array}
   */
  _getNodes(leaves) {
    var remainder = leaves.length % 2;
    var nodes = [];

    for (var i = 0; i < leaves.length - 1; i = i + 2) {
      nodes[i / 2] = this._hasher(Buffer.concat([leaves[i], leaves[i + 1]]));
    }

    if (remainder === 1){
      nodes[(leaves.length - remainder) / 2] = leaves[leaves.length - 1];
    }

    return nodes;
  }

  /**
   * Default hash function (SHA-256)
   * @private
   * @param {String} input
   * @returns {String}
   */
  static DEFAULT_HASH_FUNC(input) {
    return crypto.createHash('sha256').update(input).digest();
  }

}

module.exports.MerkleTree = MerkleTree;