/*! * txindexer.js - transaction indexer for bcoin * Copyright (c) 2018, the bcoin developers (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('bsert'); const bdb = require('bdb'); const bio = require('bufio'); const layout = require('./layout'); const consensus = require('../protocol/consensus'); const TX = require('../primitives/tx'); const TXMeta = require('../primitives/txmeta'); const Indexer = require('./indexer'); /* * TXIndexer Database Layout: * t[hash] -> tx record * b[height] -> block record * * The transaction index maps a transaction to a block * and an index, offset, and length within that block. The * block hash is stored in a separate record by height so that * the 32 byte hash is not repeated for every transaction * within a block. */ Object.assign(layout, { t: bdb.key('t', ['hash256']), b: bdb.key('b', ['uint32']) }); /** * Block Record */ class BlockRecord { /** * Create a block record. * @constructor */ constructor(options = {}) { this.block = options.block || consensus.ZERO_HASH; this.time = options.time || 0; assert(this.block.length === 32); assert((this.time >>> 0) === this.time); } /** * Inject properties from serialized data. * @private * @param {Buffer} data */ fromRaw(data) { const br = bio.read(data); this.block = br.readHash(); this.time = br.readU32(); return this; } /** * Instantiate block record from serialized data. * @param {Hash} hash * @param {Buffer} data * @returns {BlockRecord} */ static fromRaw(data) { return new this().fromRaw(data); } /** * Serialize the block record. * @returns {Buffer} */ toRaw() { const bw = bio.write(36); bw.writeHash(this.block); bw.writeU32(this.time); return bw.render(); } } /** * Transaction Record */ class TxRecord { /** * Create a transaction record. * @constructor */ constructor(options = {}) { this.height = options.height || 0; this.index = options.index || 0; this.offset = options.offset || 0; this.length = options.length || 0; assert((this.height >>> 0) === this.height); assert((this.index >>> 0) === this.index); assert((this.offset >>> 0) === this.offset); assert((this.length >>> 0) === this.length); } /** * Inject properties from serialized data. * @private * @param {Buffer} data */ fromRaw(data) { const br = bio.read(data); this.height = br.readU32(); this.index = br.readU32(); this.offset = br.readU32(); this.length = br.readU32(); return this; } /** * Instantiate transaction record from serialized data. * @param {Hash} hash * @param {Buffer} data * @returns {BlockRecord} */ static fromRaw(data) { return new this().fromRaw(data); } /** * Serialize the transaction record. * @returns {Buffer} */ toRaw() { const bw = bio.write(16); bw.writeU32(this.height); bw.writeU32(this.index); bw.writeU32(this.offset); bw.writeU32(this.length); return bw.render(); } } /** * TXIndexer * @alias module:indexer.TXIndexer * @extends Indexer */ class TXIndexer extends Indexer { /** * Create a indexer * @constructor * @param {Object} options */ constructor(options) { super('tx', options); this.db = bdb.create(this.options); } /** * Index transactions by txid. * @private * @param {BlockMeta} meta * @param {Block} block * @param {CoinView} view */ async indexBlock(meta, block, view) { const brecord = new BlockRecord({ block: meta.hash, time: block.time }); this.put(layout.b.encode(meta.height), brecord.toRaw()); for (let i = 0; i < block.txs.length; i++) { const tx = block.txs[i]; const hash = tx.hash(); const {offset, size} = tx.getPosition(); const txrecord = new TxRecord({ height: meta.height, index: i, offset: offset, length: size }); this.put(layout.t.encode(hash), txrecord.toRaw()); } } /** * Remove transactions from index. * @private * @param {BlockMeta} meta * @param {Block} block * @param {CoinView} view */ async unindexBlock(meta, block, view) { this.del(layout.b.encode(meta.height)); for (let i = 0; i < block.txs.length; i++) { const tx = block.txs[i]; const hash = tx.hash(); this.del(layout.t.encode(hash)); } } /** * Get a transaction with metadata. * @param {Hash} hash * @returns {Promise} - Returns {@link TXMeta}. */ async getMeta(hash) { const raw = await this.db.get(layout.t.encode(hash)); if (!raw) return null; const record = TxRecord.fromRaw(raw); const {height, index, offset, length} = record; const braw = await this.db.get(layout.b.encode(height)); if (!braw) return null; const brecord = BlockRecord.fromRaw(braw); const {block, time} = brecord; const data = await this.blocks.read(block, offset, length); const tx = TX.fromRaw(data); const meta = TXMeta.fromTX(tx); meta.height = height; meta.block = block; meta.time = time; meta.index = index; return meta; } /** * Retrieve a transaction. * @param {Hash} hash * @returns {Promise} - Returns {@link TX}. */ async getTX(hash) { const meta = await this.getMeta(hash); if (!meta) return null; return meta.tx; } /** * @param {Hash} hash * @returns {Promise} - Returns Boolean. */ async hasTX(hash) { return this.db.has(layout.t.encode(hash)); } /** * Get coin viewpoint (historical). * @param {TX} tx * @returns {Promise} - Returns {@link CoinView}. */ async getSpentView(tx) { const view = await this.chain.getCoinView(tx); for (const {prevout} of tx.inputs) { if (view.hasEntry(prevout)) continue; const {hash, index} = prevout; const meta = await this.getMeta(hash); if (!meta) continue; const {tx, height} = meta; if (index < tx.outputs.length) view.addIndex(tx, index, height); } return view; } } module.exports = TXIndexer;