hsd
Version:
Cryptocurrency bike-shed
1,976 lines (1,574 loc) • 125 kB
JavaScript
/*!
* 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);