891 lines
16 KiB
JavaScript
891 lines
16 KiB
JavaScript
/*!
|
|
* 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('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;
|