1250 lines
34 KiB
JavaScript
1250 lines
34 KiB
JavaScript
/* eslint-env mocha */
|
|
/* eslint prefer-arrow-callback: "off" */
|
|
|
|
'use strict';
|
|
|
|
const Logger = require('blgr');
|
|
const bio = require('bufio');
|
|
const assert = require('./util/assert');
|
|
const common = require('./util/common');
|
|
const {resolve} = require('path');
|
|
const fs = require('bfile');
|
|
const {rimraf, testdir} = require('./util/common');
|
|
const random = require('bcrypto/lib/random');
|
|
|
|
const vectors = [
|
|
common.readBlock('block300025'),
|
|
common.readBlock('block426884'),
|
|
common.readBlock('block898352')
|
|
];
|
|
|
|
const extra = [
|
|
common.readBlock('block482683')
|
|
];
|
|
|
|
const undos = [
|
|
common.readBlock('block300025'),
|
|
common.readBlock('block928816'),
|
|
common.readBlock('block928828'),
|
|
common.readBlock('block928831'),
|
|
common.readBlock('block928848'),
|
|
common.readBlock('block928849')
|
|
];
|
|
|
|
const {
|
|
AbstractBlockStore,
|
|
FileBlockStore,
|
|
LevelBlockStore
|
|
} = require('../lib/blockstore');
|
|
|
|
const layout = require('../lib/blockstore/layout');
|
|
const {types} = require('../lib/blockstore/common');
|
|
|
|
const {
|
|
BlockRecord,
|
|
FileRecord
|
|
} = require('../lib/blockstore/records');
|
|
|
|
describe('BlockStore', function() {
|
|
describe('Abstract', function() {
|
|
let logger = null;
|
|
|
|
function context(ctx) {
|
|
return {info: () => ctx};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
logger = Logger.global;
|
|
Logger.global = {context};
|
|
});
|
|
|
|
afterEach(() => {
|
|
Logger.global = logger;
|
|
});
|
|
|
|
it('construct with custom logger', async () => {
|
|
const store = new AbstractBlockStore({logger: {context}});
|
|
assert(store.logger);
|
|
assert(store.logger.info);
|
|
assert.equal(store.logger.info(), 'blockstore');
|
|
});
|
|
|
|
it('construct with default logger', async () => {
|
|
const store = new AbstractBlockStore();
|
|
assert(store.logger);
|
|
assert(store.logger.info);
|
|
assert.equal(store.logger.info(), 'blockstore');
|
|
});
|
|
|
|
it('has unimplemented base methods', async () => {
|
|
const methods = ['open', 'close', 'write', 'writeUndo',
|
|
'read', 'readUndo', 'prune', 'pruneUndo',
|
|
'has', 'hasUndo', 'ensure'];
|
|
|
|
const store = new AbstractBlockStore();
|
|
|
|
for (const method of methods) {
|
|
assert(store[method]);
|
|
|
|
let err = null;
|
|
try {
|
|
await store[method]();
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
assert(err, `Expected unimplemented method ${method}.`);
|
|
assert.equal(err.message, 'Abstract method.');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Records', function() {
|
|
describe('BlockRecord', function() {
|
|
function constructError(options) {
|
|
let err = null;
|
|
|
|
try {
|
|
new BlockRecord({
|
|
file: options.file,
|
|
position: options.position,
|
|
length: options.length
|
|
});
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
}
|
|
|
|
function toAndFromRaw(options) {
|
|
const rec1 = new BlockRecord(options);
|
|
assert.equal(rec1.file, options.file);
|
|
assert.equal(rec1.position, options.position);
|
|
assert.equal(rec1.length, options.length);
|
|
|
|
const raw = rec1.toRaw();
|
|
const rec2 = BlockRecord.fromRaw(raw);
|
|
assert.equal(rec2.file, options.file);
|
|
assert.equal(rec2.position, options.position);
|
|
assert.equal(rec2.length, options.length);
|
|
}
|
|
|
|
it('construct with correct options', () => {
|
|
const rec = new BlockRecord({
|
|
file: 12,
|
|
position: 23392,
|
|
length: 4194304
|
|
});
|
|
assert.equal(rec.file, 12);
|
|
assert.equal(rec.position, 23392);
|
|
assert.equal(rec.length, 4194304);
|
|
});
|
|
|
|
it('construct null record', () => {
|
|
const rec = new BlockRecord();
|
|
assert.equal(rec.file, 0);
|
|
assert.equal(rec.position, 0);
|
|
assert.equal(rec.length, 0);
|
|
});
|
|
|
|
it('fail with signed number (file)', () => {
|
|
constructError({file: -1, position: 1, length: 1});
|
|
});
|
|
|
|
it('fail with signed number (position)', () => {
|
|
constructError({file: 1, position: -1, length: 1});
|
|
});
|
|
|
|
it('fail with signed number (length)', () => {
|
|
constructError({file: 1, position: 1, length: -1});
|
|
});
|
|
|
|
it('fail with non-32-bit number (file)', () => {
|
|
constructError({file: Math.pow(2, 32), position: 1, length: 1});
|
|
});
|
|
|
|
it('fail with non-32-bit number (position)', () => {
|
|
constructError({file: 1, position: Math.pow(2, 32), length: 1});
|
|
});
|
|
|
|
it('fail with non-32-bit number (length)', () => {
|
|
constructError({file: 1, position: 1, length: Math.pow(2, 32)});
|
|
});
|
|
|
|
it('construct with max 32-bit numbers', () => {
|
|
const max = Math.pow(2, 32) - 1;
|
|
|
|
const rec = new BlockRecord({
|
|
file: max,
|
|
position: max,
|
|
length: max
|
|
});
|
|
|
|
assert(rec);
|
|
assert.equal(rec.file, max);
|
|
assert.equal(rec.position, max);
|
|
assert.equal(rec.length, max);
|
|
});
|
|
|
|
it('serialize/deserialize file record (min)', () => {
|
|
toAndFromRaw({file: 0, position: 0, length: 0});
|
|
});
|
|
|
|
it('serialize/deserialize file record', () => {
|
|
toAndFromRaw({file: 12, position: 23392, length: 4194304});
|
|
});
|
|
|
|
it('serialize/deserialize file record (max)', () => {
|
|
const max = Math.pow(2, 32) - 1;
|
|
toAndFromRaw({file: max, position: max, length: max});
|
|
});
|
|
});
|
|
|
|
describe('FileRecord', function() {
|
|
function constructError(options) {
|
|
let err = null;
|
|
|
|
try {
|
|
new FileRecord({
|
|
blocks: options.blocks,
|
|
used: options.used,
|
|
length: options.length
|
|
});
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
}
|
|
|
|
function toAndFromRaw(options) {
|
|
const rec1 = new FileRecord(options);
|
|
assert.equal(rec1.blocks, options.blocks);
|
|
assert.equal(rec1.used, options.used);
|
|
assert.equal(rec1.length, options.length);
|
|
|
|
const raw = rec1.toRaw();
|
|
const rec2 = FileRecord.fromRaw(raw);
|
|
assert.equal(rec2.blocks, options.blocks);
|
|
assert.equal(rec2.used, options.used);
|
|
assert.equal(rec2.length, options.length);
|
|
}
|
|
|
|
it('construct with correct options', () => {
|
|
const rec = new FileRecord({
|
|
blocks: 1,
|
|
used: 4194304,
|
|
length: 20971520
|
|
});
|
|
assert.equal(rec.blocks, 1);
|
|
assert.equal(rec.used, 4194304);
|
|
assert.equal(rec.length, 20971520);
|
|
});
|
|
|
|
it('fail to with signed number (blocks)', () => {
|
|
constructError({blocks: -1, used: 1, length: 1});
|
|
});
|
|
|
|
it('fail to with signed number (used)', () => {
|
|
constructError({blocks: 1, used: -1, length: 1});
|
|
});
|
|
|
|
it('fail to with signed number (length)', () => {
|
|
constructError({blocks: 1, used: 1, length: -1});
|
|
});
|
|
|
|
it('fail to with non-32-bit number (blocks)', () => {
|
|
constructError({blocks: Math.pow(2, 32), used: 1, length: 1});
|
|
});
|
|
|
|
it('fail to with non-32-bit number (used)', () => {
|
|
constructError({blocks: 1, used: Math.pow(2, 32), length: 1});
|
|
});
|
|
|
|
it('fail to with non-32-bit number (length)', () => {
|
|
constructError({blocks: 1, used: 1, length: Math.pow(2, 32)});
|
|
});
|
|
|
|
it('serialize/deserialize block record (min)', () => {
|
|
toAndFromRaw({blocks: 0, used: 0, length: 0});
|
|
});
|
|
|
|
it('serialize/deserialize block record', () => {
|
|
toAndFromRaw({blocks: 10, used: 4194304, length: 20971520});
|
|
});
|
|
|
|
it('serialize/deserialize block record (max)', () => {
|
|
const max = Math.pow(2, 32) - 1;
|
|
toAndFromRaw({blocks: max, used: max, length: max});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FileBlockStore (Unit)', function() {
|
|
const location = '/tmp/.bcoin/blocks';
|
|
let store = null;
|
|
|
|
before(() => {
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024
|
|
});
|
|
});
|
|
|
|
describe('constructor', function() {
|
|
it('will pass options to super', () => {
|
|
const info = () => 'info';
|
|
const logger = {
|
|
context: () => {
|
|
return {info};
|
|
}
|
|
};
|
|
|
|
const store = new FileBlockStore({
|
|
location: '/tmp/.bcoin/blocks',
|
|
maxFileLength: 1024,
|
|
logger: logger
|
|
});
|
|
|
|
assert.strictEqual(store.logger.info, info);
|
|
});
|
|
|
|
it('will error with invalid location', () => {
|
|
let err = null;
|
|
|
|
try {
|
|
new FileBlockStore({
|
|
location: 'tmp/.bcoin/blocks',
|
|
maxFileLength: 1024
|
|
});
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'Location not absolute.');
|
|
});
|
|
|
|
it('will error with invalid max file length', () => {
|
|
let err = null;
|
|
|
|
try {
|
|
new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 'notanumber'
|
|
});
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'Invalid max file length.');
|
|
});
|
|
});
|
|
|
|
describe('allocate', function() {
|
|
it('will fail with length above file max', async () => {
|
|
let err = null;
|
|
try {
|
|
await store.allocate(types.BLOCK, 1025);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
assert(err);
|
|
assert.equal(err.message, 'Block length above max file length.');
|
|
});
|
|
});
|
|
|
|
describe('filepath', function() {
|
|
it('will give correct path (0)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 0);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk00000.dat');
|
|
});
|
|
|
|
it('will give correct path (1)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 7);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk00007.dat');
|
|
});
|
|
|
|
it('will give correct path (2)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 23);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk00023.dat');
|
|
});
|
|
|
|
it('will give correct path (3)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 456);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk00456.dat');
|
|
});
|
|
|
|
it('will give correct path (4)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 8999);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk08999.dat');
|
|
});
|
|
|
|
it('will give correct path (5)', () => {
|
|
const filepath = store.filepath(types.BLOCK, 99999);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blk99999.dat');
|
|
});
|
|
|
|
it('will fail over max size', () => {
|
|
let err = null;
|
|
try {
|
|
store.filepath(types.BLOCK, 100000);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'File number too large.');
|
|
});
|
|
|
|
it('will give undo type', () => {
|
|
const filepath = store.filepath(types.UNDO, 99999);
|
|
assert.equal(filepath, '/tmp/.bcoin/blocks/blu99999.dat');
|
|
});
|
|
|
|
it('will fail for unknown prefix', () => {
|
|
let err = null;
|
|
try {
|
|
store.filepath(0, 1234);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'Unknown file prefix.');
|
|
});
|
|
});
|
|
|
|
describe('write', function() {
|
|
const write = fs.write;
|
|
const open = fs.open;
|
|
const close = fs.close;
|
|
let allocate = null;
|
|
let has = null;
|
|
|
|
beforeEach(() => {
|
|
allocate = store.allocate;
|
|
has = store.db.has;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore stubbed methods.
|
|
fs.write = write;
|
|
fs.open = open;
|
|
fs.close = close;
|
|
store.allocate = allocate;
|
|
store.db.has = has;
|
|
});
|
|
|
|
it('will error if total magic bytes not written', async () => {
|
|
let err = null;
|
|
|
|
store.allocate = () => {
|
|
return {
|
|
fileno: 20,
|
|
filerecord: {
|
|
used: 0
|
|
},
|
|
filepath: '/tmp/.bcoin/blocks/blk00020.dat'
|
|
};
|
|
};
|
|
store.db.has = () => false;
|
|
fs.open = () => 7;
|
|
fs.close = () => undefined;
|
|
fs.write = () => 0;
|
|
|
|
try {
|
|
const hash = random.randomBytes(128);
|
|
const block = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err, 'Expected error.');
|
|
assert.equal(err.message, 'Could not write block magic.');
|
|
});
|
|
|
|
it('will error if total block bytes not written', async () => {
|
|
let err = 0;
|
|
|
|
let called = 0;
|
|
store.allocate = () => {
|
|
return {
|
|
fileno: 20,
|
|
filerecord: {
|
|
used: 0
|
|
},
|
|
filepath: '/tmp/.bcoin/blocks/blk00020.dat'
|
|
};
|
|
};
|
|
store.db.has = () => false;
|
|
fs.open = () => 7;
|
|
fs.close = () => undefined;
|
|
fs.write = (fd, buffer, offset, length, position) => {
|
|
let written = 0;
|
|
|
|
if (called === 0)
|
|
written = length;
|
|
|
|
called += 1;
|
|
|
|
return written;
|
|
};
|
|
|
|
try {
|
|
const hash = random.randomBytes(128);
|
|
const block = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err, 'Expected error.');
|
|
assert.equal(err.message, 'Could not write block.');
|
|
});
|
|
|
|
it('will close file if write throws', async () => {
|
|
let err = null;
|
|
let closed = null;
|
|
|
|
store.allocate = () => {
|
|
return {
|
|
fileno: 20,
|
|
filerecord: {
|
|
used: 0
|
|
},
|
|
filepath: '/tmp/.bcoin/blocks/blk00020.dat'
|
|
};
|
|
};
|
|
store.db.has = () => false;
|
|
fs.open = () => 7;
|
|
fs.close = (fd) => {
|
|
closed = fd;
|
|
};
|
|
fs.write = () => {
|
|
throw new Error('Test.');
|
|
};
|
|
|
|
try {
|
|
const hash = random.randomBytes(128);
|
|
const block = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err, 'Expected error.');
|
|
assert.equal(err.message, 'Test.');
|
|
assert.equal(closed, 7);
|
|
});
|
|
});
|
|
|
|
describe('read', function() {
|
|
const read = fs.read;
|
|
const open = fs.open;
|
|
const close = fs.close;
|
|
let get = null;
|
|
let raw = null;
|
|
|
|
before(() => {
|
|
const record = new BlockRecord({
|
|
file: 1,
|
|
position: 8,
|
|
length: 100
|
|
});
|
|
raw = record.toRaw();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
get = store.db.get;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore stubbed methods.
|
|
store.db.get = get;
|
|
fs.read = read;
|
|
fs.open = open;
|
|
fs.close = close;
|
|
});
|
|
|
|
it('will error if total read bytes not correct', async () => {
|
|
let err = null;
|
|
|
|
store.db.get = () => raw;
|
|
fs.open = () => 7;
|
|
fs.close = () => undefined;
|
|
fs.read = () => 99;
|
|
|
|
try {
|
|
const hash = random.randomBytes(128);
|
|
const block = random.randomBytes(32);
|
|
await store.read(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err, 'Expected error.');
|
|
assert.equal(err.message, 'Wrong number of bytes read.');
|
|
});
|
|
|
|
it('will close file if read throws', async () => {
|
|
let err = null;
|
|
let closed = null;
|
|
|
|
store.db.get = () => raw;
|
|
fs.open = () => 7;
|
|
fs.close = (fd) => {
|
|
closed = fd;
|
|
};
|
|
fs.read = () => {
|
|
throw new Error('Test.');
|
|
};
|
|
|
|
try {
|
|
const hash = random.randomBytes(128);
|
|
const block = random.randomBytes(32);
|
|
await store.read(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err, 'Expected error.');
|
|
assert.equal(err.message, 'Test.');
|
|
assert.equal(closed, 7);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('FileBlockStore (Integration 1)', function() {
|
|
const location = testdir('blockstore');
|
|
let store = null;
|
|
|
|
beforeEach(async () => {
|
|
await rimraf(location);
|
|
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024
|
|
});
|
|
|
|
await store.ensure();
|
|
await store.open();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await store.close();
|
|
});
|
|
|
|
after(async () => {
|
|
await rimraf(location);
|
|
});
|
|
|
|
it('will write and read a block', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const block2 = await store.read(hash);
|
|
|
|
assert.bufferEqual(block1, block2);
|
|
});
|
|
|
|
it('will write and read block undo coins', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.writeUndo(hash, block1);
|
|
|
|
const block2 = await store.readUndo(hash);
|
|
|
|
assert.bufferEqual(block1, block2);
|
|
});
|
|
|
|
it('will read a block w/ offset and length', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const offset = 79;
|
|
const size = 15;
|
|
|
|
const block2 = await store.read(hash, offset, size);
|
|
|
|
assert.bufferEqual(block1.slice(offset, offset + size), block2);
|
|
});
|
|
|
|
it('will fail to read w/ out-of-bounds length', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const offset = 79;
|
|
const size = 50;
|
|
|
|
let err = null;
|
|
try {
|
|
await store.read(hash, offset, size);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'Out-of-bounds read.');
|
|
});
|
|
|
|
it('will allocate new files', async () => {
|
|
const blocks = [];
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
blocks.push({hash, block});
|
|
await store.write(hash, block);
|
|
const block2 = await store.read(hash);
|
|
assert.bufferEqual(block2, block);
|
|
}
|
|
|
|
const first = await fs.stat(store.filepath(types.BLOCK, 0));
|
|
const second = await fs.stat(store.filepath(types.BLOCK, 1));
|
|
const third = await fs.stat(store.filepath(types.BLOCK, 2));
|
|
assert.equal(first.size, 952);
|
|
assert.equal(second.size, 952);
|
|
assert.equal(third.size, 272);
|
|
|
|
const magic = (8 * 16);
|
|
const len = first.size + second.size + third.size - magic;
|
|
assert.equal(len, 128 * 16);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const expect = blocks[i];
|
|
const block = await store.read(expect.hash);
|
|
assert.bufferEqual(block, expect.block);
|
|
}
|
|
});
|
|
|
|
it('will allocate new files with block undo coins', async () => {
|
|
const blocks = [];
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
blocks.push({hash, block});
|
|
await store.writeUndo(hash, block);
|
|
const block2 = await store.readUndo(hash);
|
|
assert.bufferEqual(block2, block);
|
|
}
|
|
|
|
const first = await fs.stat(store.filepath(types.UNDO, 0));
|
|
const second = await fs.stat(store.filepath(types.UNDO, 1));
|
|
const third = await fs.stat(store.filepath(types.UNDO, 2));
|
|
|
|
const magic = (40 * 16);
|
|
const len = first.size + second.size + third.size - magic;
|
|
assert.equal(len, 128 * 16);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const expect = blocks[i];
|
|
const block = await store.readUndo(expect.hash);
|
|
assert.bufferEqual(block, expect.block);
|
|
}
|
|
});
|
|
|
|
it('will recover from interrupt during block write', async () => {
|
|
{
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
|
|
const block2 = await store.read(hash);
|
|
assert.bufferEqual(block2, block);
|
|
}
|
|
|
|
// Manually insert a partially written block to the
|
|
// end of file as would be the case of an untimely
|
|
// interrupted write of a block. The file record
|
|
// would not be updated to include the used bytes and
|
|
// thus this data should be overwritten.
|
|
{
|
|
const filepath = store.filepath(types.BLOCK, 0);
|
|
|
|
const fd = await fs.open(filepath, 'a');
|
|
|
|
const bw = bio.write(8);
|
|
bw.writeU32(store.network.magic);
|
|
bw.writeU32(73);
|
|
const magic = bw.render();
|
|
|
|
const failblock = random.randomBytes(73);
|
|
|
|
const mwritten = await fs.write(fd, magic, 0, 8);
|
|
const bwritten = await fs.write(fd, failblock, 0, 73);
|
|
|
|
await fs.close(fd);
|
|
|
|
assert.equal(mwritten, 8);
|
|
assert.equal(bwritten, 73);
|
|
}
|
|
|
|
// Now check that this block has the correct position
|
|
// in the file and that it can be read correctly.
|
|
{
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
|
|
const block2 = await store.read(hash);
|
|
assert.bufferEqual(block2, block);
|
|
}
|
|
});
|
|
|
|
it('will not write blocks at the same position', (done) => {
|
|
let err = null;
|
|
let finished = 0;
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
// Accidentally don't use `await` and attempt to
|
|
// write multiple blocks in parallel and at the
|
|
// same file position.
|
|
(async () => {
|
|
try {
|
|
await store.write(hash, block);
|
|
} catch (e) {
|
|
err = e;
|
|
} finally {
|
|
finished += 1;
|
|
if (finished >= 16) {
|
|
assert(err);
|
|
assert(err.message, 'Already writing.');
|
|
done();
|
|
}
|
|
}
|
|
})();
|
|
}
|
|
});
|
|
|
|
it('will not duplicate a block on disk', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
const first = await store.write(hash, block);
|
|
assert.equal(first, true);
|
|
const second = await store.write(hash, block);
|
|
assert.equal(second, false);
|
|
|
|
const pruned = await store.prune(hash);
|
|
assert.equal(pruned, true);
|
|
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 0)), false);
|
|
});
|
|
|
|
it('will return null if block not found', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const block = await store.read(hash);
|
|
assert.strictEqual(block, null);
|
|
});
|
|
|
|
it('will check if block exists (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
|
|
it('will check if block exists (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, true);
|
|
});
|
|
|
|
it('will check if block undo coins exists (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.hasUndo(hash);
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
|
|
it('will check if block undo coins exists (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.writeUndo(hash, block);
|
|
const exists = await store.hasUndo(hash);
|
|
assert.strictEqual(exists, true);
|
|
});
|
|
|
|
it('will prune blocks', async () => {
|
|
const hashes = [];
|
|
for (let i = 0; i < 16; i++) {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
hashes.push(hash);
|
|
await store.write(hash, block);
|
|
}
|
|
|
|
const first = await fs.stat(store.filepath(types.BLOCK, 0));
|
|
const second = await fs.stat(store.filepath(types.BLOCK, 1));
|
|
const third = await fs.stat(store.filepath(types.BLOCK, 2));
|
|
|
|
const magic = (8 * 16);
|
|
const len = first.size + second.size + third.size - magic;
|
|
assert.equal(len, 128 * 16);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const pruned = await store.prune(hashes[i]);
|
|
assert.strictEqual(pruned, true);
|
|
}
|
|
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 0)), false);
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 1)), false);
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 2)), false);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const exists = await store.has(hashes[i]);
|
|
assert.strictEqual(exists, false);
|
|
}
|
|
|
|
const exists = await store.db.has(layout.f.encode(types.BLOCK, 0));
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
|
|
it('will prune block undo coins', async () => {
|
|
const hashes = [];
|
|
for (let i = 0; i < 16; i++) {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
hashes.push(hash);
|
|
await store.writeUndo(hash, block);
|
|
}
|
|
|
|
const first = await fs.stat(store.filepath(types.UNDO, 0));
|
|
const second = await fs.stat(store.filepath(types.UNDO, 1));
|
|
const third = await fs.stat(store.filepath(types.UNDO, 2));
|
|
|
|
const magic = (40 * 16);
|
|
const len = first.size + second.size + third.size - magic;
|
|
assert.equal(len, 128 * 16);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const pruned = await store.pruneUndo(hashes[i]);
|
|
assert.strictEqual(pruned, true);
|
|
}
|
|
|
|
assert.equal(await fs.exists(store.filepath(types.UNDO, 0)), false);
|
|
assert.equal(await fs.exists(store.filepath(types.UNDO, 1)), false);
|
|
assert.equal(await fs.exists(store.filepath(types.UNDO, 2)), false);
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
const exists = await store.hasUndo(hashes[i]);
|
|
assert.strictEqual(exists, false);
|
|
}
|
|
|
|
const exists = await store.db.has(layout.f.encode(types.UNDO, 0));
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
});
|
|
|
|
describe('FileBlockStore (Integration 2)', function() {
|
|
const location = testdir('blockstore');
|
|
let store = null;
|
|
|
|
beforeEach(async () => {
|
|
await rimraf(location);
|
|
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024 * 1024
|
|
});
|
|
|
|
await store.ensure();
|
|
await store.open();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await store.close();
|
|
});
|
|
|
|
after(async () => {
|
|
await rimraf(location);
|
|
});
|
|
|
|
it('will import from files (e.g. db corruption)', async () => {
|
|
const blocks = [];
|
|
|
|
for (let i = 0; i < vectors.length; i++) {
|
|
const [block] = vectors[i].getBlock();
|
|
const hash = block.hash();
|
|
const raw = block.toRaw();
|
|
|
|
blocks.push({hash, block: raw});
|
|
await store.write(hash, raw);
|
|
}
|
|
|
|
await store.close();
|
|
|
|
await rimraf(resolve(location, './index'));
|
|
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024
|
|
});
|
|
|
|
await store.open();
|
|
|
|
for (let i = 0; i < vectors.length; i++) {
|
|
const expect = blocks[i];
|
|
const block = await store.read(expect.hash);
|
|
assert.equal(block.length, expect.block.length);
|
|
assert.bufferEqual(block, expect.block);
|
|
}
|
|
});
|
|
|
|
it('will import from files after write interrupt', async () => {
|
|
const blocks = [];
|
|
|
|
for (let i = 0; i < vectors.length; i++) {
|
|
const [block] = vectors[i].getBlock();
|
|
const hash = block.hash();
|
|
const raw = block.toRaw();
|
|
|
|
blocks.push({hash, block: raw});
|
|
await store.write(hash, raw);
|
|
}
|
|
|
|
await store.close();
|
|
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 0)), true);
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 1)), true);
|
|
assert.equal(await fs.exists(store.filepath(types.BLOCK, 2)), false);
|
|
|
|
// Write partial block as would be the case in a
|
|
// block write interrupt.
|
|
const [partial] = extra[0].getBlock();
|
|
{
|
|
// Include all of the header, but not the block.
|
|
let raw = partial.toRaw();
|
|
const actual = raw.length;
|
|
const part = raw.length - 1;
|
|
raw = raw.slice(0, part);
|
|
|
|
const filepath = store.filepath(types.BLOCK, 1);
|
|
|
|
const fd = await fs.open(filepath, 'a');
|
|
|
|
const bw = bio.write(8);
|
|
bw.writeU32(store.network.magic);
|
|
bw.writeU32(actual);
|
|
const magic = bw.render();
|
|
|
|
const mwritten = await fs.write(fd, magic, 0, 8);
|
|
const bwritten = await fs.write(fd, raw, 0, part);
|
|
|
|
await fs.close(fd);
|
|
|
|
assert.equal(mwritten, 8);
|
|
assert.equal(bwritten, part);
|
|
}
|
|
|
|
await rimraf(resolve(location, './index'));
|
|
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024
|
|
});
|
|
|
|
await store.open();
|
|
|
|
const incomplete = await store.read(partial.hash());
|
|
assert(incomplete === null);
|
|
|
|
for (let i = 0; i < vectors.length; i++) {
|
|
const expect = blocks[i];
|
|
const block = await store.read(expect.hash);
|
|
assert.equal(block.length, expect.block.length);
|
|
assert.bufferEqual(block, expect.block);
|
|
}
|
|
});
|
|
|
|
it('will import undo blocks from files', async () => {
|
|
const blocks = [];
|
|
|
|
for (let i = 0; i < undos.length; i++) {
|
|
const [block] = undos[i].getBlock();
|
|
const raw = undos[i].undoRaw;
|
|
const hash = block.hash();
|
|
|
|
blocks.push({hash, block: raw});
|
|
await store.writeUndo(hash, raw);
|
|
}
|
|
|
|
await store.close();
|
|
|
|
await rimraf(resolve(location, './index'));
|
|
|
|
store = new FileBlockStore({
|
|
location: location,
|
|
maxFileLength: 1024
|
|
});
|
|
|
|
await store.open();
|
|
|
|
for (let i = 0; i < undos.length; i++) {
|
|
const expect = blocks[i];
|
|
const block = await store.readUndo(expect.hash);
|
|
assert.equal(block.length, expect.block.length);
|
|
assert.bufferEqual(block, expect.block);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('LevelBlockStore', function() {
|
|
const location = testdir('blockstore');
|
|
let store = null;
|
|
|
|
beforeEach(async () => {
|
|
await rimraf(location);
|
|
|
|
store = new LevelBlockStore({
|
|
location: location
|
|
});
|
|
|
|
await store.ensure();
|
|
await store.open();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await store.close();
|
|
});
|
|
|
|
after(async () => {
|
|
await rimraf(location);
|
|
});
|
|
|
|
it('will write and read a block', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const block2 = await store.read(hash);
|
|
|
|
assert.bufferEqual(block1, block2);
|
|
});
|
|
|
|
it('will write and read block undo coins', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.writeUndo(hash, block1);
|
|
|
|
const block2 = await store.readUndo(hash);
|
|
|
|
assert.bufferEqual(block1, block2);
|
|
});
|
|
|
|
it('will read a block w/ offset and length', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const offset = 79;
|
|
const size = 15;
|
|
|
|
const block2 = await store.read(hash, offset, size);
|
|
|
|
assert.bufferEqual(block1.slice(offset, offset + size), block2);
|
|
});
|
|
|
|
it('will fail to read w/ out-of-bounds length', async () => {
|
|
const block1 = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
|
|
await store.write(hash, block1);
|
|
|
|
const offset = 79;
|
|
const size = 50;
|
|
|
|
let err = null;
|
|
try {
|
|
await store.read(hash, offset, size);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
|
|
assert(err);
|
|
assert.equal(err.message, 'Out-of-bounds read.');
|
|
});
|
|
|
|
it('will check if block exists (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
|
|
it('will check if block exists (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, true);
|
|
});
|
|
|
|
it('will check if block undo coins exists (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, false);
|
|
});
|
|
|
|
it('will check if block undo coins exists (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.writeUndo(hash, block);
|
|
const exists = await store.hasUndo(hash);
|
|
assert.strictEqual(exists, true);
|
|
});
|
|
|
|
it('will prune blocks (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.write(hash, block);
|
|
const pruned = await store.prune(hash);
|
|
assert.strictEqual(pruned, true);
|
|
const block2 = await store.read(hash);
|
|
assert.strictEqual(block2, null);
|
|
});
|
|
|
|
it('will prune blocks (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.has(hash);
|
|
assert.strictEqual(exists, false);
|
|
const pruned = await store.prune(hash);
|
|
assert.strictEqual(pruned, false);
|
|
});
|
|
|
|
it('will prune block undo coins (true)', async () => {
|
|
const block = random.randomBytes(128);
|
|
const hash = random.randomBytes(32);
|
|
await store.writeUndo(hash, block);
|
|
const pruned = await store.pruneUndo(hash);
|
|
assert.strictEqual(pruned, true);
|
|
const block2 = await store.readUndo(hash);
|
|
assert.strictEqual(block2, null);
|
|
});
|
|
|
|
it('will prune block undo coins (false)', async () => {
|
|
const hash = random.randomBytes(32);
|
|
const exists = await store.hasUndo(hash);
|
|
assert.strictEqual(exists, false);
|
|
const pruned = await store.pruneUndo(hash);
|
|
assert.strictEqual(pruned, false);
|
|
});
|
|
});
|
|
});
|