Source: mempool/contractstate.js

/*!
 * contractstate.js - mempool contract-state handling for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const assert = require('bsert');
const {BufferMap, BufferSet} = require('buffer-map');
const rules = require('../covenants/rules');
const NameState = require('../covenants/namestate');
const CoinView = require('../coins/coinview');
const {types} = rules;
const {states} = NameState;

/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../protocol/network')} Network */
/** @typedef {import('../primitives/tx')} TX */

/*
 * Constants
 */

const EMPTY = Buffer.alloc(0);

/*
 * Contract State
 */

class ContractState {
  /**
   * @constructor
   * @param {Network} network
   */

  constructor(network) {
    assert(network);

    // Current network.
    this.network = network;

    // Unique names.
    this.unique = new BufferSet();

    // Reference counter.
    /** @type {BufferMap<Number>} */
    this.refs = new BufferMap();

    // Map of nameHash->set-of-txids.
    /** @type {BufferMap<BufferSet>} */
    this.opens = new BufferMap();

    // Map of nameHash->set-of-txids.
    /** @type {BufferMap<BufferSet>} */
    this.bids = new BufferMap();

    // Map of nameHash->set-of-txids.
    /** @type {BufferMap<BufferSet>} */
    this.reveals = new BufferMap();

    // Map of nameHash->set-of-txids.
    /** @type {BufferMap<BufferSet>} */
    this.updates = new BufferMap();

    // Current on-chain state
    // of all watched names.
    this.view = new CoinView();
    this.names = this.view.names;
  }

  clear() {
    this.unique.clear();
    this.refs.clear();
    this.opens.clear();
    this.bids.clear();
    this.reveals.clear();
    this.names.clear();
    return this;
  }

  /**
   * @param {Hash} nameHash
   * @returns {Boolean}
   */

  hasName(nameHash) {
    return this.unique.has(nameHash);
  }

  /**
   * @param {Hash} nameHash
   * @returns {ContractState}
   */

  addName(nameHash) {
    this.unique.add(nameHash);
    return this;
  }

  /**
   * @param {Hash} nameHash
   * @returns {ContractState}
   */

  removeName(nameHash) {
    this.unique.delete(nameHash);
    return this;
  }

  /**
   * @param {TX} tx
   * @returns {Boolean}
   */

  hasNames(tx) {
    return rules.hasNames(tx, this.unique);
  }

  /**
   * @param {BufferMap<BufferSet>} map
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  addMap(map, nameHash, hash) {
    let set = map.get(nameHash);

    if (!set) {
      set = new BufferSet();
      map.set(nameHash, set);
    }

    set.add(hash);

    return this;
  }

  /**
   * @param {BufferMap<BufferSet>} map
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  removeMap(map, nameHash, hash) {
    const set = map.get(nameHash);

    if (!set)
      return this;

    set.delete(hash);

    if (set.size === 0)
      map.delete(nameHash);

    return this;
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  addOpen(nameHash, hash) {
    return this.addMap(this.opens, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  removeOpen(nameHash, hash) {
    return this.removeMap(this.opens, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  addBid(nameHash, hash) {
    return this.addMap(this.bids, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  removeBid(nameHash, hash) {
    return this.removeMap(this.bids, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  addReveal(nameHash, hash) {
    return this.addMap(this.reveals, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  removeReveal(nameHash, hash) {
    return this.removeMap(this.reveals, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  addUpdate(nameHash, hash) {
    return this.addMap(this.updates, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @param {Hash} hash
   * @returns {ContractState}
   */

  removeUpdate(nameHash, hash) {
    return this.removeMap(this.updates, nameHash, hash);
  }

  /**
   * @param {Hash} nameHash
   * @returns {ContractState}
   */

  reference(nameHash) {
    let count = this.refs.get(nameHash);

    if (count == null)
      count = 0;

    count += 1;

    this.refs.set(nameHash, count);

    return this;
  }

  /**
   * @param {Hash} nameHash
   * @returns {ContractState}
   */

  dereference(nameHash) {
    let count = this.refs.get(nameHash);

    if (count == null)
      return this;

    count -= 1;

    assert(count >= 0);

    if (count === 0) {
      this.refs.delete(nameHash);
      this.names.delete(nameHash);
      return this;
    }

    this.refs.set(nameHash, count);

    return this;
  }

  /**
   * @param {TX} tx
   * @param {CoinView} view
   * @returns {ContractState}
   */

  track(tx, view) {
    const hash = tx.hash();

    if (view.names.size === 0)
      return this;

    for (const {covenant} of tx.outputs) {
      if (!covenant.isName())
        continue;

      const nameHash = covenant.getHash(0);

      switch (covenant.type) {
        case types.OPEN:
          this.addOpen(nameHash, hash);
          break;
        case types.BID:
          this.addBid(nameHash, hash);
          break;
        case types.REVEAL:
        case types.CLAIM:
          this.addReveal(nameHash, hash);
          break;
        default:
          this.addUpdate(nameHash, hash);
          break;
      }
    }

    for (const [nameHash, ns] of view.names) {
      this.reference(nameHash);

      if (this.names.has(nameHash))
        continue;

      const state = ns.clone();

      // We want the on-chain state.
      if (ns.hasDelta()) {
        state.applyState(ns.delta);

        if (state.isNull())
          continue;
      }

      assert(!state.isNull());

      state.data = EMPTY;

      this.names.set(nameHash, state);
    }

    rules.addNames(tx, this.unique);

    return this;
  }

  /**
   * @param {TX} tx
   * @returns {ContractState}
   */

  untrack(tx) {
    const hash = tx.hash();
    const names = new BufferSet();

    for (const {covenant} of tx.outputs) {
      if (!covenant.isName())
        continue;

      const nameHash = covenant.getHash(0);

      switch (covenant.type) {
        case types.OPEN:
          this.removeOpen(nameHash, hash);
          break;
        case types.BID:
          this.removeBid(nameHash, hash);
          break;
        case types.REVEAL:
        case types.CLAIM:
          this.removeReveal(nameHash, hash);
          break;
        default:
          this.removeUpdate(nameHash, hash);
          break;
      }

      names.add(nameHash);
    }

    for (const nameHash of names)
      this.dereference(nameHash);

    rules.removeNames(tx, this.unique);

    return this;
  }

  /**
   * @param {CoinView} view
   * @returns {ContractState}
   */

  merge(view) {
    for (const [nameHash, ns] of view.names) {
      if (!this.refs.has(nameHash))
        continue;

      const state = ns.clone();
      state.data = EMPTY;

      this.names.set(nameHash, state);
    }

    return this;
  }

  /**
   * @param {BufferMap<BufferSet>} map
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  toSet(map, nameHash, items) {
    const hashes = map.get(nameHash);

    if (!hashes)
      return this;

    for (const hash of hashes)
      items.add(hash);

    return this;
  }

  /**
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  handleExpired(nameHash, items) {
    this.toSet(this.updates, nameHash, items);
    this.toSet(this.reveals, nameHash, items);
    return this;
  }

  /**
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  handleOpen(nameHash, items) {
    return this.toSet(this.updates, nameHash, items);
  }

  /**
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  handleBidding(nameHash, items) {
    return this.toSet(this.opens, nameHash, items);
  }

  /**
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  handleReveal(nameHash, items) {
    return this.toSet(this.bids, nameHash, items);
  }

  /**
   * @param {Hash} nameHash
   * @param {BufferSet} items
   * @returns {ContractState}
   */

  handleClosed(nameHash, items) {
    return this.toSet(this.reveals, nameHash, items);
  }

  /**
   * Invalidate transactions in the mempool.
   * @param {Number} height
   * @param {Boolean} hardened
   * @returns {BufferSet} - list of invalidated tx hashes.
   */

  invalidate(height, hardened) {
    const nextHeight = height + 1;
    const network = this.network;
    const invalid = new BufferSet();

    for (const [nameHash, ns] of this.names) {
      if (hardened && ns.weak) {
        this.handleExpired(nameHash, invalid);
        continue;
      }

      if (ns.isExpired(nextHeight, network)) {
        this.handleExpired(nameHash, invalid);
        continue;
      }

      const state = ns.state(nextHeight, network);

      switch (state) {
        case states.OPENING:
          this.handleOpen(nameHash, invalid);
          break;
        case states.BIDDING:
          this.handleBidding(nameHash, invalid);
          break;
        case states.REVEAL:
          this.handleReveal(nameHash, invalid);
          break;
        case states.CLOSED:
          this.handleClosed(nameHash, invalid);
          break;
      }
    }

    return invalid;
  }
}

/*
 * Expose
 */

module.exports = ContractState;