Source: mining/template.js

/*!
 * 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;