UNPKG

hsd

Version:
530 lines (409 loc) 10 kB
/*! * 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;