UNPKG

@yipsec/merked

Version:

a directed acyclic graph using merkle trees

154 lines (129 loc) 4.48 kB
/** * A directed acyclic graph (DAG) is a type of graph that consists of vertices * connected by directed edges, where there are no cycles, meaning you cannot * return to the same vertex by following the direction of the edges. * @module merked/dag */ 'use strict'; const { getRandomValues } = require('node:crypto'); const { splitSync } = require('./split'); const { MerkleTree } = require('./tree'); class DAG { /** * @typedef module:merked/dag~DAG~Options * @type {object} * @property {number} sliceIndex - Where original input becomes padding * @property {function} hashFunc - Hash function to use for {@link module:merked/tree~MerkleTree} */ /** * Computes the default options for {@link DAG} instance given a shard array. * @static * @param {Buffer[]} shards - Array of buffers to use as hashed leaf input * @returns {module:merked/dag~DAG~Options} */ static DEFAULT_OPTS(shards = []) { let byteLength = 0; shards.forEach(s => byteLength += s.length); return { sliceIndex: byteLength, hashFunc: MerkleTree.DEFAULT_HASH_FUNC }; } /** * Given a list of uniform buffers, construct a Merkle Tree. * @constructor * @param {Array<Buffer>} shards - Uniform shards to compose an entry * @param {module:merked/dag~DAG~Options} [options] */ constructor(shards, opts = {}) { const defaults = DAG.DEFAULT_OPTS(shards); this.shards = shards; this.leaves = this.shards.map(opts.hashFunc || defaults.hashFunc); this.merkle = new MerkleTree(this.leaves, opts.hashFunc || defaults.hashFunc); this.entries = []; this.sliceIndex = typeof opts.sliceIndex === 'undefined' ? defaults.sliceIndex : opts.sliceIndex; for (let i = 0; i < this.shards.length; i++) { this.entries.push([this.leaves[i], this.shards[i]]); } for (let i = 0; i < this.merkle.length; i++) { this.entries.push([this.leaves[i], this.shards[i]]); } } /** * Get the underlying shard array. * @returns {Array<Buffer>} */ toArray() { return this.shards; } /** * @typedef module:merked/dag~DAG~Metadata * @type {object} * @property {string} n - Application-specific name * @property {Array<string>} l - Input leaves as hex * @property {string} r - Root merkle node as hex * @property {number} s - Index where content ends and padding begins */ /** * Export a serializable metadata object * @param {string} [name="blob"] - Application-specific tag or filename * @returns {module:merked/dag~DAG~Metadata} */ toMetadata(name) { return JSON.stringify({ n: name || 'blob', l: this.leaves.map(l => l.toString('hex')), r: this.merkle.root().toString('hex'), s: this.sliceIndex }); } /** * @property {string} DEFAULT_INPUT_SIZE * @memberof {DAG} */ static get DEFAULT_INPUT_SIZE() { return '4M'; } /** * Construct a {@link DAG} from a Buffer. * @param {Buffer} buffer - Raw input bytes * @param {number} [sliceSize={DAG.DEFAULT_INPUT_SIZE}] - Split buffer into uniform chunks * @param {function} [hashFunc={module:merked/tree~MerkleTree.DEFAULT_HASH_FUNC}] - Hash function to use * @param {boolean} [padLastSlice=false] - If true, pad the final slice before hashing * @param {boolean} [randomFill=false] - If true, pad with random bytes (will always result in a different tree) * @returns {DAG} */ static fromBuffer(buffer, sliceSize = DAG.DEFAULT_INPUT_SIZE, hashFunc = MerkleTree.DEFAULT_HASH_FUNC, padLastSlice = false, randomFill = false) { const shards = splitSync(buffer, { bytes: sliceSize }).map(s => { let bufN = Buffer.alloc(sliceSize); bufN.fill(s); if (randomFill && bufN.length > s.length) { bufN.fill(DAG.randomFill(bufN.length - s.length), s.length); } else { bufN.fill(0, s.length); } return bufN; }); return new DAG(shards, { sliceIndex: buffer.length, hashFunc }); } /** * Get a buffer filled with random bytes - used to pad last shard. * @param {number} numBytes - Size of buffer to create * @returns {Buffer} */ static randomFill(numBytes) { const max = 65536; const buf = Buffer.alloc(numBytes); let offset = 0; while (offset < numBytes) { offset += buf.fill(getRandomValues(Buffer.alloc(max)), offset); } return buf; } } module.exports.DAG = DAG;