/*!
* 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} start
* @param {Buffer} rawName
* @param {Hash} blind
* @returns {Covenant}
*/
setBid(nameHash, start, rawName, blind) {
this.type = types.BID;
this.items = [];
this.pushHash(nameHash);
this.pushU32(start);
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;