384 lines
8 KiB
JavaScript
384 lines
8 KiB
JavaScript
/*!
|
|
* 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;
|