UNPKG

hsd

Version:
892 lines (719 loc) 16.3 kB
/*! * covenant.js - covenant object for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const bio = require('bufio'); const util = require('../utils/util'); const rules = require('../covenants/rules'); const consensus = require('../protocol/consensus'); const {encoding} = bio; const {types, typesByVal} = rules; /** @typedef {import('@handshake-org/bfilter').BloomFilter} BloomFilter */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').BufioWriter} BufioWriter */ /** @typedef {import('./address')} Address */ /** @typedef {ReturnType<Covenant['getJSON']>} CovenantJSON */ /** * Covenant * @alias module:primitives.Covenant * @property {Number} type * @property {Buffer[]} items * @property {Number} length */ class Covenant extends bio.Struct { /** * Create a covenant. * @constructor * @param {rules.types|Object} [type] * @param {Buffer[]} [items] */ constructor(type, items) { super(); this.type = types.NONE; this.items = []; if (type != null) this.fromOptions(type, items); } /** * Inject properties from options object. * @param {rules.types|Object} [type] * @param {Buffer[]} [items] * @returns {this} */ fromOptions(type, items) { if (type && typeof type === 'object') { items = type.items; type = type.type; } if (Array.isArray(type)) return this.fromArray(type); if (type != null) { assert((type & 0xff) === type); this.type = type; if (items) return this.fromArray(items); return this; } return this; } /** * Get an item. * @param {Number} index * @returns {Buffer} */ get(index) { if (index < 0) index += this.items.length; assert((index >>> 0) === index); assert(index < this.items.length); return this.items[index]; } /** * Set an item. * @param {Number} index * @param {Buffer} item * @returns {Covenant} */ set(index, item) { if (index < 0) index += this.items.length; assert((index >>> 0) === index); assert(index <= this.items.length); assert(Buffer.isBuffer(item)); this.items[index] = item; return this; } /** * Push an item. * @param {Buffer} item * @returns {this} */ push(item) { assert(Buffer.isBuffer(item)); this.items.push(item); return this; } /** * Get a uint8. * @param {Number} index * @returns {Number} */ getU8(index) { const item = this.get(index); assert(item.length === 1); return item[0]; } /** * Push a uint8. * @param {Number} num * @returns {Covenant} */ pushU8(num) { assert((num & 0xff) === num); const item = Buffer.allocUnsafe(1); item[0] = num; this.push(item); return this; } /** * Get a uint32. * @param {Number} index * @returns {Number} */ getU32(index) { const item = this.get(index); assert(item.length === 4); return bio.readU32(item, 0); } /** * Push a uint32. * @param {Number} num * @returns {Covenant} */ pushU32(num) { assert((num >>> 0) === num); const item = Buffer.allocUnsafe(4); bio.writeU32(item, num, 0); this.push(item); return this; } /** * Get a hash. * @param {Number} index * @returns {Hash} */ getHash(index) { const item = this.get(index); assert(item.length === 32); return item; } /** * Push a hash. * @param {Hash} hash * @returns {Covenant} */ pushHash(hash) { assert(Buffer.isBuffer(hash)); assert(hash.length === 32); this.push(hash); return this; } /** * Get a string. * @param {Number} index * @returns {String} */ getString(index) { const item = this.get(index); assert(item.length >= 1 && item.length <= 63); return item.toString('binary'); } /** * Push a string. * @param {String} str * @returns {Covenant} */ pushString(str) { assert(typeof str === 'string'); assert(str.length >= 1 && str.length <= 63); this.push(Buffer.from(str, 'binary')); return this; } /** * Test whether the covenant is known. * @returns {Boolean} */ isKnown() { return this.type <= types.REVOKE; } /** * Test whether the covenant is unknown. * @returns {Boolean} */ isUnknown() { return this.type > types.REVOKE; } /** * Test whether the covenant is a payment. * @returns {Boolean} */ isNone() { return this.type === types.NONE; } /** * Test whether the covenant is a claim. * @returns {Boolean} */ isClaim() { return this.type === types.CLAIM; } /** * Test whether the covenant is an open. * @returns {Boolean} */ isOpen() { return this.type === types.OPEN; } /** * Test whether the covenant is a bid. * @returns {Boolean} */ isBid() { return this.type === types.BID; } /** * Test whether the covenant is a reveal. * @returns {Boolean} */ isReveal() { return this.type === types.REVEAL; } /** * Test whether the covenant is a redeem. * @returns {Boolean} */ isRedeem() { return this.type === types.REDEEM; } /** * Test whether the covenant is a register. * @returns {Boolean} */ isRegister() { return this.type === types.REGISTER; } /** * Test whether the covenant is an update. * @returns {Boolean} */ isUpdate() { return this.type === types.UPDATE; } /** * Test whether the covenant is a renewal. * @returns {Boolean} */ isRenew() { return this.type === types.RENEW; } /** * Test whether the covenant is a transfer. * @returns {Boolean} */ isTransfer() { return this.type === types.TRANSFER; } /** * Test whether the covenant is a finalize. * @returns {Boolean} */ isFinalize() { return this.type === types.FINALIZE; } /** * Test whether the covenant is a revocation. * @returns {Boolean} */ isRevoke() { return this.type === types.REVOKE; } /** * Build helpers */ /** * Set covenant to NONE. * @returns {Covenant} */ setNone() { this.type = types.NONE; this.items = []; return this; } /** * Set covenant to OPEN. * @param {Hash} nameHash * @param {Buffer} rawName * @returns {Covenant} */ setOpen(nameHash, rawName) { this.type = types.OPEN; this.items = []; this.pushHash(nameHash); this.pushU32(0); this.push(rawName); return this; } /** * Set covenant to BID. * @param {Hash} nameHash * @param {Number} height * @param {Buffer} rawName * @param {Hash} blind * @returns {Covenant} */ setBid(nameHash, height, rawName, blind) { this.type = types.BID; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.push(rawName); this.pushHash(blind); return this; } /** * Set covenant to REVEAL. * @param {Hash} nameHash * @param {Number} height * @param {Hash} nonce * @returns {Covenant} */ setReveal(nameHash, height, nonce) { this.type = types.REVEAL; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.pushHash(nonce); return this; } /** * Set covenant to REDEEM. * @param {Hash} nameHash * @param {Number} height * @returns {Covenant} */ setRedeem(nameHash, height) { this.type = types.REDEEM; this.items = []; this.pushHash(nameHash); this.pushU32(height); return this; } /** * Set covenant to REGISTER. * @param {Hash} nameHash * @param {Number} height * @param {Buffer} record * @param {Hash} blockHash * @returns {Covenant} */ setRegister(nameHash, height, record, blockHash) { this.type = types.REGISTER; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.push(record); this.pushHash(blockHash); return this; } /** * Set covenant to UPDATE. * @param {Hash} nameHash * @param {Number} height * @param {Buffer} resource * @returns {Covenant} */ setUpdate(nameHash, height, resource) { this.type = types.UPDATE; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.push(resource); return this; } /** * Set covenant to RENEW. * @param {Hash} nameHash * @param {Number} height * @param {Hash} blockHash * @returns {Covenant} */ setRenew(nameHash, height, blockHash) { this.type = types.RENEW; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.pushHash(blockHash); return this; } /** * Set covenant to TRANSFER. * @param {Hash} nameHash * @param {Number} height * @param {Address} address * @returns {Covenant} */ setTransfer(nameHash, height, address) { this.type = types.TRANSFER; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.pushU8(address.version); this.push(address.hash); return this; } /** * Set covenant to REVOKE. * @param {Hash} nameHash * @param {Number} height * @param {Buffer} rawName * @param {Number} flags * @param {Number} claimed * @param {Number} renewals * @param {Hash} blockHash * @returns {Covenant} */ setFinalize(nameHash, height, rawName, flags, claimed, renewals, blockHash) { this.type = types.FINALIZE; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.push(rawName); this.pushU8(flags); this.pushU32(claimed); this.pushU32(renewals); this.pushHash(blockHash); return this; } /** * Set covenant to REVOKE. * @param {Hash} nameHash * @param {Number} height * @returns {Covenant} */ setRevoke(nameHash, height) { this.type = types.REVOKE; this.items = []; this.pushHash(nameHash); this.pushU32(height); return this; } /** * Set covenant to CLAIM. * @param {Hash} nameHash * @param {Number} height * @param {Buffer} rawName * @param {Number} flags * @param {Hash} commitHash * @param {Number} commitHeight * @returns {Covenant} */ setClaim(nameHash, height, rawName, flags, commitHash, commitHeight) { this.type = types.CLAIM; this.items = []; this.pushHash(nameHash); this.pushU32(height); this.push(rawName); this.pushU8(flags); this.pushHash(commitHash); this.pushU32(commitHeight); return this; } /** * Test whether the covenant is name-related. * @returns {Boolean} */ isName() { if (this.type < types.CLAIM) return false; if (this.type > types.REVOKE) return false; return true; } /** * Test whether a covenant type should be * considered subject to the dust policy rule. * @returns {Boolean} */ isDustworthy() { switch (this.type) { case types.NONE: case types.BID: return true; default: return this.type > types.REVOKE; } } /** * Test whether a coin should be considered * unspendable in the coin selector. * @returns {Boolean} */ isNonspendable() { switch (this.type) { case types.NONE: case types.OPEN: case types.REDEEM: return false; default: return true; } } /** * Test whether a covenant should be considered "linked". * @returns {Boolean} */ isLinked() { return this.type >= types.REVEAL && this.type <= types.REVOKE; } /** * Convert covenant to an array of buffers. * @returns {Buffer[]} */ toArray() { return this.items.slice(); } /** * Inject properties from an array of buffers. * @private * @param {Buffer[]} items */ fromArray(items) { assert(Array.isArray(items)); this.items = items; return this; } /** * Test whether the covenant is unspendable. * @returns {Boolean} */ isUnspendable() { return this.type === types.REVOKE; } /** * Convert the covenant to a string. * @returns {String} */ toString() { return this.encode().toString('hex', 1); } /** * Inject properties from covenant. * Used for cloning. * @param {this} covenant * @returns {this} */ inject(covenant) { assert(covenant instanceof this.constructor); this.type = covenant.type; this.items = covenant.items.slice(); return this; } /** * Test the covenant against a bloom filter. * @param {BloomFilter} filter * @returns {Boolean} */ test(filter) { for (const item of this.items) { if (item.length === 0) continue; if (filter.test(item)) return true; } return false; } /** * Find a data element in a covenant. * @param {Buffer} data - Data element to match against. * @returns {Number} Index (`-1` if not present). */ indexOf(data) { for (let i = 0; i < this.items.length; i++) { const item = this.items[i]; if (item.equals(data)) return i; } return -1; } /** * Calculate size of the covenant * excluding the varint size bytes. * @returns {Number} */ getSize() { let size = 0; for (const item of this.items) size += encoding.sizeVarBytes(item); return size; } /** * Calculate size of the covenant * including the varint size bytes. * @returns {Number} */ getVarSize() { return 1 + encoding.sizeVarint(this.items.length) + this.getSize(); } /** * Write covenant to a buffer writer. * @param {BufioWriter} bw * @returns {BufioWriter} */ write(bw) { bw.writeU8(this.type); bw.writeVarint(this.items.length); for (const item of this.items) bw.writeVarBytes(item); return bw; } /** * Encode covenant. * @returns {Buffer} */ encode() { const bw = bio.write(this.getVarSize()); this.write(bw); return bw.render(); } /** * Convert covenant to a hex string. */ getJSON() { const items = []; for (const item of this.items) items.push(item.toString('hex')); return { type: this.type, action: typesByVal[this.type], items }; } /** * Inject properties from json object. * @param {CovenantJSON} json * @returns {this} */ fromJSON(json) { assert(json && typeof json === 'object', 'Covenant must be an object.'); assert((json.type & 0xff) === json.type); assert(Array.isArray(json.items)); this.type = json.type; for (const str of json.items) { const item = util.parseHex(str, -1); this.items.push(item); } return this; } /** * Inject properties from buffer reader. * @param {bio.BufferReader} br * @returns {this} */ read(br) { this.type = br.readU8(); const count = br.readVarint(); if (count > consensus.MAX_SCRIPT_STACK) throw new Error('Too many covenant items.'); for (let i = 0; i < count; i++) this.items.push(br.readVarBytes()); return this; } /** * Inject items from string. * @param {String|String[]} items * @returns {this} */ fromString(items) { if (!Array.isArray(items)) { assert(typeof items === 'string'); items = items.trim(); if (items.length === 0) return this; items = items.split(/\s+/); } for (const item of items) this.items.push(util.parseHex(item, -1)); return this; } /** * Inspect a covenant object. * @returns {String} */ format() { return `<Covenant: ${typesByVal[this.type]}:${this.toString()}>`; } /** * Insantiate covenant from an array of buffers. * @param {Buffer[]} items * @returns {Covenant} */ static fromArray(items) { return new this().fromArray(items); } /** * Test an object to see if it is a covenant. * @param {Object} obj * @returns {Boolean} */ static isCovenant(obj) { return obj instanceof Covenant; } } Covenant.types = types; /* * Expose */ module.exports = Covenant;