1773 lines
44 KiB
JavaScript
1773 lines
44 KiB
JavaScript
/*!
|
|
* wallet/migrations.js - wallet db migrations for hsd
|
|
* Copyright (c) 2021, Nodari Chkuaselidze (MIT License).
|
|
* https://github.com/handshake-org/hsd
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('bsert');
|
|
const Logger = require('blgr');
|
|
const bdb = require('bdb');
|
|
const bio = require('bufio');
|
|
const {BufferSet} = require('buffer-map');
|
|
const LRU = require('blru');
|
|
const HDPublicKey = require('../hd/public');
|
|
const binary = require('../utils/binary');
|
|
const {encoding} = bio;
|
|
const Network = require('../protocol/network');
|
|
const consensus = require('../protocol/consensus');
|
|
const Coin = require('../primitives/coin');
|
|
const Outpoint = require('../primitives/outpoint');
|
|
const Script = require('../script/script');
|
|
const TX = require('../primitives/tx');
|
|
const Account = require('./account');
|
|
const WalletKey = require('./walletkey');
|
|
const Path = require('./path');
|
|
const MapRecord = require('./records').MapRecord;
|
|
const AbstractMigration = require('../migrations/migration');
|
|
const migrator = require('../migrations/migrator');
|
|
const {
|
|
MigrationResult,
|
|
MigrationContext,
|
|
Migrator,
|
|
types,
|
|
oldLayout
|
|
} = migrator;
|
|
const layouts = require('./layout');
|
|
const wlayout = layouts.wdb;
|
|
|
|
/** @typedef {migrator.types} MigrationType */
|
|
/** @typedef {import('../migrations/state')} MigrationState */
|
|
/** @typedef {ReturnType<bdb.DB['batch']>} Batch */
|
|
/** @typedef {ReturnType<bdb.DB['bucket']>} Bucket */
|
|
/** @typedef {import('./walletdb')} WalletDB */
|
|
/** @typedef {import('../types').Hash} Hash */
|
|
/** @typedef {import('./txdb').BlockExtraInfo} BlockExtraInfo */
|
|
|
|
/**
|
|
* Switch to new migrations layout.
|
|
*/
|
|
|
|
class MigrateMigrations extends AbstractMigration {
|
|
/**
|
|
* Create migrations migration.
|
|
* @param {WalletMigratorOptions} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-migrate');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateMigrations.layout();
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @param {WalletMigrationContext} ctx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b, ctx) {
|
|
this.logger.info('Migrating migrations..');
|
|
let nextMigration = 1;
|
|
|
|
if (await this.ldb.get(this.layout.oldLayout.wdb.M.encode(0))) {
|
|
b.del(this.layout.oldLayout.wdb.M.encode(0));
|
|
nextMigration = 2;
|
|
}
|
|
|
|
this.db.writeVersion(b, 1);
|
|
|
|
ctx.state.version = 0;
|
|
ctx.state.nextMigration = nextMigration;
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Migrate wallet migrations',
|
|
description: 'Wallet migration layout has changed.'
|
|
};
|
|
}
|
|
|
|
static layout() {
|
|
return {
|
|
oldLayout: {
|
|
wdb: {
|
|
M: bdb.key('M', ['uint32'])
|
|
}
|
|
},
|
|
newLayout: {
|
|
wdb: {
|
|
M: bdb.key('M')
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run change address migration.
|
|
* Applies to WalletDB v0
|
|
*/
|
|
|
|
class MigrateChangeAddress extends AbstractMigration {
|
|
/**
|
|
* Create change address migration object.
|
|
* @constructor
|
|
* @param {WalletMigratorOptions} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-change-address');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateChangeAddress.layout();
|
|
}
|
|
|
|
/**
|
|
* Migration and check for the change address
|
|
* are done in the same step.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @param {WalletMigrationContext} ctx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b, ctx) {
|
|
const wlayout = this.layout.wdb;
|
|
const wids = await this.ldb.keys({
|
|
gte: wlayout.W.min(),
|
|
lte: wlayout.W.max(),
|
|
parse: key => wlayout.W.decode(key)[0]
|
|
});
|
|
|
|
let total = 0;
|
|
for (const wid of wids) {
|
|
this.logger.info('Checking wallet (wid=%d).', wid);
|
|
total += await this.migrateWallet(b, wid);
|
|
}
|
|
|
|
if (total > 0)
|
|
ctx.pending.rescan = true;
|
|
}
|
|
|
|
/**
|
|
* @param {Batch} b
|
|
* @param {Number} wid
|
|
* @returns {Promise<Number>}
|
|
*/
|
|
|
|
async migrateWallet(b, wid) {
|
|
const accounts = this.ldb.iterator({
|
|
gte: this.layout.wdb.a.min(wid),
|
|
lte: this.layout.wdb.a.max(wid),
|
|
values: true
|
|
});
|
|
|
|
let total = 0;
|
|
for await (const {key, value} of accounts) {
|
|
const [awid, aindex] = this.layout.wdb.a.decode(key);
|
|
const name = await this.ldb.get(this.layout.wdb.n.encode(wid, aindex));
|
|
assert(awid === wid);
|
|
const br = bio.read(value);
|
|
const initialized = br.readU8();
|
|
|
|
if (!initialized)
|
|
continue;
|
|
|
|
const type = br.readU8();
|
|
const m = br.readU8();
|
|
const n = br.readU8();
|
|
br.seek(4); // skip receive
|
|
const changeDepth = br.readU32();
|
|
const lookahead = br.readU8();
|
|
const accountKey = this.readKey(br);
|
|
const count = br.readU8();
|
|
assert(br.left() === count * 74);
|
|
|
|
const keys = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const key = this.readKey(br);
|
|
const cmp = (a, b) => a.compare(b);
|
|
binary.insert(keys, key, cmp, true);
|
|
}
|
|
|
|
for (let i = 0; i < changeDepth + lookahead; i++) {
|
|
const key = this.deriveKey({
|
|
accountName: name,
|
|
accountIndex: aindex,
|
|
accountKey: accountKey,
|
|
type: type,
|
|
m: m,
|
|
n: n,
|
|
branch: 1,
|
|
index: i,
|
|
keys: keys
|
|
});
|
|
|
|
const path = key.toPath();
|
|
|
|
if (!await this.hasPath(wid, path.hash)) {
|
|
await this.savePath(b, wid, path);
|
|
total += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options
|
|
* @returns {WalletKey}
|
|
*/
|
|
|
|
deriveKey(options) {
|
|
const key = options.accountKey.derive(options.branch).derive(options.index);
|
|
const wkey = new WalletKey();
|
|
wkey.keyType = Path.types.HD;
|
|
wkey.name = options.accountName;
|
|
wkey.account = options.accountIndex;
|
|
wkey.branch = options.branch;
|
|
wkey.index = options.index;
|
|
wkey.publicKey = key.publicKey;
|
|
|
|
const keys = [];
|
|
switch (options.type) {
|
|
case Account.types.PUBKEYHASH:
|
|
break;
|
|
|
|
case Account.types.MULTISIG:
|
|
keys.push(wkey.publicKey);
|
|
|
|
for (const shared of options.keys) {
|
|
const key = shared.derive(options.branch).derive(options.index);
|
|
keys.push(key.publicKey);
|
|
}
|
|
|
|
wkey.script = Script.fromMultisig(options.m, options.n, keys);
|
|
|
|
break;
|
|
}
|
|
|
|
return wkey;
|
|
}
|
|
|
|
/**
|
|
* @param {bio.BufferReader} br
|
|
* @returns {HDPublicKey}
|
|
*/
|
|
|
|
readKey(br) {
|
|
const key = new HDPublicKey();
|
|
key.depth = br.readU8();
|
|
key.parentFingerPrint = br.readU32BE();
|
|
key.childIndex = br.readU32BE();
|
|
key.chainCode = br.readBytes(32);
|
|
key.publicKey = br.readBytes(33);
|
|
return key;
|
|
}
|
|
|
|
/**
|
|
* @param {Number} wid
|
|
* @param {Hash} hash
|
|
* @returns {Promise<Boolean>}
|
|
*/
|
|
|
|
async hasPath(wid, hash) {
|
|
return this.ldb.has(this.layout.wdb.P.encode(wid, hash));
|
|
}
|
|
|
|
/**
|
|
* @param {Batch} b
|
|
* @param {Number} wid
|
|
* @param {Path} path
|
|
*/
|
|
|
|
async savePath(b, wid, path) {
|
|
const wlayout = this.layout.wdb;
|
|
|
|
const data = await this.ldb.get(wlayout.p.encode(path.hash));
|
|
/** @type {MapRecord} */
|
|
const map = data ? MapRecord.decode(data) : new MapRecord();
|
|
|
|
map.add(wid);
|
|
b.put(wlayout.p.encode(path.hash), map.encode());
|
|
b.put(wlayout.P.encode(wid, path.hash), path.encode());
|
|
b.put(wlayout.r.encode(wid, path.account, path.hash), null);
|
|
}
|
|
|
|
/**
|
|
* Return info about the migration.
|
|
*/
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Change address migration',
|
|
description: 'Wallet is corrupted.'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get layout that migration is going to affect.
|
|
* @returns {Object}
|
|
*/
|
|
|
|
static layout() {
|
|
return {
|
|
wdb: {
|
|
// W[wid] -> wallet id
|
|
W: bdb.key('W', ['uint32']),
|
|
|
|
// n[wid][index] -> account name
|
|
n: bdb.key('n', ['uint32', 'uint32']),
|
|
|
|
// a[wid][index] -> account
|
|
a: bdb.key('a', ['uint32', 'uint32']),
|
|
|
|
// p[addr-hash] -> address->wid map
|
|
p: bdb.key('p', ['hash']),
|
|
|
|
// P[wid][addr-hash] -> path data
|
|
P: bdb.key('P', ['uint32', 'hash']),
|
|
|
|
// r[wid][index][addr-hash] -> dummy (addr by account)
|
|
r: bdb.key('r', ['uint32', 'uint32', 'hash'])
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate account for new lookahead entry.
|
|
* Applies to WalletDB v1
|
|
*/
|
|
|
|
class MigrateAccountLookahead extends AbstractMigration {
|
|
/**
|
|
* Create migration object.
|
|
* @param {WalletMigratorOptions} options
|
|
*/
|
|
|
|
constructor (options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-account-lookahead');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateAccountLookahead.layout();
|
|
}
|
|
|
|
/**
|
|
* We always migrate account.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b) {
|
|
const wlayout = this.layout.wdb;
|
|
const wids = await this.ldb.keys({
|
|
gte: wlayout.W.min(),
|
|
lte: wlayout.W.max(),
|
|
parse: key => wlayout.W.decode(key)[0]
|
|
});
|
|
|
|
for (const wid of wids)
|
|
await this.migrateWallet(b, wid);
|
|
|
|
this.db.writeVersion(b, 2);
|
|
}
|
|
|
|
/**
|
|
* @param {Batch} b
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateWallet(b, wid) {
|
|
const wlayout = this.layout.wdb;
|
|
const accounts = await this.ldb.keys({
|
|
gte: wlayout.a.min(wid),
|
|
lte: wlayout.a.max(wid),
|
|
parse: key => wlayout.a.decode(key)[1]
|
|
});
|
|
|
|
for (const accID of accounts) {
|
|
const key = wlayout.a.encode(wid, accID);
|
|
const rawAccount = await this.ldb.get(key);
|
|
const newRaw = this.accountEncode(rawAccount);
|
|
b.put(key, newRaw);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Buffer} raw
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
accountEncode(raw) {
|
|
// flags, type, m, n, receiveDepth, changeDepth
|
|
const preLen = 1 + 1 + 1 + 1 + 4 + 4;
|
|
const pre = raw.slice(0, preLen);
|
|
const lookahead = raw.slice(preLen, preLen + 1);
|
|
const post = raw.slice(preLen + 1);
|
|
const newLookahead = Buffer.alloc(4, 0x00);
|
|
|
|
encoding.writeU32(newLookahead, lookahead[0], 0);
|
|
|
|
return Buffer.concat([
|
|
pre,
|
|
newLookahead,
|
|
post
|
|
]);
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Account lookahead migration',
|
|
description: 'Account lookahead now supports up to 2^32 - 1'
|
|
};
|
|
}
|
|
|
|
static layout() {
|
|
return {
|
|
wdb: {
|
|
// W[wid] -> wallet id
|
|
W: bdb.key('W', ['uint32']),
|
|
|
|
// a[wid][index] -> account
|
|
a: bdb.key('a', ['uint32', 'uint32'])
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
class MigrateTXDBBalances extends AbstractMigration {
|
|
/**
|
|
* Create TXDB Balance migration object.
|
|
* @param {WalletMigratorOptions} options
|
|
* @constructor
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-txdb-balance');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
}
|
|
|
|
/**
|
|
* We always migrate.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @param {WalletMigrationContext} [ctx]
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b, ctx) {
|
|
ctx.pending.recalculateTXDB = true;
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'TXDB balance refresh',
|
|
description: 'Refresh balances for TXDB after txdb updates'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies to WalletDB v2
|
|
* Migrate bid reveal entries.
|
|
* - Adds height to the blind bid entries.
|
|
* - NOTE: This can not be recovered if the bid is not owned by the wallet.
|
|
* Wallet does not store transactions for not-owned bids.
|
|
* - Add Bid Outpoint information to the reveal (BidReveal) entries.
|
|
* - NOTE: This information can not be recovered for not-owned reveals.
|
|
* Wallet does not store transactions for not-owned reveals.
|
|
* - Add new BID -> REVEAL index. (layout.E)
|
|
* - NOTE: This information can not be recovered for not-owned reveals.
|
|
* Wallet does not store transactions for not-owned reveals.
|
|
*
|
|
*/
|
|
|
|
class MigrateBidRevealEntries extends AbstractMigration {
|
|
/**
|
|
* Create Bid Reveal Entries migration object.
|
|
* @param {WalletMigratorOptions} options
|
|
* @constructor
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-bid-reveal-entries');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateBidRevealEntries.layout();
|
|
}
|
|
|
|
/**
|
|
* We always migrate.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b) {
|
|
/** @type {Number[]} */
|
|
const wids = await this.ldb.keys({
|
|
gte: wlayout.W.min(),
|
|
lte: wlayout.W.max(),
|
|
parse: key => wlayout.W.decode(key)[0]
|
|
});
|
|
|
|
for (const wid of wids) {
|
|
await this.migrateReveals(wid);
|
|
await this.migrateBids(wid);
|
|
}
|
|
|
|
this.db.writeVersion(b, 3);
|
|
}
|
|
|
|
/**
|
|
* Migrate reveals and index Bid2Reveal
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateReveals(wid) {
|
|
const txlayout = this.layout.txdb;
|
|
const prefix = txlayout.prefix.encode(wid);
|
|
const bucket = this.ldb.bucket(prefix);
|
|
const emptyOutpoint = new Outpoint();
|
|
|
|
const reveals = bucket.iterator({
|
|
gte: txlayout.B.min(),
|
|
lte: txlayout.B.max(),
|
|
values: true
|
|
});
|
|
|
|
for await (const {key, value} of reveals) {
|
|
const b = bucket.batch();
|
|
const [nameHash, txHash, txIndex] = txlayout.B.decode(key);
|
|
const nameLen = value[0];
|
|
const totalOld = nameLen + 1 + 13;
|
|
const totalNew = nameLen + 1 + 13 + 36;
|
|
|
|
// allow migration to be interrupted in the middle.
|
|
assert(value.length === totalOld || value.length === totalNew);
|
|
|
|
// skip if already migrated.
|
|
if (value.length === totalNew)
|
|
continue;
|
|
|
|
const owned = value[nameLen + 1 + 12];
|
|
const rawTXRecord = await bucket.get(txlayout.t.encode(txHash));
|
|
assert(owned && rawTXRecord || !owned);
|
|
|
|
// We can not index the bid link and bid2reveal index if
|
|
// the transaction is not owned by the wallet.
|
|
// But we need to put null outpoint to the reveal for serialization.
|
|
if (!owned) {
|
|
const newReveal = Buffer.concat([value, emptyOutpoint.encode()]);
|
|
assert(newReveal.length === totalNew);
|
|
b.put(key, newReveal);
|
|
await b.write();
|
|
continue;
|
|
}
|
|
|
|
const reader = bio.read(rawTXRecord);
|
|
/** @type {TX} */
|
|
const tx = TX.fromReader(reader);
|
|
assert(tx.inputs[txIndex]);
|
|
|
|
const bidPrevout = tx.inputs[txIndex].prevout;
|
|
const bidKey = txlayout.i.encode(
|
|
nameHash, bidPrevout.hash, bidPrevout.index);
|
|
const bidRecord = await bucket.get(bidKey);
|
|
// ensure bid exists.
|
|
assert(bidRecord);
|
|
|
|
const newReveal = Buffer.concat([value, bidPrevout.encode()]);
|
|
assert(newReveal.length === totalNew);
|
|
// update reveal with bid outpoint.
|
|
b.put(key, newReveal);
|
|
// index bid to reveal.
|
|
b.put(txlayout.E.encode(nameHash, bidPrevout.hash, bidPrevout.index),
|
|
(new Outpoint(txHash, txIndex)).encode());
|
|
await b.write();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate bids, add height to the entries.
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateBids(wid) {
|
|
const txlayout = this.layout.txdb;
|
|
const prefix = txlayout.prefix.encode(wid);
|
|
const bucket = this.ldb.bucket(prefix);
|
|
|
|
const bids = bucket.iterator({
|
|
gte: txlayout.i.min(),
|
|
lte: txlayout.i.max(),
|
|
values: true
|
|
});
|
|
|
|
/**
|
|
* @param {Buffer} blindBid
|
|
* @param {Number} height
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
const reencodeBlindBid = (blindBid, height) => {
|
|
const nameLen = blindBid[0];
|
|
const totalOld = nameLen + 1 + 41;
|
|
const totalNew = nameLen + 1 + 41 + 4;
|
|
assert(blindBid.length === totalOld);
|
|
|
|
const newBlindBid = Buffer.alloc(totalNew);
|
|
// copy everything before expected height place.
|
|
blindBid.copy(newBlindBid, 0, 0, totalOld - 1);
|
|
// copy height.
|
|
bio.encoding.writeU32(newBlindBid, height, totalOld - 1);
|
|
// copy last byte (owned flag).
|
|
blindBid.copy(newBlindBid, totalNew - 1, totalOld - 1);
|
|
|
|
return newBlindBid;
|
|
};
|
|
|
|
for await (const {key, value} of bids) {
|
|
const b = bucket.batch();
|
|
const [,txHash] = txlayout.i.decode(key);
|
|
const nameLen = value[0];
|
|
const totalNew = nameLen + 1 + 41 + 4;
|
|
|
|
// allow migration to be interrupted in the middle.
|
|
if (totalNew === value.length)
|
|
continue;
|
|
|
|
const owned = value[nameLen + 1 + 40];
|
|
if (!owned) {
|
|
const height = 0xffffffff; // -1
|
|
const newValue = reencodeBlindBid(value, height);
|
|
b.put(key, newValue);
|
|
await b.write();
|
|
continue;
|
|
}
|
|
|
|
const rawTXRecord = await bucket.get(txlayout.t.encode(txHash));
|
|
assert(rawTXRecord);
|
|
|
|
const br = bio.read(rawTXRecord);
|
|
TX.fromReader(br);
|
|
// skip mtime.
|
|
br.seek(4);
|
|
|
|
const hasBlock = br.readU8() === 1;
|
|
// We only index the bid in blocks, not in mempool.
|
|
assert(hasBlock);
|
|
|
|
// skip hash.
|
|
br.seek(32);
|
|
const height = br.readU32();
|
|
const newValue = reencodeBlindBid(value, height);
|
|
b.put(key, newValue);
|
|
|
|
await b.write();
|
|
}
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Bid reveal entries migration',
|
|
description: 'Migrate bids and reveals to link each other.'
|
|
};
|
|
}
|
|
|
|
static layout() {
|
|
return {
|
|
wdb: {
|
|
V: bdb.key('V'),
|
|
// W[wid] -> wallet id
|
|
W: bdb.key('W', ['uint32'])
|
|
},
|
|
txdb: {
|
|
prefix: bdb.key('t', ['uint32']),
|
|
// t[tx-hash] -> extended tx (Read only)
|
|
t: bdb.key('t', ['hash256']),
|
|
// i[name-hash][tx-hash][index] -> txdb.BlindBid
|
|
i: bdb.key('i', ['hash256', 'hash256', 'uint32']),
|
|
// B[name-hash][tx-hash][index] -> txdb.BidReveal
|
|
B: bdb.key('B', ['hash256', 'hash256', 'uint32']),
|
|
// E[name-hash][tx-hash][index] -> bid to reveal out.
|
|
E: bdb.key('E', ['hash256', 'hash256', 'uint32'])
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies to WalletDB v3.
|
|
* Migrate TX Count Time Index.
|
|
* - Adds time to the block entries (layout.wdb.h)
|
|
* - ...
|
|
*/
|
|
|
|
class MigrateTXCountTimeIndex extends AbstractMigration {
|
|
/**
|
|
* Create TX Count Time Index migration object.
|
|
* @param {WalletMigratorOptions} options
|
|
* @constructor
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context(
|
|
'wallet-migration-tx-count-time-index');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateTXCountTimeIndex.layout();
|
|
|
|
this.headersBatchSize = 1000;
|
|
|
|
this.UNCONFIRMED_HEIGHT = 0xffffffff;
|
|
|
|
this.blockTimeCache = new LRU(50);
|
|
}
|
|
|
|
/**
|
|
* TX Count Time Index migration check.
|
|
* It will always migrate.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Migrate TX Count Time Index.
|
|
* Needs fullnode to be available via http.
|
|
* @param {Batch} b
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b) {
|
|
await this.migrateHeaders();
|
|
|
|
const wlayout = this.layout.wdb;
|
|
const wids = await this.ldb.keys({
|
|
gte: wlayout.W.min(),
|
|
lte: wlayout.W.max(),
|
|
parse: key => wlayout.W.decode(key)[0]
|
|
});
|
|
|
|
for (const wid of wids) {
|
|
await this.migrateConfirmed(wid);
|
|
await this.migrateUnconfirmed(wid);
|
|
await this.verifyTimeEntries(wid);
|
|
}
|
|
|
|
this.db.writeVersion(b, 4);
|
|
}
|
|
|
|
/**
|
|
* Migrate headers in the walletdb.
|
|
* Add time entry.
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateHeaders() {
|
|
const iter = this.ldb.iterator({
|
|
gte: this.layout.wdb.h.min(),
|
|
lte: this.layout.wdb.h.max(),
|
|
values: true
|
|
});
|
|
|
|
let parent = this.ldb.batch();
|
|
let total = 0;
|
|
|
|
for await (const {key, value} of iter) {
|
|
const [height] = this.layout.wdb.h.decode(key);
|
|
const hash = value.slice(0, 32);
|
|
|
|
// Skip if already migrated.
|
|
if (value.length === 40)
|
|
continue;
|
|
|
|
const entry = await this.db.client.getBlockHeader(hash.toString('hex'));
|
|
|
|
if (!entry)
|
|
throw new Error('Could not get entry from the chain.');
|
|
|
|
assert(entry.height === height);
|
|
|
|
const out = Buffer.allocUnsafe(32 + 8);
|
|
bio.writeBytes(out, hash, 0);
|
|
bio.writeU64(out, entry.time, 32);
|
|
|
|
parent.put(key, out);
|
|
|
|
if (++total % this.headersBatchSize === 0) {
|
|
await parent.write();
|
|
parent = this.ldb.batch();
|
|
}
|
|
}
|
|
|
|
await parent.write();
|
|
}
|
|
|
|
/**
|
|
* Migrate confirmed transactions.
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateConfirmed(wid) {
|
|
const txlayout = this.layout.txdb;
|
|
const bucket = this.ldb.bucket(txlayout.prefix.encode(wid));
|
|
|
|
const lastHeight = await bucket.range({
|
|
gte: txlayout.Ot.min(),
|
|
lt: txlayout.Ot.min(this.UNCONFIRMED_HEIGHT),
|
|
reverse: true,
|
|
limit: 1
|
|
});
|
|
|
|
let startHeight = 0;
|
|
|
|
if (lastHeight.length !== 0) {
|
|
const [height] = txlayout.Ot.decode(lastHeight[0].key);
|
|
startHeight = height;
|
|
}
|
|
|
|
const rawBlockRecords = bucket.iterator({
|
|
gte: txlayout.b.encode(startHeight),
|
|
lte: txlayout.b.max(),
|
|
values: true
|
|
});
|
|
|
|
for await (const {key, value: rawBlockRecord} of rawBlockRecords) {
|
|
const height = txlayout.b.decode(key)[0];
|
|
const hash = rawBlockRecord.slice(0, 32);
|
|
const blockTime = encoding.readU32(rawBlockRecord, 32 + 4);
|
|
const txCount = encoding.readU32(rawBlockRecord, 32 + 4 + 4);
|
|
const medianTime = await this.getMedianTime(height, hash);
|
|
assert(medianTime, 'Could not get medianTime');
|
|
|
|
const hashes = new BufferSet();
|
|
let count = 0;
|
|
|
|
for (let i = 0; i < txCount; i++) {
|
|
const pos = 32 + 4 + 4 + 4 + i * 32;
|
|
const txHash = encoding.readBytes(rawBlockRecord, pos, 32);
|
|
const block = {
|
|
height,
|
|
time: blockTime
|
|
};
|
|
|
|
if (hashes.has(txHash))
|
|
continue;
|
|
|
|
hashes.add(txHash);
|
|
|
|
/** @type {BlockExtraInfo} */
|
|
const extra = {
|
|
medianTime: medianTime,
|
|
txIndex: count++
|
|
};
|
|
|
|
await this.migrateTX(bucket, wid, txHash, block, extra);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate unconfirmed transactions.
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateUnconfirmed(wid) {
|
|
const txlayout = this.layout.txdb;
|
|
const bucket = this.ldb.bucket(txlayout.prefix.encode(wid));
|
|
|
|
// The only transactions remaining in layout.m should be unconfirmed.
|
|
const txsByOldTime = bucket.iterator({
|
|
gte: txlayout.m.min(),
|
|
lte: txlayout.m.max()
|
|
});
|
|
|
|
// Here we don't need to skip as the old time index
|
|
// is getting cleaned up in the same migration.
|
|
for await (const {key} of txsByOldTime) {
|
|
const [, txHash] = txlayout.m.decode(key);
|
|
await this.migrateTX(bucket, wid, txHash);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate specific transaction. Index by count and time.
|
|
* @param {Bucket} bucket
|
|
* @param {Number} wid
|
|
* @param {Hash} txHash - txhash.
|
|
* @param {Object} [block]
|
|
* @param {Number} block.height
|
|
* @param {Number} block.time
|
|
* @param {BlockExtraInfo} [extra]
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateTX(bucket, wid, txHash, block, extra) {
|
|
const txlayout = this.layout.txdb;
|
|
|
|
// Skip if already migrated.
|
|
if (await bucket.get(txlayout.Oc.encode(txHash)))
|
|
return;
|
|
|
|
const batch = bucket.batch();
|
|
/** @type {Set<Number>} */
|
|
const accounts = new Set();
|
|
const rawTXRecord = await bucket.get(txlayout.t.encode(txHash));
|
|
|
|
// Skip if we have not recorded the transaction. This can happen
|
|
// for bids, reveals, etc. that are not owned by the wallet.
|
|
if (!rawTXRecord)
|
|
return;
|
|
|
|
const recordReader = bio.read(rawTXRecord);
|
|
/** @type {TX} */
|
|
const tx = TX.fromReader(recordReader);
|
|
const mtime = recordReader.readU32();
|
|
|
|
// Inputs, whether in a block or for unconfirmed transactions,
|
|
// will be indexed as spent inputs. We can leverage
|
|
// these entries to determine related accounts.
|
|
if (!tx.isCoinbase()) {
|
|
// inputs that were spent by the wallet.
|
|
const spentCoins = bucket.iterator({
|
|
gte: txlayout.d.min(txHash),
|
|
lte: txlayout.d.max(txHash),
|
|
values: true
|
|
});
|
|
|
|
for await (const {value} of spentCoins) {
|
|
const coin = Coin.decode(value);
|
|
const account = await this.getAccount(wid, coin);
|
|
assert(account != null);
|
|
accounts.add(account);
|
|
}
|
|
}
|
|
|
|
// For outputs, we don't need to bother with coins at all.
|
|
// We can gather paths directly from the outputs.
|
|
for (const output of tx.outputs) {
|
|
const account = await this.getAccount(wid, output);
|
|
|
|
if (account != null)
|
|
accounts.add(account);
|
|
}
|
|
|
|
// remove old indexes.
|
|
batch.del(txlayout.m.encode(mtime, txHash));
|
|
|
|
for (const acct of accounts)
|
|
batch.del(txlayout.M.encode(acct, mtime, txHash));
|
|
|
|
if (!block) {
|
|
// Expanded the following code.
|
|
// Add indexing for unconfirmed transactions.
|
|
// await this.addCountAndTimeIndexUnconfirmed(b, state.accounts, hash);
|
|
const rawLastUnconfirmedIndex = await bucket.get(txlayout.Ol.encode());
|
|
let lastUnconfirmedIndex = 0;
|
|
|
|
if (rawLastUnconfirmedIndex)
|
|
lastUnconfirmedIndex = encoding.readU32(rawLastUnconfirmedIndex, 0);
|
|
|
|
const height = this.UNCONFIRMED_HEIGHT;
|
|
const index = lastUnconfirmedIndex;
|
|
const count = { height, index };
|
|
|
|
batch.put(txlayout.Ot.encode(height, index), txHash);
|
|
batch.put(txlayout.Oc.encode(txHash), this.encodeTXCount(count));
|
|
|
|
batch.put(txlayout.Oe.encode(txHash), fromU32(mtime));
|
|
batch.put(txlayout.Om.encode(mtime, index, txHash));
|
|
|
|
for (const acct of accounts) {
|
|
batch.put(txlayout.OT.encode(acct, height, index), txHash);
|
|
batch.put(txlayout.OM.encode(acct, mtime, index, txHash));
|
|
}
|
|
|
|
batch.put(txlayout.Ol.encode(), fromU32(lastUnconfirmedIndex + 1));
|
|
|
|
await batch.write();
|
|
return;
|
|
}
|
|
|
|
// we have the block!
|
|
// await this.addCountAndTimeIndex(b, {
|
|
// accounts: state.accounts,
|
|
// hash,
|
|
// height: block.height,
|
|
// blockextra: extra
|
|
// });
|
|
const index = extra.txIndex;
|
|
const height = block.height;
|
|
const count = { height, index };
|
|
|
|
batch.put(txlayout.Ot.encode(height, index), txHash);
|
|
batch.put(txlayout.Oc.encode(txHash), this.encodeTXCount(count));
|
|
|
|
const time = extra.medianTime;
|
|
batch.put(txlayout.Oi.encode(time, height, index, txHash));
|
|
|
|
for (const acct of accounts) {
|
|
batch.put(txlayout.OT.encode(acct, height, index), txHash);
|
|
batch.put(txlayout.OI.encode(acct, time, height, index, txHash));
|
|
}
|
|
|
|
// await this.addTimeAndCountIndexUnconfirmedUndo(b, hash);
|
|
const rawLastUnconfirmedIndex = await bucket.get(txlayout.Ol.encode());
|
|
let lastUnconfirmedIndex = 0;
|
|
|
|
if (rawLastUnconfirmedIndex)
|
|
lastUnconfirmedIndex = encoding.readU32(rawLastUnconfirmedIndex, 0);
|
|
|
|
batch.put(txlayout.Oe.encode(txHash), fromU32(mtime));
|
|
batch.put(txlayout.Ou.encode(txHash), this.encodeTXCount({
|
|
height: this.UNCONFIRMED_HEIGHT,
|
|
index: lastUnconfirmedIndex
|
|
}));
|
|
|
|
batch.put(txlayout.Ol.encode(), fromU32(lastUnconfirmedIndex + 1));
|
|
await batch.write();
|
|
}
|
|
|
|
/**
|
|
* Get path for the coin.
|
|
* @param {Number} wid
|
|
* @param {Coin|Output} coin
|
|
* @returns {Promise<Number>} - account index
|
|
*/
|
|
|
|
async getAccount(wid, coin) {
|
|
const hash = coin.getHash();
|
|
|
|
if (!hash)
|
|
return null;
|
|
|
|
const rawPath = await this.ldb.get(this.layout.wdb.P.encode(wid, hash));
|
|
|
|
if (!rawPath)
|
|
return null;
|
|
|
|
const account = encoding.readU32(rawPath, 0);
|
|
return account;
|
|
}
|
|
|
|
/**
|
|
* Encode TXCount.
|
|
* @param {Object} txCount
|
|
* @param {Number} txCount.height
|
|
* @param {Number} txCount.index
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
encodeTXCount(txCount) {
|
|
const bw = bio.write(8);
|
|
bw.writeU32(txCount.height);
|
|
bw.writeU32(txCount.index);
|
|
return bw.render();
|
|
}
|
|
|
|
/**
|
|
* Get median time for the block.
|
|
* @param {Number} height
|
|
* @param {Hash} lastHash
|
|
* @returns {Promise<Number>}
|
|
*/
|
|
|
|
async getMedianTime(height, lastHash) {
|
|
const getBlockTime = async (bheight) => {
|
|
const cache = this.blockTimeCache.get(bheight);
|
|
|
|
if (cache != null)
|
|
return cache;
|
|
|
|
if (bheight < 0)
|
|
return null;
|
|
|
|
let time;
|
|
const data = await this.ldb.get(this.layout.wdb.h.encode(bheight));
|
|
|
|
if (!data) {
|
|
// Special case when txlayout.b exists, but txlayout.wdb.h does not.
|
|
// This can happen when walletDB is stopped during addBlock.
|
|
if (height !== bheight)
|
|
return null;
|
|
|
|
const header = await this.db.client.getBlockHeader(bheight);
|
|
|
|
if (!header)
|
|
return null;
|
|
|
|
// double check hash.
|
|
if (header.hash !== lastHash.toString('hex'))
|
|
throw new Error('Bad block time response.');
|
|
|
|
time = header.time;
|
|
} else {
|
|
time = encoding.readU64(data, 32);
|
|
}
|
|
|
|
this.blockTimeCache.set(bheight, time);
|
|
|
|
return time;
|
|
};
|
|
|
|
const timespan = consensus.MEDIAN_TIMESPAN;
|
|
const median = [];
|
|
|
|
let time = await getBlockTime(height);
|
|
|
|
for (let i = 0; i < timespan && time; i++) {
|
|
median.push(time);
|
|
|
|
time = await getBlockTime(height - i - 1);
|
|
}
|
|
|
|
median.sort((a, b) => a - b);
|
|
return median[median.length >>> 1];
|
|
}
|
|
|
|
/**
|
|
* Verify time entries have been removed.
|
|
* @param {Number} wid
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async verifyTimeEntries(wid) {
|
|
const txlayout = this.layout.txdb;
|
|
const bucket = this.ldb.bucket(txlayout.prefix.encode(wid));
|
|
const timeEntries = await bucket.range({
|
|
gte: txlayout.m.min(),
|
|
lte: txlayout.m.max()
|
|
});
|
|
|
|
assert(timeEntries.length === 0);
|
|
|
|
const timeEntriesByAcct = await bucket.range({
|
|
gte: txlayout.M.min(),
|
|
lte: txlayout.M.max()
|
|
});
|
|
|
|
assert(timeEntriesByAcct.length === 0);
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'TX Count Time Index migration',
|
|
description: 'Migrate TX data and index them for pagination'
|
|
};
|
|
}
|
|
|
|
static layout() {
|
|
return {
|
|
wdb: {
|
|
V: bdb.key('V'),
|
|
|
|
// h[height] -> block hash + time
|
|
h: bdb.key('h', ['uint32']),
|
|
|
|
// W[wid] -> wallet id
|
|
W: bdb.key('W', ['uint32']),
|
|
|
|
// P[wid][addr-hash] -> path data
|
|
P: bdb.key('P', ['uint32', 'hash'])
|
|
},
|
|
txdb: {
|
|
prefix: bdb.key('t', ['uint32']),
|
|
|
|
// We need this for spent inputs in blocks.
|
|
// d[tx-hash][index] -> undo coin
|
|
d: bdb.key('d', ['hash256', 'uint32']),
|
|
|
|
// these two are no longer used.
|
|
// m[time][tx-hash] -> dummy (tx by time)
|
|
m: bdb.key('m', ['uint32', 'hash256']),
|
|
// M[account][time][tx-hash] -> dummy (tx by time + account)
|
|
M: bdb.key('M', ['uint32', 'uint32', 'hash256']),
|
|
|
|
// This is not affected by the migration, but here for reference
|
|
// and time check.
|
|
// t[tx-hash] -> extended tx
|
|
t: bdb.key('t', ['hash256']),
|
|
|
|
// Confirmed.
|
|
// b[height] -> block record
|
|
b: bdb.key('b', ['uint32']),
|
|
|
|
// Count and Time Index.
|
|
// Latest unconfirmed Index.
|
|
Ol: bdb.key('Ol'),
|
|
|
|
// Transaction.
|
|
// z[height][index] -> tx hash (tx by count)
|
|
Ot: bdb.key('Ot', ['uint32', 'uint32']),
|
|
// Z[account][height][index] -> tx hash (tx by count + account)
|
|
OT: bdb.key('OT', ['uint32', 'uint32', 'uint32']),
|
|
// Oc[hash] -> count (count for tx)
|
|
Oc: bdb.key('Oc', ['hash256']),
|
|
// Ou[hash] -> undo count (unconfirmed count for tx)
|
|
Ou: bdb.key('Ou', ['hash256']),
|
|
|
|
// Unconfirmed.
|
|
// Om[time][count][hash] -> dummy (tx by time)
|
|
Om: bdb.key('Om', ['uint32', 'uint32', 'hash256']),
|
|
// OM[account][time][count][hash] -> dummy (tx by time + account)
|
|
OM: bdb.key('OM', ['uint32', 'uint32', 'uint32', 'hash256']),
|
|
// Oe[hash] -> undo time (unconfirmed time for tx)
|
|
Oe: bdb.key('Oe', ['hash256']),
|
|
|
|
// Confirmed.
|
|
// Oi[time][height][index][hash] -> dummy (tx by time)
|
|
Oi: bdb.key('Oi', ['uint32', 'uint32', 'uint32', 'hash256']),
|
|
|
|
// OI[account][time][height][index][hash] -> dummy(tx by time + account)
|
|
OI: bdb.key('OI', ['uint32', 'uint32', 'uint32', 'uint32', 'hash256'])
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
class MigrateMigrationStateV1 extends AbstractMigration {
|
|
/**
|
|
* Create Migration State migration object.
|
|
* @param {WalletMigratorOptions} options
|
|
* @constructor
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-migration-state-v1');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
}
|
|
|
|
/**
|
|
* We always migrate.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Migrate Migration State.
|
|
* @param {Batch} b
|
|
* @param {WalletMigrationContext} ctx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b, ctx) {
|
|
ctx.state.version = 1;
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Migrate Migration State',
|
|
description: 'Migrate migration state to v1'
|
|
};
|
|
}
|
|
}
|
|
|
|
class MigrateCoinSelection extends AbstractMigration {
|
|
/**
|
|
* @param {WalletMigratorOptions} options
|
|
* @constructor
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
/** @type {WalletMigratorOptions} */
|
|
this.options = options;
|
|
this.logger = options.logger.context('wallet-migration-coin-selection');
|
|
this.db = options.db;
|
|
this.ldb = options.ldb;
|
|
this.layout = MigrateCoinSelection.layout();
|
|
|
|
this.UNCONFIRMED_HEIGHT = 0xffffffff;
|
|
this.batchSize = 5000;
|
|
this.progress = {
|
|
wid: 0,
|
|
account: 0,
|
|
hash: consensus.ZERO_HASH,
|
|
index: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* We always migrate.
|
|
* @returns {Promise<MigrationType>}
|
|
*/
|
|
|
|
async check() {
|
|
return types.MIGRATE;
|
|
}
|
|
|
|
/**
|
|
* Actual migration
|
|
* @param {Batch} b
|
|
* @param {WalletMigrationContext} ctx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrate(b, ctx) {
|
|
const wlayout = this.layout.wdb;
|
|
const wids = await this.ldb.keys({
|
|
gte: wlayout.W.min(),
|
|
lte: wlayout.W.max(),
|
|
parse: key => wlayout.W.decode(key)[0]
|
|
});
|
|
|
|
await this.decodeProgress(ctx.state.inProgressData);
|
|
|
|
for (const wid of wids) {
|
|
if (wid < this.progress.wid) {
|
|
this.logger.debug(
|
|
'Skipping wallet %d (%d/%d), already migrated.',
|
|
wid, this.progress.wid, wids.length);
|
|
continue;
|
|
}
|
|
|
|
this.logger.info(
|
|
'Migrating wallet %d (%d/%d)',
|
|
wid, this.progress.wid, wids.length);
|
|
await this.migrateWallet(wid, ctx);
|
|
}
|
|
|
|
this.db.writeVersion(b, 5);
|
|
}
|
|
|
|
/**
|
|
* @param {Number} wid
|
|
* @param {WalletMigrationContext} ctx
|
|
* @returns {Promise}
|
|
*/
|
|
|
|
async migrateWallet(wid, ctx) {
|
|
const txlayout = this.layout.txdb;
|
|
const prefix = txlayout.prefix.encode(wid);
|
|
const bucket = this.ldb.bucket(prefix);
|
|
|
|
const min = txlayout.C.encode(
|
|
this.progress.account,
|
|
this.progress.hash,
|
|
this.progress.index
|
|
);
|
|
|
|
const coinsIter = bucket.iterator({
|
|
gte: min,
|
|
lte: txlayout.C.max()
|
|
});
|
|
|
|
let parent = bucket.batch();
|
|
let total = 0;
|
|
|
|
for await (const {key} of coinsIter) {
|
|
const [account, hash, index] = txlayout.C.decode(key);
|
|
const rawCoin = await bucket.get(txlayout.c.encode(hash, index));
|
|
const coin = Coin.decode(rawCoin);
|
|
|
|
if (coin.isUnspendable() || coin.covenant.isNonspendable())
|
|
continue;
|
|
|
|
if (coin.height === -1) {
|
|
// index coins by value
|
|
parent.put(txlayout.Su.encode(coin.value, hash, index), null);
|
|
parent.put(txlayout.SU.encode(account, coin.value, hash, index), null);
|
|
|
|
// index coins by height
|
|
parent.put(txlayout.Sh.encode(this.UNCONFIRMED_HEIGHT, hash, index),
|
|
null);
|
|
|
|
parent.put(
|
|
txlayout.SH.encode(account, this.UNCONFIRMED_HEIGHT, hash, index),
|
|
null
|
|
);
|
|
} else {
|
|
parent.put(txlayout.Sv.encode(coin.value, hash, index), null);
|
|
parent.put(txlayout.SV.encode(account, coin.value, hash, index), null);
|
|
|
|
parent.put(txlayout.Sh.encode(coin.height, hash, index), null);
|
|
parent.put(txlayout.SH.encode(account, coin.height, hash, index), null);
|
|
}
|
|
|
|
if (++total % this.batchSize === 0) {
|
|
// save progress
|
|
this.progress.wid = wid;
|
|
this.progress.account = account;
|
|
this.progress.hash = hash;
|
|
this.progress.index = index + 1;
|
|
|
|
ctx.state.inProgressData = this.encodeProgress();
|
|
ctx.writeState(parent.root());
|
|
|
|
await parent.write();
|
|
parent = bucket.batch();
|
|
}
|
|
};
|
|
|
|
this.progress.wid = wid + 1;
|
|
this.progress.account = 0;
|
|
this.progress.hash = consensus.ZERO_HASH;
|
|
this.progress.index = 0;
|
|
ctx.state.inProgressData = this.encodeProgress();
|
|
ctx.writeState(parent.root());
|
|
await parent.write();
|
|
}
|
|
|
|
/**
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
encodeProgress() {
|
|
const bw = bio.write(44);
|
|
bw.writeU32(this.progress.wid);
|
|
bw.writeU32(this.progress.account);
|
|
bw.writeBytes(this.progress.hash);
|
|
bw.writeU32(this.progress.index);
|
|
return bw.render();
|
|
}
|
|
|
|
/**
|
|
* Get migration info.
|
|
* @param {Buffer} data
|
|
* @return {Object}
|
|
*/
|
|
|
|
decodeProgress(data) {
|
|
if (data.length === 0)
|
|
return;
|
|
|
|
assert(data.length === 44);
|
|
|
|
const br = bio.read(data);
|
|
this.progress.wid = br.readU32();
|
|
this.progress.account = br.readU32();
|
|
this.progress.hash = br.readBytes(32);
|
|
this.progress.index = br.readU32();
|
|
}
|
|
|
|
static info() {
|
|
return {
|
|
name: 'Wallet Coin Selection Migration',
|
|
description: 'Reindex coins for better coin selection'
|
|
};
|
|
}
|
|
|
|
static layout() {
|
|
return {
|
|
wdb: {
|
|
V: bdb.key('V'),
|
|
|
|
// W[wid] -> wallet id
|
|
W: bdb.key('W', ['uint32'])
|
|
},
|
|
txdb: {
|
|
prefix: bdb.key('t', ['uint32']),
|
|
|
|
// Coins
|
|
c: bdb.key('c', ['hash256', 'uint32']),
|
|
C: bdb.key('C', ['uint32', 'hash256', 'uint32']),
|
|
d: bdb.key('d', ['hash256', 'uint32']),
|
|
s: bdb.key('s', ['hash256', 'uint32']),
|
|
|
|
// confirmed by Value
|
|
Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']),
|
|
// confirmed by account + Value
|
|
SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']),
|
|
// Unconfirmed by value
|
|
Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']),
|
|
// Unconfirmed by account + value
|
|
SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']),
|
|
// by height
|
|
Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']),
|
|
// by account + height
|
|
SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32'])
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wallet migration results.
|
|
* @alias module:blockchain.WalletMigrationResult
|
|
*/
|
|
|
|
class WalletMigrationResult extends MigrationResult {
|
|
constructor() {
|
|
super();
|
|
|
|
this.rescan = false;
|
|
this.recalculateTXDB = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @alias module:blockchain.WalletMigrationContext
|
|
*/
|
|
|
|
class WalletMigrationContext extends MigrationContext {
|
|
/**
|
|
* @param {WalletMigrator} migrator
|
|
* @param {MigrationState} state
|
|
* @param {WalletMigrationResult} pending
|
|
*/
|
|
|
|
constructor(migrator, state, pending) {
|
|
super(migrator, state, pending);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wallet Migrator
|
|
* @alias module:blockchain.WalletMigrator
|
|
*/
|
|
class WalletMigrator extends Migrator {
|
|
/**
|
|
* Create WalletMigrator object.
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
super(new WalletMigratorOptions(options));
|
|
|
|
this.logger = this.options.logger.context('wallet-migrations');
|
|
this.pending = new WalletMigrationResult();
|
|
this.flagError = 'Restart with '
|
|
+ `\`hsd --wallet-migrate=${this.lastMigration}\` or `
|
|
+ `\`hsw --migrate=${this.lastMigration}\`\n`
|
|
+ '(Full node may be required for rescan)';
|
|
}
|
|
|
|
/**
|
|
* Get list of migrations to run
|
|
* @returns {Promise<Set>}
|
|
*/
|
|
|
|
async getMigrationsToRun() {
|
|
const state = await this.getState();
|
|
const lastID = this.getLastMigrationID();
|
|
|
|
if (state.nextMigration > lastID)
|
|
return new Set();
|
|
|
|
const ids = new Set();
|
|
|
|
for (let i = state.nextMigration; i <= lastID; i++)
|
|
ids.add(i);
|
|
|
|
if (state.nextMigration === 0 && await this.ldb.get(oldLayout.M.encode(0)))
|
|
ids.delete(1);
|
|
|
|
return ids;
|
|
}
|
|
|
|
/**
|
|
* @param {MigrationState} state
|
|
* @returns {WalletMigrationContext}
|
|
*/
|
|
|
|
createContext(state) {
|
|
return new WalletMigrationContext(this, state, this.pending);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WalletMigratorOptions
|
|
* @alias module:wallet.WalletMigratorOptions
|
|
*/
|
|
|
|
class WalletMigratorOptions {
|
|
/**
|
|
* Create Wallet Migrator Options.
|
|
* @constructor
|
|
* @param {Object} options
|
|
*/
|
|
|
|
constructor(options) {
|
|
this.network = Network.primary;
|
|
this.logger = Logger.global;
|
|
|
|
this.migrations = WalletMigrator.migrations;
|
|
this.migrateFlag = -1;
|
|
|
|
this.dbVersion = 0;
|
|
/** @type {WalletDB} */
|
|
this.db = null;
|
|
/** @type {bdb.DB} */
|
|
this.ldb = null;
|
|
this.layout = layouts.wdb;
|
|
|
|
assert(options);
|
|
this.fromOptions(options);
|
|
}
|
|
|
|
/**
|
|
* Inject properties from object.
|
|
* @param {Object} options
|
|
* @returns {WalletMigratorOptions}
|
|
*/
|
|
|
|
fromOptions(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.walletDB != null) {
|
|
assert(typeof options.walletDB === 'object');
|
|
this.db = options.walletDB;
|
|
this.ldb = this.db.db;
|
|
}
|
|
|
|
if (options.walletMigrate != null) {
|
|
assert(typeof options.walletMigrate === 'number');
|
|
this.migrateFlag = options.walletMigrate;
|
|
}
|
|
|
|
if (options.dbVersion != null) {
|
|
assert(typeof options.dbVersion === 'number');
|
|
this.dbVersion = options.dbVersion;
|
|
}
|
|
|
|
if (options.migrations != null) {
|
|
assert(typeof options.migrations === 'object');
|
|
this.migrations = options.migrations;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
/**
|
|
* @param {Number} num
|
|
* @returns {Buffer}
|
|
*/
|
|
|
|
function fromU32(num) {
|
|
const data = Buffer.allocUnsafe(4);
|
|
data.writeUInt32LE(num, 0);
|
|
return data;
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
WalletMigrator.WalletMigrationResult = WalletMigrationResult;
|
|
|
|
// List of the migrations with ids
|
|
WalletMigrator.migrations = {
|
|
0: MigrateMigrations,
|
|
1: MigrateChangeAddress,
|
|
2: MigrateAccountLookahead,
|
|
3: MigrateTXDBBalances,
|
|
4: MigrateBidRevealEntries,
|
|
5: MigrateTXCountTimeIndex,
|
|
6: MigrateMigrationStateV1,
|
|
7: MigrateCoinSelection
|
|
};
|
|
|
|
// Expose migrations
|
|
WalletMigrator.MigrateChangeAddress = MigrateChangeAddress;
|
|
WalletMigrator.MigrateMigrations = MigrateMigrations;
|
|
WalletMigrator.MigrateAccountLookahead = MigrateAccountLookahead;
|
|
WalletMigrator.MigrateTXDBBalances = MigrateTXDBBalances;
|
|
WalletMigrator.MigrateBidRevealEntries = MigrateBidRevealEntries;
|
|
WalletMigrator.MigrateTXCountTimeIndex = MigrateTXCountTimeIndex;
|
|
WalletMigrator.MigrateMigrationStateV1 = MigrateMigrationStateV1;
|
|
WalletMigrator.MigrateCoinSelection = MigrateCoinSelection;
|
|
|
|
module.exports = WalletMigrator;
|