UNPKG

hsd

Version:
385 lines (304 loc) 8.24 kB
/*! * blockstore/level.js - leveldb blockstore for hsd * Copyright (c) 2019, Braydon Fuller (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const bdb = require('bdb'); const fs = require('bfile'); const AbstractBlockStore = require('./abstract'); const {AbstractBatch} = require('./abstract'); const layout = require('./layout'); const {types} = require('./common'); /** * LevelDB Block Store * * @alias module:blockstore:LevelBlockStore * @abstract */ class LevelBlockStore extends AbstractBlockStore { /** * Create a blockstore that stores blocks in LevelDB. * @constructor * @param {Object} [options] */ constructor(options) { super(options); this.location = options.location; this.db = bdb.create({ location: this.location, cacheSize: options.cacheSize, compression: false, memory: options.memory }); } /** * This method ensures that the storage directory exists * before opening. * @returns {Promise} */ async ensure() { return fs.mkdirp(this.location); } /** * Opens the block storage. * @returns {Promise} */ async open() { this.logger.info('Opening LevelBlockStore...'); await this.db.open(); await this.db.verify(layout.V.encode(), 'levelblockstore', 0); } /** * Closes the block storage. */ async close() { this.logger.info('Closing LevelBlockStore...'); await this.db.close(); } /** * This method stores merkle block data in LevelDB. * @param {Buffer} hash - The block hash * @param {Buffer} data - The block data * @returns {Promise} */ async writeMerkle(hash, data) { return this.db.put(layout.b.encode(types.MERKLE, hash), data); } /** * This method stores block undo coin data in LevelDB. * @param {Buffer} hash - The block hash * @param {Buffer} data - The block data * @returns {Promise} */ async writeUndo(hash, data) { return this.db.put(layout.b.encode(types.UNDO, hash), data); } /** * This method stores block data in LevelDB. * @param {Buffer} hash - The block hash * @param {Buffer} data - The block data * @returns {Promise} */ async writeBlock(hash, data) { return this.db.put(layout.b.encode(types.BLOCK, hash), data); } /** * This method will retrieve merkle block data. * @param {Buffer} hash - The block hash * @returns {Promise} */ async readMerkle(hash) { return this.db.get(layout.b.encode(types.MERKLE, hash)); } /** * This method will retrieve block undo coin data. * @param {Buffer} hash - The block hash * @returns {Promise} */ async readUndo(hash) { return this.db.get(layout.b.encode(types.UNDO, hash)); } /** * This method will retrieve block data. Smaller portions of the * block (e.g. transactions) can be returned using the offset and * length arguments. However, the entire block will be read as the * data is stored in a key/value database. * @param {Buffer} hash - The block hash * @param {Number} offset - The offset within the block * @param {Number} length - The number of bytes of the data * @returns {Promise} */ async readBlock(hash, offset, length) { let raw = await this.db.get(layout.b.encode(types.BLOCK, hash)); if (offset) { if (offset + length > raw.length) throw new Error('Out-of-bounds read.'); raw = raw.slice(offset, offset + length); } return raw; } /** * This will free resources for storing merkle block data. * The block data may not be immediately removed from disk, and will * be reclaimed during LevelDB compaction. * @param {Buffer} hash - The block hash * @returns {Promise} */ async pruneMerkle(hash) { if (!await this.hasMerkle(hash)) return false; await this.db.del(layout.b.encode(types.MERKLE, hash)); return true; } /** * This will free resources for storing the block undo coin data. * The block data may not be immediately removed from disk, and will * be reclaimed during LevelDB compaction. * @param {Buffer} hash - The block hash * @returns {Promise} */ async pruneUndo(hash) { if (!await this.hasUndo(hash)) return false; await this.db.del(layout.b.encode(types.UNDO, hash)); return true; } /** * This will free resources for storing the block data. The block * data may not be immediately removed from disk, and will be reclaimed * during LevelDB compaction. * @param {Buffer} hash - The block hash * @returns {Promise} */ async pruneBlock(hash) { if (!await this.hasBlock(hash)) return false; await this.db.del(layout.b.encode(types.BLOCK, hash)); return true; } /** * This will check if a merkle block data has been stored * and is available. * @param {Buffer} hash - The block hash * @returns {Promise} */ async hasMerkle(hash) { return this.db.has(layout.b.encode(types.MERKLE, hash)); } /** * This will check if a block undo coin data has been stored * and is available. * @param {Buffer} hash - The block hash * @returns {Promise} */ async hasUndo(hash) { return this.db.has(layout.b.encode(types.UNDO, hash)); } /** * This will check if a block has been stored and is available. * @param {Buffer} hash - The block hash * @returns {Promise} */ async hasBlock(hash) { return this.db.has(layout.b.encode(types.BLOCK, hash)); } /** * Create batch. * @returns {LevelBatch} */ batch() { return new LevelBatch(this.db); } } /** * Batch wrapper for the level blockstore. * @alias module:blockstore.LevelBatch */ class LevelBatch extends AbstractBatch { /** * Create LevelBatch * @param {bdb.DB} db */ constructor(db) { super(); this.writesBatch = db.batch(); this.prunesBatch = db.batch(); this.committedWrites = false; this.committedPrunes = false; } get written() { return this.committedPrunes && this.committedWrites; } /** * Write merkle block data to the batch. * @param {Buffer} hash * @param {Buffer} data * @returns {this} */ writeMerkle(hash, data) { this.writesBatch.put(layout.b.encode(types.MERKLE, hash), data); return this; } /** * Write undo coin data to the batch. * @param {Buffer} hash * @param {Buffer} data * @returns {this} */ writeUndo(hash, data) { this.writesBatch.put(layout.b.encode(types.UNDO, hash), data); return this; } /** * Write block data to the batch. * @param {Buffer} hash * @param {Buffer} data * @returns {this} */ writeBlock(hash, data) { this.writesBatch.put(layout.b.encode(types.BLOCK, hash), data); return this; } /** * Remove merkle block data from the batch. * @param {Buffer} hash * @returns {this} */ pruneMerkle(hash) { this.prunesBatch.del(layout.b.encode(types.MERKLE, hash)); return this; } /** * Remove undo data from the batch. * @param {Buffer} hash * @returns {this} */ pruneUndo(hash) { this.prunesBatch.del(layout.b.encode(types.UNDO, hash)); return this; } /** * Prune block data from the batch. * @param {Buffer} hash * @returns {this} */ pruneBlock(hash) { this.prunesBatch.del(layout.b.encode(types.BLOCK, hash)); return this; } /** * Clear the batch. * @returns {this} */ clear() { assert(!this.written, 'Already written all.'); this.writesBatch.clear(); this.prunesBatch.clear(); return this; } async commitWrites() { assert(!this.committedWrites, 'Already written writes.'); await this.writesBatch.write(); this.committedWrites = true; } async commitPrunes() { assert(!this.committedPrunes, 'Already written prunes.'); await this.prunesBatch.write(); this.committedPrunes = true; } /** * Write change to the store. * @returns {Promise} */ async commit() { assert(!this.written, 'Already written all.'); await this.commitWrites(); await this.commitPrunes(); } } /* * Expose */ module.exports = LevelBlockStore;