UNPKG

hsd

Version:
912 lines (733 loc) 20.1 kB
/*! * template.js - block template 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 BLAKE2b = require('bcrypto/lib/blake2b'); const random = require('bcrypto/lib/random'); const merkle = require('bcrypto/lib/mrkl'); const Address = require('../primitives/address'); const TX = require('../primitives/tx'); const Block = require('../primitives/block'); const Headers = require('../primitives/headers'); const Input = require('../primitives/input'); const Output = require('../primitives/output'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const CoinView = require('../coins/coinview'); const rules = require('../covenants/rules'); const common = require('./common'); /** @typedef {import('../types').Amount} AmountValue */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../primitives/claim')} Claim */ /** @typedef {import('../primitives/airdropproof')} AirdropProof */ /** @typedef {import('../mempool/airdropentry')} AirdropEntry */ /** @typedef {import('../mempool/claimentry')} ClaimEntry */ /** @typedef {import('../mempool/mempoolentry')} MempoolEntry */ /* * Constants */ const DUMMY = Buffer.alloc(0); /** * Block Template * @alias module:mining.BlockTemplate */ class BlockTemplate { /** * Create a block template. * @constructor * @param {Object} [options] */ constructor(options) { this.prevBlock = consensus.ZERO_HASH; this.version = 0; this.height = 0; this.time = 0; this.bits = 0; this.target = consensus.ZERO_HASH; this.mtp = 0; this.flags = 0; this.coinbaseFlags = DUMMY; this.address = new Address(); this.sigops = 400; this.weight = 4000; this.opens = 0; this.updates = 0; this.renewals = 0; this.interval = 170000; this.fees = 0; this.merkleRoot = consensus.ZERO_HASH; this.witnessRoot = consensus.ZERO_HASH; this.treeRoot = consensus.ZERO_HASH; this.reservedRoot = consensus.ZERO_HASH; this.coinbase = new TX(); this.items = []; this.claims = []; this.airdrops = []; if (options) this.fromOptions(options); } /** * Inject properties from options. * @param {Object} options * @returns {BlockTemplate} */ fromOptions(options) { assert(options); if (options.prevBlock != null) { assert(Buffer.isBuffer(options.prevBlock)); this.prevBlock = options.prevBlock; } if (options.merkleRoot != null) { assert(Buffer.isBuffer(options.merkleRoot)); this.merkleRoot = options.merkleRoot; } if (options.witnessRoot != null) { assert(Buffer.isBuffer(options.witnessRoot)); this.witnessRoot = options.witnessRoot; } if (options.treeRoot != null) { assert(Buffer.isBuffer(options.treeRoot)); this.treeRoot = options.treeRoot; } if (options.reservedRoot != null) { assert(Buffer.isBuffer(options.reservedRoot)); this.reservedRoot = options.reservedRoot; } if (options.version != null) { assert(typeof options.version === 'number'); this.version = options.version; } if (options.height != null) { assert(typeof options.height === 'number'); this.height = options.height; } if (options.time != null) { assert(typeof options.time === 'number'); this.time = options.time; } if (options.bits != null) this.setBits(options.bits); if (options.target != null) this.setTarget(options.target); if (options.mtp != null) { assert(typeof options.mtp === 'number'); this.mtp = options.mtp; } if (options.flags != null) { assert(typeof options.flags === 'number'); this.flags = options.flags; } if (options.coinbaseFlags != null) { assert(Buffer.isBuffer(options.coinbaseFlags)); this.coinbaseFlags = options.coinbaseFlags; } if (options.address != null) this.address.fromOptions(options.address); if (options.sigops != null) { assert(typeof options.sigops === 'number'); this.sigops = options.sigops; } if (options.weight != null) { assert(typeof options.weight === 'number'); this.weight = options.weight; } if (options.opens != null) { assert(typeof options.opens === 'number'); this.opens = options.opens; } if (options.updates != null) { assert(typeof options.updates === 'number'); this.updates = options.updates; } if (options.renewals != null) { assert(typeof options.renewals === 'number'); this.renewals = options.renewals; } if (options.interval != null) { assert(typeof options.interval === 'number'); this.interval = options.interval; } if (options.fees != null) { assert(typeof options.fees === 'number'); this.fees = options.fees; } if (options.items != null) { assert(Array.isArray(options.items)); this.items = options.items; } if (options.claims != null) { assert(Array.isArray(options.claims)); this.claims = options.claims; } if (options.airdrops != null) { assert(Array.isArray(options.airdrops)); this.airdrops = options.airdrops; } return this; } /** * Instantiate block template from options. * @param {Object} options * @returns {BlockTemplate} */ static fromOptions(options) { return new this().fromOptions(options); } /** * Set the target (bits). * @param {Number} bits */ setBits(bits) { assert(typeof bits === 'number'); this.bits = bits; this.target = common.getTarget(bits); } /** * Set the target (uint256le). * @param {Buffer} target */ setTarget(target) { assert(Buffer.isBuffer(target)); this.bits = common.getBits(target); this.target = target; } /** * Calculate the block reward. * @returns {AmountValue} */ getReward() { const reward = consensus.getReward(this.height, this.interval); return reward + this.fees; } /** * Initialize the default coinbase. * @returns {TX} */ createCoinbase() { const cb = new TX(); // Commit to height. cb.locktime = this.height; // Coinbase input. const input = new Input(); input.sequence = random.randomInt(); input.witness.pushData(Buffer.alloc(20, 0x00)); input.witness.pushData(Buffer.alloc(8, 0x00)); input.witness.pushData(Buffer.alloc(8, 0x00)); input.witness.compile(); cb.inputs.push(input); // Reward output. const output = new Output(); output.address.fromPubkeyhash(Buffer.alloc(20, 0x00)); output.value = this.getReward(); cb.outputs.push(output); // Setup coinbase flags (variable size). input.witness.setData(0, this.coinbaseFlags); input.witness.setData(1, random.randomBytes(8)); input.witness.compile(); // Setup output address (variable size). output.address = this.address; // Add any claims. for (const claim of this.claims) { const input = new Input(); input.witness.items.push(claim.blob); cb.inputs.push(input); let flags = 0; if (claim.weak) flags |= 1; const output = new Output(); output.value = claim.value - claim.fee; output.address = claim.address; output.covenant.setClaim( claim.nameHash, this.height, claim.name, flags, claim.commitHash, claim.commitHeight ); cb.outputs.push(output); } // Add any airdrop proofs. for (const proof of this.airdrops) { const input = new Input(); input.witness.items.push(proof.blob); cb.inputs.push(input); const output = new Output(); output.value = proof.value - proof.fee; output.address = proof.address; cb.outputs.push(output); } cb.refresh(); assert(input.witness.getSize() <= 1000, 'Coinbase witness is too large!'); return cb; } /** * Refresh the coinbase and merkle tree. */ refresh() { const cb = this.createCoinbase(); { const leaves = []; leaves.push(cb.hash()); for (const {tx} of this.items) leaves.push(tx.hash()); this.merkleRoot = merkle.createRoot(BLAKE2b, leaves); } { const leaves = []; leaves.push(cb.witnessHash()); for (const {tx} of this.items) leaves.push(tx.witnessHash()); this.witnessRoot = merkle.createRoot(BLAKE2b, leaves); } this.coinbase = cb; } /** * Create raw block header with given parameters. * @param {Number} nonce * @param {Number} time * @param {Buffer} extraNonce * @param {Buffer} mask * @returns {Buffer} */ getHeader(nonce, time, extraNonce, mask) { const hdr = new Headers(); hdr.version = this.version; hdr.prevBlock = this.prevBlock; hdr.merkleRoot = this.merkleRoot; hdr.witnessRoot = this.witnessRoot; hdr.treeRoot = this.treeRoot; hdr.reservedRoot = this.reservedRoot; hdr.time = time; hdr.bits = this.bits; hdr.nonce = nonce; hdr.extraNonce = extraNonce; hdr.mask = mask; return hdr.toMiner(); } /** * Calculate proof with given parameters. * @param {Number} nonce * @param {Number} time * @param {Buffer} extraNonce * @param {Buffer} mask * @returns {BlockProof} */ getProof(nonce, time, extraNonce, mask) { const hdr = this.getHeader(nonce, time, extraNonce, mask); const proof = new BlockProof(); proof.hdr = hdr; proof.time = time; proof.nonce = nonce; proof.extraNonce = extraNonce; proof.mask = mask; return proof; } /** * Create block from calculated proof. * @param {BlockProof} proof * @returns {Block} */ commit(proof) { const block = new Block(); block.version = this.version; block.prevBlock = this.prevBlock; block.merkleRoot = this.merkleRoot; block.witnessRoot = this.witnessRoot; block.treeRoot = this.treeRoot; block.reservedRoot = this.reservedRoot; block.time = proof.time; block.bits = this.bits; block.nonce = proof.nonce; block.extraNonce = proof.extraNonce; block.mask = proof.mask; block.txs.push(this.coinbase); for (const item of this.items) block.txs.push(item.tx); return block; } /** * Quick and dirty way to * get a coinbase tx object. * @returns {TX} */ toCoinbase() { return this.coinbase.clone(); } /** * Quick and dirty way to get a block * object (most likely to be an invalid one). * @returns {Block} */ toBlock() { const extraNonce = consensus.ZERO_NONCE; const mask = consensus.ZERO_HASH; const proof = this.getProof(0, this.time, extraNonce, mask); return this.commit(proof); } /** * Calculate the target difficulty. * @returns {Number} */ getDifficulty() { return common.getDifficulty(this.target); } /** * Set the reward output * address and refresh. * @param {Address} address */ setAddress(address) { this.address = new Address(address); this.refresh(); } /** * Add a transaction to the template. * @param {TX} tx * @param {CoinView} view * @returns {Boolean} */ addTX(tx, view) { assert(!tx.mutable, 'Cannot add mutable TX to block.'); const item = BlockEntry.fromTX(tx, view, this); const weight = item.tx.getWeight(); const sigops = item.sigops; const opens = rules.countOpens(tx); const updates = rules.countUpdates(tx); const renewals = rules.countRenewals(tx); if (!tx.isFinal(this.height, this.mtp)) return false; if (this.weight + weight > consensus.MAX_BLOCK_WEIGHT) return false; if (this.sigops + sigops > consensus.MAX_BLOCK_SIGOPS) return false; if (this.opens + opens > consensus.MAX_BLOCK_OPENS) return false; if (this.updates + updates > consensus.MAX_BLOCK_UPDATES) return false; if (this.renewals + renewals > consensus.MAX_BLOCK_RENEWALS) return false; this.weight += weight; this.sigops += sigops; this.opens += opens; this.updates += updates; this.renewals += renewals; this.fees += item.fee; // Add the tx to our block this.items.push(item); return true; } /** * Add a transaction to the template * (less verification than addTX). * @param {TX} tx * @param {CoinView?} [view] * @returns {Boolean} */ pushTX(tx, view) { assert(!tx.mutable, 'Cannot add mutable TX to block.'); if (!view) view = new CoinView(); const item = BlockEntry.fromTX(tx, view, this); const weight = item.tx.getWeight(); const sigops = item.sigops; const opens = rules.countOpens(tx); const updates = rules.countUpdates(tx); const renewals = rules.countRenewals(tx); this.weight += weight; this.sigops += sigops; this.opens += opens; this.updates += updates; this.renewals += renewals; this.fees += item.fee; // Add the tx to our block this.items.push(item); return true; } /** * Add a claim to the template. * @param {Claim} claim * @param {Object} data * @returns {Boolean} */ addClaim(claim, data) { const entry = BlockClaim.fromClaim(claim, data); if (entry.commitHeight === 1) this.fees += entry.fee; this.claims.push(entry); return true; } /** * Add a claim to the template. * @param {AirdropProof} proof * @returns {Boolean} */ addAirdrop(proof) { const entry = BlockAirdrop.fromAirdrop(proof); this.fees += entry.fee; this.airdrops.push(entry); return true; } } /** * Block Entry * @alias module:mining.BlockEntry * @property {TX} tx * @property {Hash} hash * @property {Amount} fee * @property {Rate} rate * @property {Number} priority * @property {Boolean} free * @property {Sigops} sigops * @property {Number} depCount */ class BlockEntry { /** * Create a block entry. * @constructor * @param {TX} tx */ constructor(tx) { this.tx = tx; this.hash = tx.hash(); this.fee = 0; this.rate = 0; this.priority = 0; this.free = false; this.sigops = 0; this.descRate = 0; this.depCount = 0; } /** * Instantiate block entry from transaction. * @param {TX} tx * @param {CoinView} view * @param {BlockTemplate} attempt * @returns {BlockEntry} */ static fromTX(tx, view, attempt) { const item = new this(tx); item.fee = tx.getFee(view); item.rate = tx.getRate(view); item.priority = tx.getPriority(view, attempt.height); item.free = false; item.sigops = tx.getSigops(view); item.descRate = item.rate; return item; } /** * Instantiate block entry from mempool entry. * @param {MempoolEntry} entry * @param {BlockTemplate} attempt * @returns {BlockEntry} */ static fromEntry(entry, attempt) { const item = new this(entry.tx); item.fee = entry.getFee(); item.rate = entry.getDeltaRate(); item.priority = entry.getPriority(attempt.height); item.free = entry.getDeltaFee() < policy.getMinFee(entry.size); item.sigops = entry.sigops; item.descRate = entry.getDescRate(); return item; } } /** * Block Claim * @alias module:mining.BlockClaim */ class BlockClaim { /** * Create a block entry. * @constructor */ constructor() { this.blob = DUMMY; this.nameHash = consensus.ZERO_HASH; this.name = DUMMY; this.address = new Address(); this.value = 0; this.fee = 0; this.rate = 0; this.weak = false; this.commitHash = consensus.ZERO_HASH; this.commitHeight = 0; } /** * Calculate weight. * @returns {Number} */ getWeight() { const size = 1 + 8 + this.address.getSize() + (90 + this.name.length); const weight = size * consensus.WITNESS_SCALE_FACTOR; return 1 + bio.sizeVarBytes(this.blob) + weight; } /** * Instantiate block entry from transaction. * @param {Claim} claim * @param {Object} data * @returns {BlockClaim} */ static fromClaim(claim, data) { const size = claim.getVirtualSize(); const name = Buffer.from(data.name, 'binary'); const item = new this(); item.blob = claim.blob; item.nameHash = rules.hashName(name); item.name = name; item.address = Address.fromHash(data.hash, data.version); item.value = data.value; item.fee = data.fee; item.rate = policy.getRate(size, item.fee); item.weak = data.weak; item.commitHash = data.commitHash; item.commitHeight = data.commitHeight; return item; } /** * Instantiate block entry from mempool entry. * @param {ClaimEntry} entry * @returns {BlockClaim} */ static fromEntry(entry) { const item = new this(); item.blob = entry.blob; item.nameHash = entry.nameHash; item.name = entry.name; item.address = entry.address; item.value = entry.value; item.fee = entry.fee; item.rate = entry.rate; item.weak = entry.weak; item.commitHash = entry.commitHash; item.commitHeight = entry.commitHeight; return item; } } /** * Block Airdrop * @alias module:mining.BlockAirdrop */ class BlockAirdrop { /** * Create a block entry. * @constructor */ constructor() { this.blob = DUMMY; this.position = 0; this.address = new Address(); this.value = 0; this.fee = 0; this.rate = 0; this.weak = false; } /** * Calculate weight. * @returns {Number} */ getWeight() { const size = 1 + 8 + this.address.getSize() + 5; const weight = size * consensus.WITNESS_SCALE_FACTOR; return 1 + bio.sizeVarBytes(this.blob) + weight; } /** * Instantiate block entry from transaction. * @param {AirdropProof} proof * @returns {BlockAirdrop} */ static fromAirdrop(proof) { const size = proof.getVirtualSize(); const item = new this(); item.blob = proof.encode(); item.position = proof.position(); item.address = Address.fromHash(proof.address, proof.version); item.value = proof.getValue(); item.fee = proof.fee; item.rate = policy.getRate(size, proof.fee); item.weak = proof.isWeak(); return item; } /** * Instantiate block airdrop from mempool airdropentry. * @param {AirdropEntry} entry * @returns {BlockAirdrop} */ static fromEntry(entry) { const item = new this(); item.blob = entry.blob; item.position = entry.position; item.address = entry.address; item.value = entry.value; item.fee = entry.fee; item.rate = entry.rate; item.weak = entry.weak; return item; } } /** * Block Proof */ class BlockProof { /** * Create a block proof. * @constructor */ constructor() { this.hdr = consensus.ZERO_HEADER; this.time = 0; this.nonce = 0; this.extraNonce = consensus.ZERO_NONCE; this.mask = consensus.ZERO_HASH; } /** * @returns {Hash} */ hash() { return this.powHash(); } /** * @returns {Hash} */ shareHash() { const hdr = Headers.fromMiner(this.hdr); return hdr.shareHash(); } /** * @returns {Hash} */ powHash() { const hash = this.shareHash(); for (let i = 0; i < 32; i++) hash[i] ^= this.mask[i]; return hash; } /** * @param {Buffer} target * @returns {Boolean} */ verify(target) { return this.powHash().compare(target) <= 0; } /** * Calculate the target difficulty. * @returns {Number} */ getDifficulty() { return common.getDifficulty(this.powHash()); } } /* * Expose */ exports = BlockTemplate; exports.BlockTemplate = BlockTemplate; exports.BlockEntry = BlockEntry; exports.BlockClaim = BlockClaim; exports.BlockAirdrop = BlockAirdrop; module.exports = exports;