itns-sidechain/test/util/migrations.js

469 lines
11 KiB
JavaScript

/*!
* migrations.js - Mock chain and migrations for the migration test.
* Copyright (c) 2021, Nodari Chkuaselidze (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const Logger = require('blgr');
const Network = require('../../lib/protocol/network');
const consensus = require('../../lib/protocol/consensus');
const BlockTemplate = require('../../lib/mining/template');
const bdb = require('bdb');
/** @typedef {import('../../lib/blockchain/chain')} Chain */
/** @typedef {import('../../lib/mining/miner')} Miner */
/** @typedef {import('../../lib/primitives/address')} Address */
let Migrator = class {};
try {
const migrator = require('../../lib/migrations/migrator');
Migrator = migrator.Migrator;
} catch (e) {
;
}
const oldMockLayout = {
V: bdb.key('V'),
M: bdb.key('M', ['uint32'])
};
const mockLayout = {
V: bdb.key('V'),
M: bdb.key('M'),
// data for testing
a: bdb.key('a'),
b: bdb.key('b'),
c: bdb.key('c'),
d: bdb.key('d')
};
const DB_FLAG_ERROR = 'mock chain needs migration';
/**
* This could be ChainDB or WalletDB.
* This will resemble ChainDB because it's easier to illustrate
* structure of the migrations.
*/
class MockChainDB {
constructor(options) {
this.options = new MockChainDBOptions(options);
this.logger = this.options.logger;
this.network = this.options.network;
this.db = bdb.create(this.options);
this.dbVersion = 0;
this.spv = this.options.spv;
this.prune = this.options.prune;
}
async open() {
this.logger.debug('Opening mock chaindb.');
await this.db.open();
// This is here for testing purposes.
const migrations = new MockChainDBMigrator({
...this.options,
db: this,
dbVersion: this.dbVersion
});
await migrations.migrate();
await this.db.verify(mockLayout.V.encode(), 'chain', this.dbVersion);
}
async close() {
this.logger.debug('Closing mock chaindb.');
await this.db.close();
}
}
class MockChainDBOptions {
constructor(options) {
this.network = Network.primary;
this.logger = Logger.global;
this.prefix = null;
this.location = null;
this.memory = true;
this.spv = false;
this.prune = false;
this.migrateFlag = -1;
this.migrations = null;
this.fromOptions(options);
}
fromOptions(options) {
assert(options);
if (options.network != null)
this.network = Network.get(options.network);
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger;
}
if (options.prefix != null) {
assert(typeof options.prefix === 'string');
this.prefix = options.prefix;
}
if (options.spv != null) {
assert(typeof options.spv === 'boolean');
this.spv = options.spv;
}
if (options.prune != null) {
assert(typeof options.prune === 'boolean');
this.prune = options.prune;
}
if (options.prefix != null) {
assert(typeof options.prefix === 'string');
this.prefix = options.prefix;
this.location = options.prefix;
}
if (options.memory != null) {
assert(typeof options.memory === 'boolean');
this.memory = options.memory;
}
if (options.migrateFlag != null) {
assert(typeof options.migrateFlag === 'number');
this.migrateFlag = options.migrateFlag;
}
if (options.migrations != null) {
assert(typeof options.migrations === 'object');
this.migrations = options.migrations;
}
}
}
class MockChainDBMigrator extends Migrator {
constructor(options) {
super(new MockChainDBMigratorOptions(options));
this.logger = this.options.logger.context('mock-migrations');
this.flagError = DB_FLAG_ERROR;
}
}
class MockChainDBMigratorOptions {
constructor(options) {
this.network = options.network;
this.logger = options.logger;
this.migrateFlag = options.migrateFlag;
this.migrations = exports.migrations;
this.dbVersion = options.dbVersion;
this.db = options.db;
this.ldb = options.db.db;
this.layout = mockLayout;
this.spv = options.spv;
this.prune = options.prune;
this.fromOptions(options);
}
fromOptions(options) {
if (options.migrations != null) {
assert(typeof options.migrations === 'object');
this.migrations = options.migrations;
}
}
}
exports.migrations = {};
exports.MockChainDB = MockChainDB;
exports.MockChainDBMigrator = MockChainDBMigrator;
exports.mockLayout = mockLayout;
exports.oldMockLayout = oldMockLayout;
exports.DB_FLAG_ERROR = DB_FLAG_ERROR;
exports.migrationError = function migrationError(migrations, ids, flagError) {
let error = 'Database needs migration(s):\n';
for (const id of ids) {
const info = migrations[id].info();
error += ` - ${info.name} - ${info.description}\n`;
}
error += flagError;
return error;
};
exports.prefix2hex = function prefix2hex(prefix) {
return Buffer.from(prefix, 'ascii').toString('hex');
};
exports.dumpDB = async function dumpDB(db, prefixes) {
const data = await db.dump();
return exports.filteredObject(data, prefixes);
};
exports.filteredObject = function filteredObject(data, prefixes) {
const filtered = {};
for (const [key, value] of Object.entries(data)) {
for (const prefix of prefixes) {
if (key.startsWith(prefix)) {
filtered[key] = value;
break;
}
}
}
return filtered;
};
exports.dumpChainDB = async function dumpChainDB(chaindb, prefixes) {
return exports.dumpDB(chaindb.db, prefixes);
};
/**
* @param {bdb.DB} ldb
* @param {Object} options
* @param {Object} options.before - key value pairs to check before.
* @param {Object} options.after - key value pairs to check.
* @param {Boolean} options.throw - throw on error.
* @param {Boolean} options.bail - bail on first error.
* @param {Boolean} options.logErrors - log errors.
* @returns {Promise<String[]>} - errors.
*/
exports.checkEntries = async function checkEntries(ldb, options) {
const errors = [];
options.before = options.before || {};
options.after = options.after || {};
for (const [key, value] of Object.entries(options.after)) {
if (errors.length > 0 && options.bail) {
if (options.throw)
throw new Error(errors[0]);
break;
}
const bkey = Buffer.from(key, 'hex');
const bvalue = Buffer.from(value, 'hex');
const stored = await ldb.get(bkey);
if (!stored) {
errors.push(`Value for ${key} not found in db, expected: ${value}`);
continue;
}
if (!bvalue.equals(stored)) {
errors.push(`Value for ${key}: ${stored.toString('hex')} does not match expected: ${value}`);
continue;
}
}
// check that entries have been removed.
for (const [key] of Object.entries(options.before)) {
// if after also has this key, skip.
if (options.after[key] != null)
continue;
const bkey = Buffer.from(key, 'hex');
const stored = await ldb.get(bkey);
if (stored) {
errors.push(`Value for ${key}: ${stored.toString('hex')} should have been removed.`);
continue;
}
}
if (options.logErrors && errors.length !== 0) {
console.error(
JSON.stringify(errors, null, 2)
);
}
if (errors.length > 0 && options.throw)
throw new Error(`Check entries failed with ${errors.length} errors.`);
return errors;
};
/**
* @param {bdb.DB} ldb
* @param {String[]} prefixes
* @param {Object} options
* @param {Object} options.after - key value pairs to check.
* @param {Boolean} options.throw - throw on error.
* @param {Boolean} options.bail - bail on first error.
* @param {Boolean} options.logErrors - log errors.
* @returns {Promise<String[]>} - errors.
*/
exports.checkExactEntries = async function checkExactEntries(ldb, prefixes, options) {
const dumped = await exports.dumpDB(ldb, prefixes);
const after = exports.filteredObject(options.after, prefixes);
const checks = new Set(Object.keys(after));
const errors = [];
for (const [key, value] of Object.entries(dumped)) {
if (errors.length > 0 && options.bail) {
if (options.throw)
throw new Error(errors[0]);
break;
}
if (!checks.has(key)) {
errors.push(`Unexpected key found in db: ${key}`);
continue;
}
if (value !== after[key]) {
errors.push(`Value for ${key}: ${value} does not match expected: ${after[key]}`);
continue;
}
checks.delete(key);
}
if (checks.size > 0) {
for (const key of checks) {
errors.push(`Expected key ${key} not found in db.`);
}
}
if (options.logErrors && errors.length !== 0) {
console.error(
JSON.stringify(errors, null, 2)
);
}
if (errors.length > 0 && options.throw)
throw new Error(`Check exact entries failed with ${errors.length} errors.`);
return errors;
};
exports.fillEntries = async function fillEntries(ldb, data) {
const batch = await ldb.batch();
for (const [key, value] of Object.entries(data)) {
const bkey = Buffer.from(key, 'hex');
const bvalue = Buffer.from(value, 'hex');
batch.put(bkey, bvalue);
}
await batch.write();
};
exports.writeVersion = function writeVersion(b, key, name, version) {
const value = Buffer.alloc(name.length + 4);
value.write(name, 0, 'ascii');
value.writeUInt32LE(version, name.length);
b.put(key, value);
};
exports.getVersion = function getVersion(data, name) {
const error = 'version mismatch';
if (data.length !== name.length + 4)
throw new Error(error);
if (data.toString('ascii', 0, name.length) !== name)
throw new Error(error);
return data.readUInt32LE(name.length);
};
exports.checkVersion = async function checkVersion(ldb, versionDBKey, expectedVersion) {
const data = await ldb.get(versionDBKey);
const version = exports.getVersion(data, 'wallet');
assert.strictEqual(version, expectedVersion);
};
// Chain generation
const REGTEST_TIME = 1580745078;
const getBlockTime = height => REGTEST_TIME + (height * 10 * 60);
/**
* Create deterministic block.
* @param {Object} options
* @param {Chain} options.chain
* @param {Miner} options.miner
* @param {ChainEntry} options.tip
* @param {Address} options.address
* @param {Number} options.txno
* @returns {BlockTemplate}
*/
exports.createBlock = async function createBlock(options) {
const {
chain,
miner,
tip,
address,
txno
} = options;
const version = await chain.computeBlockVersion(tip);
const mtp = await chain.getMedianTime(tip);
const time = getBlockTime(tip.height + 1);
const state = await chain.getDeployments(time, tip);
const target = await chain.getTarget(time, tip);
const root = chain.db.treeRoot();
const attempt = new BlockTemplate({
prevBlock: tip.hash,
treeRoot: root,
reservedRoot: consensus.ZERO_HASH,
height: tip.height + 1,
version: version,
time: time,
bits: target,
mtp: mtp,
flags: state.flags,
address: address,
coinbaseFlags: Buffer.from('Miner for data gen', 'ascii'),
interval: miner.network.halvingInterval,
weight: miner.options.reservedWeight,
sigops: miner.options.reservedSigops
});
miner.assemble(attempt);
const _createCB = attempt.createCoinbase.bind(attempt);
attempt.createCoinbase = function createCoinbase() {
const cb = _createCB();
const wit = Buffer.alloc(8);
const id = txno;
// make txs deterministic
wit.writeUInt32LE(id, 0, true);
cb.inputs[0].sequence = id;
cb.inputs[0].witness.setData(1, wit);
cb.refresh();
return cb;
};
return attempt;
};