UNPKG

hsd

Version:
1,976 lines (1,574 loc) 125 kB
/*! * txdb.js - persistent transaction pool * 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 {BufferSet} = require('buffer-map'); const util = require('../utils/util'); const Amount = require('../ui/amount'); const CoinView = require('../coins/coinview'); const Coin = require('../primitives/coin'); const Outpoint = require('../primitives/outpoint'); const layouts = require('./layout'); const layout = layouts.txdb; const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const rules = require('../covenants/rules'); const NameState = require('../covenants/namestate'); const NameUndo = require('../covenants/undo'); const {TXRecord} = require('./records'); const {types} = rules; /** @typedef {import('bdb').DB} DB */ /** @typedef {ReturnType<DB['batch']>} Batch */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').BufioWriter} BufioWriter */ /** @typedef {import('../types').NetworkType} NetworkType */ /** @typedef {import('../types').Amount} AmountValue */ /** @typedef {import('../types').Rate} Rate */ /** @typedef {import('../protocol/network')} Network */ /** @typedef {import('../primitives/output')} Output */ /** @typedef {import('../primitives/tx')} TX */ /** @typedef {import('./records').BlockMeta} BlockMeta */ /** @typedef {import('./walletdb')} WalletDB */ /** @typedef {import('./wallet')} Wallet */ /** @typedef {import('./path')} Path */ /** * @typedef {Object} BlockExtraInfo * @property {Number} medianTime * @property {Number} txIndex */ /* * Constants */ const EMPTY = Buffer.alloc(0); const UNCONFIRMED_HEIGHT = 0xffffffff; /** * TXDB * @alias module:wallet.TXDB */ class TXDB { /** * Create a TXDB. * @constructor * @param {WalletDB} wdb * @param {Number} [wid=0] */ constructor(wdb, wid) { /** @type {WalletDB} */ this.wdb = wdb; this.db = wdb.db; this.logger = wdb.logger; this.nowFn = wdb.options.nowFn || util.now; this.maxTXs = wdb.options.maxHistoryTXs || 100; this.wid = wid || 0; this.bucket = null; this.wallet = null; this.locked = new BufferSet(); } /** * Open TXDB. * @param {Wallet} wallet * @returns {Promise} */ async open(wallet) { const prefix = layout.prefix.encode(wallet.wid); this.wid = wallet.wid; this.bucket = this.db.bucket(prefix); this.wallet = wallet; } /** * Emit transaction event. * @private * @param {String} event * @param {Object} data * @param {Details} [details] */ emit(event, data, details) { this.wdb.emit(event, this.wallet, data, details); this.wallet.emit(event, data, details); } /** * Get wallet path for output. * @param {Output} output * @returns {Promise<Path?>} */ getPath(output) { const hash = output.getHash(); if (!hash) return null; return this.wdb.getPath(this.wid, hash); } /** * Test whether path exists for output. * @param {Output} output * @returns {Promise<Boolean>} */ async hasPath(output) { const hash = output.getHash(); if (!hash) return false; return this.wdb.hasPath(this.wid, hash); } /** * Save credit. * @param {Batch} b * @param {Credit} credit * @param {Path} path */ async saveCredit(b, credit, path) { const {coin} = credit; b.put(layout.c.encode(coin.hash, coin.index), credit.encode()); b.put(layout.C.encode(path.account, coin.hash, coin.index), null); return this.addOutpointMap(b, coin.hash, coin.index); } /** * Remove credit. * @param {Batch} b * @param {Credit} credit * @param {Path} path */ async removeCredit(b, credit, path) { const {coin} = credit; b.del(layout.c.encode(coin.hash, coin.index)); b.del(layout.C.encode(path.account, coin.hash, coin.index)); return this.removeOutpointMap(b, coin.hash, coin.index); } /** * Spend credit. * @param {Batch} b * @param {Credit} credit * @param {TX} tx * @param {Number} index */ spendCredit(b, credit, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); b.put(layout.s.encode(prevout.hash, prevout.index), spender.encode()); b.put(layout.d.encode(spender.hash, spender.index), credit.coin.encode()); } /** * Unspend credit. * @param {Batch} b * @param {TX} tx * @param {Number} index */ unspendCredit(b, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); b.del(layout.s.encode(prevout.hash, prevout.index)); b.del(layout.d.encode(spender.hash, spender.index)); } /** * Coin selection index credit. * @param {Batch} b * @param {Credit} credit * @param {Path} path * @param {Number?} oldHeight */ indexCSCredit(b, credit, path, oldHeight) { const {coin} = credit; if (coin.isUnspendable() || coin.covenant.isNonspendable()) return; // value index if (coin.height === -1) { // index unconfirmed coin by value b.put(layout.Su.encode(coin.value, coin.hash, coin.index), null); // index unconfirmed coin by account + value. b.put(layout.SU.encode( path.account, coin.value, coin.hash, coin.index), null); } else { // index confirmed coin by value b.put(layout.Sv.encode(coin.value, coin.hash, coin.index), null); // index confirmed coin by account + value. b.put(layout.SV.encode( path.account, coin.value, coin.hash, coin.index), null); } // cleanup old value indexes. if (oldHeight && oldHeight === -1) { // remove unconfirmed indexes, now that it's confirmed. b.del(layout.Su.encode(coin.value, coin.hash, coin.index)); b.del(layout.SU.encode(path.account, coin.value, coin.hash, coin.index)); } else if (oldHeight && oldHeight !== -1) { // remove confirmed indexes, now that it's unconfirmed. b.del(layout.Sv.encode(coin.value, coin.hash, coin.index)); b.del(layout.SV.encode(path.account, coin.value, coin.hash, coin.index)); } // handle height indexes // index coin by account + height const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height; b.put(layout.Sh.encode(height, coin.hash, coin.index), null); b.put(layout.SH.encode(path.account, height, coin.hash, coin.index), null); if (oldHeight != null) { const height = oldHeight === -1 ? UNCONFIRMED_HEIGHT : oldHeight; b.del(layout.Sh.encode(height, coin.hash, coin.index)); b.del(layout.SH.encode(path.account, height, coin.hash, coin.index)); } } /** * Unindex Credit. * @param {Batch} b * @param {Credit} credit * @param {Path} path */ unindexCSCredit(b, credit, path) { const {coin} = credit; // Remove coin by account + value. if (coin.height === -1) { b.del(layout.Su.encode(coin.value, coin.hash, coin.index)); b.del(layout.SU.encode(path.account, coin.value, coin.hash, coin.index)); } else { b.del(layout.Sv.encode(coin.value, coin.hash, coin.index)); b.del(layout.SV.encode(path.account, coin.value, coin.hash, coin.index)); } // Remove coin by account + height const height = coin.height === -1 ? UNCONFIRMED_HEIGHT : coin.height; b.del(layout.Sh.encode(height, coin.hash, coin.index)); b.del(layout.SH.encode(path.account, height, coin.hash, coin.index)); } /** * Spend credit by spender/input record. * Add undo coin to the input record. * @param {Batch} b * @param {Credit} credit * @param {Outpoint} spender */ addUndoToInput(b, credit, spender) { b.put(layout.d.encode(spender.hash, spender.index), credit.coin.encode()); } /** * Write input record. * @param {Batch} b * @param {TX} tx * @param {Number} index */ async writeInput(b, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); b.put(layout.s.encode(prevout.hash, prevout.index), spender.encode()); return this.addOutpointMap(b, prevout.hash, prevout.index); } /** * Remove input record. * @param {Batch} b * @param {TX} tx * @param {Number} index */ async removeInput(b, tx, index) { const prevout = tx.inputs[index].prevout; b.del(layout.s.encode(prevout.hash, prevout.index)); return this.removeOutpointMap(b, prevout.hash, prevout.index); } /** * Update wallet balance. * @param {Batch} b * @param {BalanceDelta} state */ async updateBalance(b, state) { const balance = await this.getWalletBalance(); state.applyTo(balance); b.put(layout.R.encode(), balance.encode()); return balance; } /** * Update account balance. * @param {Batch} b * @param {Number} acct * @param {Balance} delta - account balance * @returns {Promise<Balance>} */ async updateAccountBalance(b, acct, delta) { const balance = await this.getAccountBalance(acct); delta.applyTo(balance); b.put(layout.r.encode(acct), balance.encode()); return balance; } /** * Test a whether a coin has been spent. * @param {Hash} hash * @param {Number} index * @returns {Promise<Outpoint?>} */ async getSpent(hash, index) { const data = await this.bucket.get(layout.s.encode(hash, index)); if (!data) return null; return Outpoint.decode(data); } /** * Test a whether a coin has been spent. * @param {Hash} hash * @param {Number} index * @returns {Promise<Boolean>} */ isSpent(hash, index) { return this.bucket.has(layout.s.encode(hash, index)); } /** * Append to global map. * @param {Batch} b * @param {Number} height * @returns {Promise} */ addBlockMap(b, height) { return this.wdb.addBlockMap(b.root(), height, this.wid); } /** * Remove from global map. * @param {Batch} b * @param {Number} height * @returns {Promise} */ removeBlockMap(b, height) { return this.wdb.removeBlockMap(b.root(), height, this.wid); } /** * Append to global map. * @param {Batch} b * @param {Hash} hash * @returns {Promise} */ addTXMap(b, hash) { return this.wdb.addTXMap(b.root(), hash, this.wid); } /** * Remove from global map. * @param {Batch} b * @param {Hash} hash * @returns {Promise} */ removeTXMap(b, hash) { return this.wdb.removeTXMap(b.root(), hash, this.wid); } /** * Append to global map. * @param {Batch} b * @param {Hash} hash * @param {Number} index * @returns {Promise} */ addOutpointMap(b, hash, index) { return this.wdb.addOutpointMap(b.root(), hash, index, this.wid); } /** * Remove from global map. * @param {Batch} b * @param {Hash} hash * @param {Number} index * @returns {Promise} */ removeOutpointMap(b, hash, index) { return this.wdb.removeOutpointMap(b.root(), hash, index, this.wid); } /** * Append to global map. * @param {Batch} b * @param {Hash} nameHash * @returns {Promise} */ addNameMap(b, nameHash) { return this.wdb.addNameMap(b.root(), nameHash, this.wid); } /** * Remove from global map. * @param {Batch} b * @param {Hash} nameHash * @returns {Promise} */ removeNameMap(b, nameHash) { return this.wdb.removeNameMap(b.root(), nameHash, this.wid); } /** * List block records. * @returns {Promise<BlockRecord[]>} */ getBlocks() { return this.bucket.keys({ gte: layout.b.min(), lte: layout.b.max(), parse: key => layout.b.decode(key)[0] }); } /** * Get block record. * @param {Number} height * @returns {Promise<BlockRecord?>} */ async getBlock(height) { const data = await this.bucket.get(layout.b.encode(height)); if (!data) return null; return BlockRecord.decode(data); } /** * Get block hashes size. * @param {Number} height * @returns {Promise<Number>} */ async getBlockTXsSize(height) { const data = await this.bucket.get(layout.b.encode(height)); if (!data) return 0; return data.readUInt32LE(40, true); } /** * Append to the global block record. * @param {Batch} b * @param {Hash} hash - transaction hash. * @param {BlockMeta} block * @returns {Promise} */ async addBlock(b, hash, block) { const key = layout.b.encode(block.height); const data = await this.bucket.get(key); if (!data) { const blk = BlockRecord.fromMeta(block); blk.add(hash); b.put(key, blk.encode()); return; } const raw = Buffer.allocUnsafe(data.length + 32); data.copy(raw, 0); const size = raw.readUInt32LE(40); raw.writeUInt32LE(size + 1, 40); hash.copy(raw, data.length); b.put(key, raw); } /** * Remove from the global block record. * @param {Batch} b * @param {Hash} hash * @param {Number} height * @returns {Promise} */ async removeBlock(b, hash, height) { const key = layout.b.encode(height); const data = await this.bucket.get(key); if (!data) return; const size = data.readUInt32LE(40, true); assert(size > 0); assert(data.slice(-32).equals(hash)); if (size === 1) { b.del(key); return; } const raw = data.slice(0, -32); raw.writeUInt32LE(size - 1, 40, true); b.put(key, raw); } /** * Remove from the global block record. * @param {Batch} b * @param {Hash} hash * @param {Number} height * @returns {Promise} */ async spliceBlock(b, hash, height) { const block = await this.getBlock(height); if (!block) return; if (!block.remove(hash)) return; if (block.hashes.size === 0) { b.del(layout.b.encode(height)); return; } b.put(layout.b.encode(height), block.encode()); } /** * Test whether we have a name. * @param {Buffer} nameHash * @returns {Promise<Boolean>} */ async hasNameState(nameHash) { return this.bucket.has(layout.A.encode(nameHash)); } /** * Get a name state if present. * @param {Buffer} nameHash * @returns {Promise<NameState?>} */ async getNameState(nameHash) { const raw = await this.bucket.get(layout.A.encode(nameHash)); if (!raw) return null; const ns = NameState.decode(raw); ns.nameHash = nameHash; return ns; } /** * Get all names. * @returns {Promise<NameState[]>} */ async getNames() { const iter = this.bucket.iterator({ gte: layout.A.min(), lte: layout.A.max(), values: true }); const names = []; await iter.each((key, raw) => { const [nameHash] = layout.A.decode(key); const ns = NameState.decode(raw); ns.nameHash = nameHash; names.push(ns); }); return names; } /** * Test whether we have a bid. * @param {Buffer} nameHash * @param {Outpoint} outpoint * @returns {Promise<Boolean>} */ async hasBid(nameHash, outpoint) { const {hash, index} = outpoint; return this.bucket.has(layout.i.encode(nameHash, hash, index)); } /** * Get a bid if present. * @param {Buffer} nameHash * @param {Outpoint} outpoint * @returns {Promise<BlindBid?>} */ async getBid(nameHash, outpoint) { const {hash, index} = outpoint; const raw = await this.bucket.get(layout.i.encode(nameHash, hash, index)); if (!raw) return null; const bb = BlindBid.decode(raw); bb.nameHash = nameHash; bb.prevout = outpoint; return bb; } /** * Write a bid. * @param {Object} b * @param {Buffer} nameHash * @param {Outpoint} outpoint * @param {Object} options */ putBid(b, nameHash, outpoint, options) { const {hash, index} = outpoint; const bb = new BlindBid(); bb.nameHash = nameHash; bb.name = options.name; bb.lockup = options.lockup; bb.blind = options.blind; bb.own = options.own; bb.height = options.height; b.put(layout.i.encode(nameHash, hash, index), bb.encode()); } /** * Delete a bid. * @param {Object} b * @param {Buffer} nameHash * @param {Outpoint} outpoint */ removeBid(b, nameHash, outpoint) { const {hash, index} = outpoint; b.del(layout.i.encode(nameHash, hash, index)); } /** * Get all bids for name. * @param {Buffer} [nameHash] * @returns {Promise<BlindBid[]>} */ async getBids(nameHash) { const iter = this.bucket.iterator({ gte: nameHash ? layout.i.min(nameHash) : layout.i.min(), lte: nameHash ? layout.i.max(nameHash) : layout.i.max(), values: true }); const bids = []; await iter.each(async (key, raw) => { const [nameHash, hash, index] = layout.i.decode(key); const bb = BlindBid.decode(raw); bb.nameHash = nameHash; bb.prevout = new Outpoint(hash, index); const bv = await this.getBlind(bb.blind); if (bv) bb.value = bv.value; bids.push(bb); }); return bids; } /** * Remove all bids for name. * @param {Batch} b * @param {Buffer} nameHash * @returns {Promise} */ async removeBids(b, nameHash) { const iter = this.bucket.iterator({ gte: layout.i.min(nameHash), lte: layout.i.max(nameHash) }); await iter.each(k => b.del(k)); } /** * Test whether we have a reveal. * @param {Buffer} nameHash * @param {Outpoint} outpoint * @returns {Promise<Boolean>} */ async hasReveal(nameHash, outpoint) { const {hash, index} = outpoint; return this.bucket.has(layout.B.encode(nameHash, hash, index)); } /** * Get a reveal if present. * @param {Buffer} nameHash * @param {Outpoint} outpoint * @returns {Promise<BidReveal?>} */ async getReveal(nameHash, outpoint) { const {hash, index} = outpoint; const raw = await this.bucket.get(layout.B.encode(nameHash, hash, index)); if (!raw) return null; const brv = BidReveal.decode(raw); brv.nameHash = nameHash; brv.prevout = outpoint; return brv; } /** * Get reveal by bid outpoint. * @param {Buffer} nameHash * @param {Outpoint} bidOut * @returns {Promise<BidReveal?>} */ async getRevealByBid(nameHash, bidOut) { const rawOutpoint = await this.bucket.get( layout.E.encode(nameHash, bidOut.hash, bidOut.index)); if (!rawOutpoint) return null; const outpoint = Outpoint.decode(rawOutpoint); return this.getReveal(nameHash, outpoint); } /** * Get bid by reveal outpoint. * @param {Buffer} nameHash * @param {Outpoint} revealOut * @returns {Promise<BlindBid?>} */ async getBidByReveal(nameHash, revealOut) { const reveal = await this.getReveal(nameHash, revealOut); if (!reveal) return null; return this.getBid(nameHash, reveal.bidPrevout); } /** * Write a reveal. * @param {Object} b * @param {Buffer} nameHash * @param {Outpoint} outpoint * @param {Object} options * @param {Buffer} options.name * @param {AmountValue} options.value * @param {Number} options.height * @param {Boolean} options.own * @param {Outpoint} options.bidPrevout * @returns {void} */ putReveal(b, nameHash, outpoint, options) { const {hash, index} = outpoint; const {bidPrevout} = options; const brv = new BidReveal(); brv.nameHash = nameHash; brv.name = options.name; brv.value = options.value; brv.height = options.height; brv.own = options.own; brv.bidPrevout = bidPrevout; b.put(layout.B.encode(nameHash, hash, index), brv.encode()); b.put(layout.E.encode(nameHash, bidPrevout.hash, bidPrevout.index), outpoint.encode()); } /** * Delete a reveal. * @param {Object} b * @param {Buffer} nameHash * @param {Outpoint} outpoint * @param {Outpoint} bidPrevout */ removeReveal(b, nameHash, outpoint, bidPrevout) { const {hash, index} = outpoint; b.del(layout.B.encode(nameHash, hash, index)); b.del(layout.E.encode(nameHash, bidPrevout.hash, bidPrevout.index)); } /** * Get all reveals by name. * @param {Buffer} [nameHash] * @returns {Promise<BidReveal[]>} */ async getReveals(nameHash) { const iter = this.bucket.iterator({ gte: nameHash ? layout.B.min(nameHash) : layout.B.min(), lte: nameHash ? layout.B.max(nameHash) : layout.B.max(), values: true }); const reveals = []; await iter.each(async (key, raw) => { const [nameHash, hash, index] = layout.B.decode(key); const brv = BidReveal.decode(raw); brv.nameHash = nameHash; brv.prevout = new Outpoint(hash, index); reveals.push(brv); }); return reveals; } /** * Remove all reveals by name. * @param {Object} b * @param {Buffer} nameHash * @returns {Promise} */ async removeReveals(b, nameHash) { const iter = this.bucket.iterator({ gte: layout.B.min(nameHash), lte: layout.B.max(nameHash) }); await iter.each(k => b.del(k)); } /** * Test whether a blind value is present. * @param {Buffer} blind - Blind hash. * @returns {Promise<Boolean>} */ async hasBlind(blind) { return this.bucket.has(layout.v.encode(blind)); } /** * Get a blind value if present. * @param {Buffer} blind - Blind hash. * @returns {Promise<BlindValue?>} */ async getBlind(blind) { const raw = await this.bucket.get(layout.v.encode(blind)); if (!raw) return null; return BlindValue.decode(raw); } /** * Write a blind value. * @param {Object} b * @param {Buffer} blind * @param {Object} options */ putBlind(b, blind, options) { const {value, nonce} = options; const bv = new BlindValue(); bv.value = value; bv.nonce = nonce; b.put(layout.v.encode(blind), bv.encode()); } /** * Save blind value. * @param {Buffer} blind * @param {Object} options * @returns {Promise} */ async saveBlind(blind, options) { const b = this.bucket.batch(); this.putBlind(b, blind, options); await b.write(); } /** * Delete a blind value. * @param {Object} b * @param {Buffer} blind */ removeBlind(b, blind) { b.del(layout.v.encode(blind)); } /** * Add transaction without a batch. * @param {TX} tx * @param {BlockMeta} [block] * @param {BlockExtraInfo} [extra] * @returns {Promise<Details?>} */ async add(tx, block, extra) { const hash = tx.hash(); const existing = await this.getTX(hash); assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); if (existing) { // Existing tx is already confirmed. Ignore. if (existing.height !== -1) return null; // The incoming tx won't confirm the // existing one anyway. Ignore. if (!block) return null; // Get txIndex of the wallet. extra.txIndex = await this.getBlockTXsSize(block.height); // Confirm transaction. return this.confirm(existing, block, extra); } const now = this.nowFn(); const wtx = TXRecord.fromTX(tx, block, now); if (!block) { // Potentially remove double-spenders. // Only remove if they're not confirmed. if (!await this.removeConflicts(tx, true)) return null; if (await this.isDoubleOpen(tx)) return null; } else { // Potentially remove double-spenders. await this.removeConflicts(tx, false); await this.removeDoubleOpen(tx); } if (block) extra.txIndex = await this.getBlockTXsSize(block.height); // Finally we can do a regular insertion. return this.insert(wtx, block, extra); } /** * Test whether the transaction * has a duplicate open. * @param {TX} tx * @returns {Promise<Boolean>} */ async isDoubleOpen(tx) { for (const {covenant} of tx.outputs) { if (!covenant.isOpen()) continue; const nameHash = covenant.getHash(0); const key = layout.o.encode(nameHash); const hash = await this.bucket.get(key); // Allow a double open if previous auction period has expired // this is not a complete check for name availability or status! if (hash) { const names = this.wdb.network.names; const period = names.biddingPeriod + names.revealPeriod; const oldTX = await this.getTX(hash); if (oldTX.height !== -1 && oldTX.height + period < this.wdb.height) return false; return true; } } return false; } /** * Remove duplicate opens. * @private * @param {TX} tx * @returns {Promise} */ async removeDoubleOpen(tx) { for (const {covenant} of tx.outputs) { if (!covenant.isOpen()) continue; const nameHash = covenant.getHash(0); const key = layout.o.encode(nameHash); const hash = await this.bucket.get(key); if (!hash) continue; const wtx = await this.getTX(hash); if (wtx.height !== -1) return; await this.remove(hash); } } /** * Index open covenants. * @private * @param {Batch} b * @param {TX} tx */ indexOpens(b, tx) { for (const {covenant} of tx.outputs) { if (!covenant.isOpen()) continue; const nameHash = covenant.getHash(0); const key = layout.o.encode(nameHash); b.put(key, tx.hash()); } } /** * Unindex open covenants. * @private * @param {Batch} b * @param {TX} tx */ unindexOpens(b, tx) { for (const {covenant} of tx.outputs) { if (!covenant.isOpen()) continue; const nameHash = covenant.getHash(0); const key = layout.o.encode(nameHash); b.del(key); } } /** * Insert transaction. * @private * @param {TXRecord} wtx * @param {BlockMeta} [block] * @param {BlockExtraInfo} [extra] * @returns {Promise<Details>} */ async insert(wtx, block, extra) { const b = this.bucket.batch(); const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(wtx, block); const state = new BalanceDelta(); const view = new CoinView(); let own = false; if (!tx.isCoinbase()) { // We need to potentially spend some coins here. for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; const {hash, index} = input.prevout; const credit = await this.getCredit(hash, index); if (!credit) { // Watch all inputs for incoming txs. // This allows us to check for double spends. if (!block) await this.writeInput(b, tx, i); continue; } const coin = credit.coin; const path = await this.getPath(coin); assert(path); // Build the tx details object // as we go, for speed. details.setInput(i, path, coin); // Write an undo coin for the credit // and add it to the stxo set. this.spendCredit(b, credit, tx, i); // Unconfirmed balance should always // be updated as it reflects the on-chain // balance _and_ mempool balance assuming // everything in the mempool were to confirm. state.tx(path, 1); state.coin(path, -1); state.unconfirmed(path, -coin.value); this.unlockBalances(state, credit, path, -1); if (!block) { // If the tx is not mined, we do not // disconnect the coin, we simply mark // a `spent` flag on the credit. This // effectively prevents the mempool // from altering our utxo state // permanently. It also makes it // possible to compare the on-chain // state vs. the mempool state. credit.spent = true; await this.saveCredit(b, credit, path); } else { // If the tx is mined, we can safely // remove the coin being spent. This // coin will be indexed as an undo // coin so it can be reconnected // later during a reorg. state.confirmed(path, -coin.value); this.unlockBalances(state, credit, path, height); await this.removeCredit(b, credit, path); this.unindexCSCredit(b, credit, path); view.addCoin(coin); } own = true; } } // Potentially add coins to the utxo set. for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const path = await this.getPath(output); if (!path) continue; details.setOutput(i, path); const credit = Credit.fromTX(tx, i, height); credit.own = own; state.tx(path, 1); state.coin(path, 1); state.unconfirmed(path, output.value); this.lockBalances(state, credit, path, -1); if (block) { state.confirmed(path, output.value); this.lockBalances(state, credit, path, height); } const spender = await this.getSpent(hash, i); if (spender) { credit.spent = true; this.addUndoToInput(b, credit, spender); // TODO: emit 'missed credit' state.coin(path, -1); state.unconfirmed(path, -output.value); this.unlockBalances(state, credit, path, -1); } await this.saveCredit(b, credit, path); this.indexCSCredit(b, credit, path, null); await this.watchOpensEarly(b, output); } // Handle names. if (block && !await this.bucket.has(layout.U.encode(hash))) { const updated = await this.connectNames(b, tx, view, height); if (updated && !state.updated()) { // Always save namestate transitions, // even if they don't affect wallet balance await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); await b.write(); } } // If this didn't update any coins, // it's not our transaction. if (!state.updated()) return null; // Index open covenants. if (!block) this.indexOpens(b, tx); // Save and index the transaction record. b.put(layout.t.encode(hash), wtx.encode()); if (!block) b.put(layout.p.encode(hash), null); else b.put(layout.h.encode(height, hash), null); // Do some secondary indexing for account-based // queries. This saves us a lot of time for // queries later. for (const [acct, delta] of state.accounts) { await this.updateAccountBalance(b, acct, delta); b.put(layout.T.encode(acct, hash), null); if (!block) b.put(layout.P.encode(acct, hash), null); else b.put(layout.H.encode(acct, height, hash), null); } // Update block records. if (block) { // If confirmed in a block (e.g. coinbase tx) and not // being updated or previously seen, we need to add // the monotonic time and count index for the transaction await this.addCountAndTimeIndex(b, { accounts: state.accounts, hash, height: block.height, blockextra: extra }); // In the event that this transaction becomes unconfirmed // during a reorganization, this transaction will need an // unconfirmed time and unconfirmed index, however since this // transaction was not previously seen previous to the block, // we need to add that information. // TODO: This can be skipped if TX is coinbase, but even now // it will be properly cleaned up on erase. await this.addCountAndTimeIndexUnconfirmedUndo(b, hash, wtx.mtime); await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); } else { // Add indexing for unconfirmed transactions. await this.addCountAndTimeIndexUnconfirmed(b, state.accounts, hash, wtx.mtime); await this.addTXMap(b, hash); } // Commit the new state. const balance = await this.updateBalance(b, state); await b.write(); // This transaction may unlock some // coins now that we've seen it. this.unlockTX(tx); // Emit events for potential local and // websocket listeners. Note that these // will only be emitted if the batch is // successfully written to disk. this.emit('tx', tx, details); this.emit('balance', balance); return details; } /** * Attempt to confirm a transaction. * @private * @param {TXRecord} wtx * @param {BlockMeta} block * @param {BlockExtraInfo} extra * @returns {Promise<Details>} */ async confirm(wtx, block, extra) { const b = this.bucket.batch(); const {tx, hash} = wtx; const height = block.height; const details = new Details(wtx, block); const state = new BalanceDelta(); const view = new CoinView(); let own = false; assert(block && extra); wtx.setBlock(block); if (!tx.isCoinbase()) { const credits = await this.getSpentCredits(tx); // Potentially spend coins. Now that the tx // is mined, we can actually _remove_ coins // from the utxo state. for (let i = 0; i < tx.inputs.length; i++) { const input = tx.inputs[i]; const {hash, index} = input.prevout; let resolved = false; // There may be new credits available // that we haven't seen yet. if (!credits[i]) { await this.removeInput(b, tx, i); // NOTE: This check has been moved to the outputs // processing in insert(pending), insert(block) and confirm. // But will still be here just in case. const credit = await this.getCredit(hash, index); if (!credit) continue; // Add a spend record and undo coin // for the coin we now know is ours. // We don't need to remove the coin // since it was never added in the // first place. this.spendCredit(b, credit, tx, i); credits[i] = credit; resolved = true; } const credit = credits[i]; const coin = credit.coin; assert(coin.height !== -1); const path = await this.getPath(coin); assert(path); own = true; details.setInput(i, path, coin); if (resolved) { state.coin(path, -1); state.unconfirmed(path, -coin.value); this.unlockBalances(state, credit, path, -1); } state.confirmed(path, -coin.value); this.unlockBalances(state, credit, path, height); // We can now safely remove the credit // entirely, now that we know it's also // been removed on-chain. await this.removeCredit(b, credit, path); this.unindexCSCredit(b, credit, path); view.addCoin(coin); } } // Update credit heights, including undo coins. for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const path = await this.getPath(output); if (!path) continue; details.setOutput(i, path); let credit = await this.getCredit(hash, i); if (!credit) { // TODO: Emit 'missed credit' event. // This credit didn't belong to us the first time we // saw the transaction (before confirmation or rescan). // Create new credit for database. credit = Credit.fromTX(tx, i, height); // If this tx spent any of our own coins, we "own" this output, // meaning if it becomes unconfirmed, we can still confidently spend it. credit.own = own; const spender = await this.getSpent(hash, i); if (spender) { credit.spent = true; this.addUndoToInput(b, credit, spender); } else { // Add coin to "unconfirmed" balance (which includes confirmed coins) state.coin(path, 1); state.unconfirmed(path, credit.coin.value); this.lockBalances(state, credit, path, -1); } } // Credits spent in the mempool add an // undo coin for ease. If this credit is // spent in the mempool, we need to // update the undo coin's height. if (credit.spent) await this.updateSpentCoin(b, tx, i, height); // Update coin height and confirmed // balance. Save once again. state.confirmed(path, output.value); this.lockBalances(state, credit, path, height); credit.coin.height = height; await this.saveCredit(b, credit, path); this.indexCSCredit(b, credit, path, -1); } // Handle names. await this.connectNames(b, tx, view, height); // Disconnect unconfirmed time index for the transaction. // This must go before adding the the indexes, as the // unconfirmed count needs to be copied first. await this.disconnectCountAndTimeIndexUnconfirmed(b, state.accounts, hash); // Add monotonic and count time index for transactions // that already exist in the database and are now // being confirmed. await this.addCountAndTimeIndex(b, { accounts: state.accounts, hash, height: block.height, blockextra: extra }); // Save the new serialized transaction as // the block-related properties have been // updated. Also reindex for height. b.put(layout.t.encode(hash), wtx.encode()); b.del(layout.p.encode(hash)); b.put(layout.h.encode(height, hash), null); // Secondary indexing also needs to change. for (const [acct, delta] of state.accounts) { await this.updateAccountBalance(b, acct, delta); b.del(layout.P.encode(acct, hash)); b.put(layout.H.encode(acct, height, hash), null); } await this.removeTXMap(b, hash); await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); // Commit the new state. The balance has updated. const balance = await this.updateBalance(b, state); this.unindexOpens(b, tx); await b.write(); this.unlockTX(tx); this.emit('confirmed', tx, details); this.emit('balance', balance); return details; } /** * Recursively remove a transaction * from the database. * @param {Hash} hash * @returns {Promise<Details?>} */ async remove(hash) { const wtx = await this.getTX(hash); if (!wtx) return null; return this.removeRecursive(wtx); } /** * Remove a transaction from the * database. Disconnect inputs. * @private * @param {TXRecord} wtx * @param {BlockMeta} [block] * @param {Number} [medianTime] * @returns {Promise<Details>} */ async erase(wtx, block, medianTime) { const b = this.bucket.batch(); const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(wtx, block); const state = new BalanceDelta(); if (!tx.isCoinbase()) { // We need to undo every part of the // state this transaction ever touched. // Start by getting the undo coins. const credits = await this.getSpentCredits(tx); for (let i = 0; i < tx.inputs.length; i++) { const credit = credits[i]; if (!credit) { if (!block) await this.removeInput(b, tx, i); continue; } const coin = credit.coin; const path = await this.getPath(coin); assert(path); details.setInput(i, path, coin); // Recalculate the balance, remove // from stxo set, remove the undo // coin, and resave the credit. state.tx(path, -1); state.coin(path, 1); state.unconfirmed(path, coin.value); this.lockBalances(state, credit, path, -1); if (block) { state.confirmed(path, coin.value); this.lockBalances(state, credit, path, height); } this.unspendCredit(b, tx, i); credit.spent = false; await this.saveCredit(b, credit, path); this.indexCSCredit(b, credit, path, null); } } // We need to remove all credits // this transaction created. for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const path = await this.getPath(output); if (!path) continue; const credit = await this.getCredit(hash, i); // If we don't have credit for the output, then we don't need // to do anything, because they were getting erased anyway. if (!credit) continue; details.setOutput(i, path); state.tx(path, -1); state.coin(path, -1); state.unconfirmed(path, -output.value); this.unlockBalances(state, credit, path, -1); if (block) { state.confirmed(path, -output.value); this.unlockBalances(state, credit, path, height); } await this.removeCredit(b, credit, path); this.unindexCSCredit(b, credit, path); } // Undo name state. await this.undoNameState(b, tx); if (!block) this.unindexOpens(b, tx); // Remove the transaction data // itself as well as unindex. b.del(layout.t.encode(hash)); if (!block) b.del(layout.p.encode(hash)); else b.del(layout.h.encode(height, hash)); // Remove all secondary indexing. for (const [acct, delta] of state.accounts) { await this.updateAccountBalance(b, acct, delta); b.del(layout.T.encode(acct, hash)); if (!block) b.del(layout.P.encode(acct, hash)); else b.del(layout.H.encode(acct, height, hash)); } // Update block records. if (block) { // Remove tx count and time indexing. await this.removeCountAndTimeIndex(b, { hash, medianTime, accounts: state.accounts }); // We also need to clean up unconfirmed undos. b.del(layout.Oe.encode(hash)); await this.removeBlockMap(b, height); await this.spliceBlock(b, hash, height); } else { await this.removeTXMap(b, hash); // Remove count and time indexes. await this.removeCountAndTimeIndexUnconfirmed(b, state.accounts, hash); } // Update the transaction counter // and commit new state due to // balance change. const balance = await this.updateBalance(b, state); await b.write(); this.emit('remove tx', tx, details); this.emit('balance', balance); return details; } /** * Remove a transaction and recursively * remove all of its spenders. * @private * @param {TXRecord} wtx * @returns {Promise<Details?>} */ async removeRecursive(wtx) { const {tx, hash} = wtx; if (!await this.hasTX(hash)) return null; for (let i = 0; i < tx.outputs.length; i++) { const spent = await this.getSpent(hash, i); if (!spent) continue; // Remove all of the spender's spenders first. const stx = await this.getTX(spent.hash); assert(stx); await this.removeRecursive(stx); } const block = wtx.getBlock(); let medianTime; if (block) medianTime = await this.wdb.getMedianTime(block.height); // Remove the spender. return this.erase(wtx, block, medianTime); } /** * Revert a block. * @param {Number} height * @returns {Promise<Number>} - number of txs removed. */ async revert(height) { const block = await this.getBlock(height); if (!block) return 0; const hashes = block.toArray(); const mtp = await this.wdb.getMedianTime(height); assert(mtp); for (let i = hashes.length - 1; i >= 0; i--) { const hash = hashes[i]; /** @type {BlockExtraInfo} */ const extra = { medianTime: mtp, // txIndex is not used in revert. txIndex: i }; await this.unconfirm(hash, height, extra); } return hashes.length; } /** * Unconfirm a transaction without a batch. * @private * @param {Hash} hash * @param {Number} height * @param {BlockExtraInfo} extra * @returns {Promise<Details?>} */ async unconfirm(hash, height, extra) { const wtx = await this.getTX(hash); if (!wtx) { this.logger.spam( 'Reverting namestate without transaction: %x', hash ); const b = this.bucket.batch(); if (await this.applyNameUndo(b, hash)) { await this.removeBlockMap(b, height); await this.removeBlock(b, hash, height); return b.write(); } return null; } if (wtx.height === -1) return null; const tx = wtx.tx; if (tx.isCoinbase()) return this.removeRecursive(wtx); // On unconfirm, if we already have OPEN txs in the pending list we // remove transaction and it's descendants instead of storing them in // the pending list. This follows the mempool behaviour where the first // entries in the mempool will be the ones left, instead of txs coming // from the block. This ensures consistency with the double open rules. if (await this.isDoubleOpen(tx)) return this.removeRecursive(wtx); return this.disconnect(wtx, wtx.getBlock(), extra); } /** * Unconfirm a transaction. Necessary after a reorg. * @param {TXRecord} wtx * @param {BlockMeta} block * @param {BlockExtraInfo} extra * @returns {Promise<Details>} */ async disconnect(wtx, block, extra) { const b = this.bucket.batch(); const {tx, hash, height} = wtx; const details = new Details(wtx, block); const state = new BalanceDelta(); let own = false; assert(block && extra); wtx.unsetBlock(); if (!tx.isCoinbase()) { // We need to reconnect the coins. Start // by getting all of the undo coins we know // about. const credits = await this.getSpentCredits(tx); for (let i = 0; i < tx.inputs.length; i++) { const credit = credits[i]; if (!credit) { await this.writeInput(b, tx, i); continue; } const coin = credit.coin; assert(coin.height !== -1); const path = await this.getPath(coin); assert(path); details.setInput(i, path, coin); state.confirmed(path, coin.value); this.lockBalances(state, credit, path, height); // Resave the credit and mark it // as spent in the mempool instead. credit.spent = true; own = true; await this.saveCredit(b, credit, path); this.indexCSCredit(b, credit, path, null); } } // We need to remove heights on // the credits and undo coins. for (let i = 0; i < tx.outputs.length; i++) { const output = tx.outputs[i]; const path = await this.getPath(output); if (!path) continue; let credit = await this.getCredit(hash, i); let resolved = false; // Potentially update undo coin height. if (!credit) { // TODO: Emit 'missed credit' event. // This credit didn't belong to us the first time we // saw the transaction (after confirmation). // Create new credit for database. credit = Credit.fromTX(tx, i, height); // If this tx spent any of our own coins, we "own" this output, // meaning if it becomes unconfirmed, we can still confidently spend it. credit.own = own; resolved = true; const spender = await this.getSpent(hash, i); if (spender) { credit.spent = true; this.addUndoToInput(b, credit, spender); } else { // If the newly discovered Coin is not spent, // we need to add these to the balance. state.coin(path, 1); state.unconfirmed(path, credit.coin.value); this.lockBalances(state, credit, path, -1); } } else if (credit.spent) { // The coin height of this output becomes -1 // as it is being unconfirmed. await this.updateSpentCoin(b, tx, i, -1); } details.setOutput(i, path); // Update coin height and confirmed // balance. Save once again. const oldHeight = credit.coin.height; credit.coin.height = -1; // If the coin was not discovered now, it means // we need to subtract the values as they were part of // the balance. // If the credit is new, confirmed balances did not account for it. if (!resolved) { state.confirmed(path, -output.value); this.unlockBalances(state, credit, path, height); } await this.saveCredit(b, credit, path); this.indexCSCredit(b, credit, path, oldHeight); } // Unconfirm will also index OPENs as the transaction is now part of the // wallet pending transactions. this.indexOpens(b, tx); // Undo name state. await this.undoNameState(b, tx); await this.addTXMap(b, hash); await this.removeBlockMap(b, height); await this.removeBlock(b, tx.hash(), height); // We need to update the now-removed // block properties and reindex due // to the height change. b.put(layout.t.encode(hash), wtx.encode()); b.put(layout.p.encode(hash), null); b.del(layout.h.encode(height, hash)); // Secondary indexing also needs to change. for (const [acct, delta] of state.accounts) { await this.updateAccountBalance(b, acct, delta); b.put(layout.P.encode(acct, hash), null); b.del(layout.H.encode(acct, height, hash)); } // Remove tx count and time indexing. This must // go before restoring the unconfirmed count. await this.removeCountAndTimeIndex(b, { hash, medianTime: extra.medianTime, accounts: state.accounts }); // Restore count indexing for unconfirmed txs. await this.restoreCountAndTimeIndexUnconfirmed(b, state.accounts, hash);