indexer: fix, simplify and rewrite indexer base
- Write indexer state and index atomically. - Simplify chain state with current height. - Roll forward to best height. - Synchronize the index with the chain with common method `sync` for the events 'connect', 'disconnect' and 'reset'. This will prevent any of the events from conflicting with each other. - Fix the chain rollback and reset. - Make sure blocks are connected in the correct order. - Improve logging to log similar to chain.
This commit is contained in:
parent
e2a6a92ebc
commit
93c6ff845e
@ -127,7 +127,6 @@ class AddrIndexer extends Indexer {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async indexBlock(entry, block, view) {
|
async indexBlock(entry, block, view) {
|
||||||
const b = this.db.batch();
|
|
||||||
const height = entry.height;
|
const height = entry.height;
|
||||||
|
|
||||||
for (let i = 0; i < block.txs.length; i++) {
|
for (let i = 0; i < block.txs.length; i++) {
|
||||||
@ -136,15 +135,17 @@ class AddrIndexer extends Indexer {
|
|||||||
|
|
||||||
for (const addr of tx.getAddresses(view)) {
|
for (const addr of tx.getAddresses(view)) {
|
||||||
const prefix = addr.getPrefix();
|
const prefix = addr.getPrefix();
|
||||||
|
|
||||||
|
if (prefix < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
const addrHash = addr.getHash();
|
const addrHash = addr.getHash();
|
||||||
const count = new Count(height, i);
|
const count = new Count(height, i);
|
||||||
|
|
||||||
b.put(layout.A.encode(prefix, addrHash, height, i, hash), null);
|
this.put(layout.A.encode(prefix, addrHash, height, i, hash), null);
|
||||||
b.put(layout.a.encode(prefix, addrHash, hash), count.toRaw());
|
this.put(layout.a.encode(prefix, addrHash, hash), count.toRaw());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -156,7 +157,6 @@ class AddrIndexer extends Indexer {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async unindexBlock(entry, block, view) {
|
async unindexBlock(entry, block, view) {
|
||||||
const b = this.db.batch();
|
|
||||||
const height = entry.height;
|
const height = entry.height;
|
||||||
|
|
||||||
for (let i = 0; i < block.txs.length; i++) {
|
for (let i = 0; i < block.txs.length; i++) {
|
||||||
@ -165,13 +165,15 @@ class AddrIndexer extends Indexer {
|
|||||||
|
|
||||||
for (const addr of tx.getAddresses(view)) {
|
for (const addr of tx.getAddresses(view)) {
|
||||||
const prefix = addr.getPrefix();
|
const prefix = addr.getPrefix();
|
||||||
|
|
||||||
|
if (prefix < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
const addrHash = addr.getHash();
|
const addrHash = addr.getHash();
|
||||||
b.del(layout.A.encode(prefix, addrHash, height, i, hash));
|
this.del(layout.A.encode(prefix, addrHash, height, i, hash));
|
||||||
b.del(layout.a.encode(prefix, addrHash, hash));
|
this.del(layout.a.encode(prefix, addrHash, hash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,17 +9,13 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('bfile');
|
const fs = require('bfile');
|
||||||
|
const bio = require('bufio');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const {Lock} = require('bmutex');
|
|
||||||
const Logger = require('blgr');
|
const Logger = require('blgr');
|
||||||
const Network = require('../protocol/network');
|
const Network = require('../protocol/network');
|
||||||
|
const util = require('../utils/util');
|
||||||
const layout = require('./layout');
|
const layout = require('./layout');
|
||||||
const records = require('./records');
|
const {BlockMeta} = require('./records');
|
||||||
|
|
||||||
const {
|
|
||||||
ChainState,
|
|
||||||
BlockMeta
|
|
||||||
} = records;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indexer
|
* Indexer
|
||||||
@ -27,13 +23,12 @@ const {
|
|||||||
* @extends EventEmitter
|
* @extends EventEmitter
|
||||||
* @property {IndexerDB} db
|
* @property {IndexerDB} db
|
||||||
* @property {Number} height
|
* @property {Number} height
|
||||||
* @property {ChainState} state
|
|
||||||
* @emits Indexer#chain tip
|
* @emits Indexer#chain tip
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Indexer extends EventEmitter {
|
class Indexer extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Create a index db.
|
* Create an indexer.
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {String} module
|
* @param {String} module
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -53,103 +48,72 @@ class Indexer extends EventEmitter {
|
|||||||
this.chain = this.options.chain;
|
this.chain = this.options.chain;
|
||||||
|
|
||||||
this.db = null;
|
this.db = null;
|
||||||
this.rescanning = false;
|
this.batch = null;
|
||||||
|
this.syncing = false;
|
||||||
this.state = new ChainState();
|
|
||||||
this.height = 0;
|
this.height = 0;
|
||||||
|
|
||||||
this.lock = new Lock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind to chain events.
|
* Start a new batch write.
|
||||||
* @private
|
* @returns {Batch}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
bind() {
|
start() {
|
||||||
this.chain.on('connect', async (entry, block, view) => {
|
assert(this.batch === null, 'Already started.');
|
||||||
if (this.rescanning)
|
this.batch = this.db.batch();
|
||||||
return;
|
return this.batch;
|
||||||
|
|
||||||
try {
|
|
||||||
await this.addBlock(entry, block, view);
|
|
||||||
} catch (e) {
|
|
||||||
this.emit('error', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.chain.on('disconnect', async (entry, block, view) => {
|
|
||||||
if (this.rescanning)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.removeBlock(entry, block, view);
|
|
||||||
} catch (e) {
|
|
||||||
this.emit('error', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.chain.on('reset', async (tip) => {
|
|
||||||
try {
|
|
||||||
await this.resetChain(tip);
|
|
||||||
} catch (e) {
|
|
||||||
this.emit('error', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure prefix directory (prefix/index).
|
* Put key and value to the current batch.
|
||||||
|
* @param {String} key
|
||||||
|
* @param {Buffer} value
|
||||||
|
*/
|
||||||
|
|
||||||
|
put(key, value) {
|
||||||
|
this.batch.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete key from the current batch.
|
||||||
|
* @param {String} key
|
||||||
|
*/
|
||||||
|
|
||||||
|
del(key) {
|
||||||
|
this.batch.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit the current batch.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async ensure() {
|
async commit() {
|
||||||
if (fs.unsupported)
|
await this.batch.write();
|
||||||
return undefined;
|
this.batch = null;
|
||||||
|
|
||||||
if (this.options.memory)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return fs.mkdirp(this.options.prefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the indexdb, wait for the database to load.
|
* Open the indexer, open the database,
|
||||||
|
* initialize height, and bind to events.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
|
this.logger.info('Indexer is loading.');
|
||||||
|
|
||||||
await this.ensure();
|
await this.ensure();
|
||||||
await this.db.open();
|
await this.db.open();
|
||||||
await this.db.verify(layout.V.encode(), 'index', 0);
|
await this.db.verify(layout.V.encode(), 'index', 0);
|
||||||
|
|
||||||
await this.verifyNetwork();
|
await this.verifyNetwork();
|
||||||
|
|
||||||
|
// Initialize the indexed height.
|
||||||
|
const data = await this.db.get(layout.R.encode());
|
||||||
|
if (data)
|
||||||
|
this.height = bio.readU32(data, 0);
|
||||||
|
|
||||||
|
// Bind to chain events.
|
||||||
this.bind();
|
this.bind();
|
||||||
|
|
||||||
await this.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify network.
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async verifyNetwork() {
|
|
||||||
const raw = await this.db.get(layout.O.encode());
|
|
||||||
|
|
||||||
if (!raw) {
|
|
||||||
const b = this.db.batch();
|
|
||||||
b.put(layout.O.encode(), fromU32(this.network.magic));
|
|
||||||
return b.write();
|
|
||||||
}
|
|
||||||
|
|
||||||
const magic = raw.readUInt32LE(0, true);
|
|
||||||
|
|
||||||
if (magic !== this.network.magic)
|
|
||||||
throw new Error('Network mismatch for Indexer.');
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,68 +126,69 @@ class Indexer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync state with server on every connect.
|
* Ensure prefix directory (prefix/index).
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async sync() {
|
async ensure() {
|
||||||
const unlock = await this.lock.lock();
|
if (fs.unsupported)
|
||||||
try {
|
return;
|
||||||
this.logger.info('Resyncing from server...');
|
|
||||||
await this.syncState();
|
if (this.options.memory)
|
||||||
await this.syncChain();
|
return;
|
||||||
} finally {
|
|
||||||
unlock();
|
await fs.mkdirp(this.options.prefix);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize and write initial sync state.
|
* Verify network of index.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async syncState() {
|
async verifyNetwork() {
|
||||||
const cache = await this.getState();
|
let raw = await this.db.get(layout.O.encode());
|
||||||
|
|
||||||
if (cache) {
|
if (!raw) {
|
||||||
this.state = cache;
|
raw = bio.write(4).writeU32(this.network.magic).render();
|
||||||
this.height = cache.height;
|
await this.db.put(layout.O.encode(), raw);
|
||||||
|
return;
|
||||||
this.logger.info(
|
|
||||||
'Indexer loaded (height=%d, start=%d).',
|
|
||||||
this.state.height,
|
|
||||||
this.state.startHeight);
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info('Initializing database state from server.');
|
const magic = bio.readU32(raw, 0);
|
||||||
|
|
||||||
const b = this.db.batch();
|
if (magic !== this.network.magic)
|
||||||
const hashes = await this.chain.getHashes();
|
throw new Error('Indexer: Network mismatch.');
|
||||||
|
}
|
||||||
|
|
||||||
let tip = null;
|
/**
|
||||||
|
* Bind to chain events.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
for (let height = 0; height < hashes.length; height++) {
|
bind() {
|
||||||
const hash = hashes[height];
|
this.chain.on('connect', async (entry, block, view) => {
|
||||||
const meta = new BlockMeta(hash, height);
|
try {
|
||||||
b.put(layout.h.encode(height), meta.toHash());
|
await this.sync(entry, block, view);
|
||||||
tip = meta;
|
} catch (e) {
|
||||||
}
|
this.emit('error', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
assert(tip);
|
this.chain.on('disconnect', async (entry, block, view) => {
|
||||||
|
try {
|
||||||
|
await this.sync(entry, block, view);
|
||||||
|
} catch (e) {
|
||||||
|
this.emit('error', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const state = this.state.clone();
|
this.chain.on('reset', async (tip) => {
|
||||||
state.startHeight = 0;
|
try {
|
||||||
state.height = tip.height;
|
await this.sync(tip);
|
||||||
|
} catch (e) {
|
||||||
b.put(layout.R.encode(), state.toRaw());
|
this.emit('error', e);
|
||||||
|
}
|
||||||
await b.write();
|
});
|
||||||
|
|
||||||
this.state = state;
|
|
||||||
this.height = state.height;
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,179 +209,13 @@ class Indexer extends EventEmitter {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect and sync with the chain server.
|
|
||||||
* @private
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async syncChain() {
|
|
||||||
let height = this.state.height;
|
|
||||||
|
|
||||||
this.logger.info('Syncing state from height %d.', height);
|
|
||||||
|
|
||||||
// A re-org when we're offline might
|
|
||||||
// leave chain in a different state.
|
|
||||||
// Scan chain backwards until we
|
|
||||||
// find a known 'good' height.
|
|
||||||
for (;;) {
|
|
||||||
const tip = await this.getBlock(height);
|
|
||||||
assert(tip);
|
|
||||||
|
|
||||||
if (await this.getEntry(tip.hash))
|
|
||||||
break;
|
|
||||||
|
|
||||||
assert(height !== 0);
|
|
||||||
height -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start scan from last indexed OR
|
|
||||||
// last known 'good' height whichever
|
|
||||||
// is lower, because `scan` scans from
|
|
||||||
// low to high blocks
|
|
||||||
if (this.state.startHeight < height)
|
|
||||||
height = this.state.startHeight;
|
|
||||||
|
|
||||||
return this._rescan(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rescan a block.
|
|
||||||
* @private
|
|
||||||
* @param {ChainEntry} entry
|
|
||||||
* @param {TX[]} txs
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async rescanBlock(entry, block, view) {
|
|
||||||
this.logger.spam('Rescanning block: %d.', entry.height);
|
|
||||||
|
|
||||||
if (!this.rescanning) {
|
|
||||||
this.logger.warning('Unsolicited rescan block: %d.', entry.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.height % 1000 === 0)
|
|
||||||
this.logger.debug('Rescanned block: %d.', entry.height);
|
|
||||||
|
|
||||||
if (entry.height > this.state.height + 1) {
|
|
||||||
this.logger.warning('Rescan block too high: %d.', entry.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this._addBlock(entry, block, view);
|
|
||||||
} catch (e) {
|
|
||||||
this.emit('error', e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force a rescan.
|
|
||||||
* @param {Number} height
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async rescan(height) {
|
|
||||||
const unlock = await this.lock.lock();
|
|
||||||
try {
|
|
||||||
return await this._rescan(height);
|
|
||||||
} finally {
|
|
||||||
unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rescan blockchain from a given height.
|
|
||||||
* @private
|
|
||||||
* @param {Number} height
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async _rescan(height) {
|
|
||||||
assert((height >>> 0) === height, 'Must pass in a height.');
|
|
||||||
|
|
||||||
await this.rollback(height);
|
|
||||||
|
|
||||||
const tip = this.state.height;
|
|
||||||
|
|
||||||
this.logger.debug('Rescanning from %d to %d', height, tip);
|
|
||||||
|
|
||||||
this.rescanning = true;
|
|
||||||
|
|
||||||
for (let i = height; ; i++) {
|
|
||||||
const entry = await this.chain.getEntry(i);
|
|
||||||
if (!entry)
|
|
||||||
break;
|
|
||||||
|
|
||||||
const block = await this.chain.getBlock(entry.hash);
|
|
||||||
assert(block);
|
|
||||||
|
|
||||||
const view = await this.chain.getBlockView(block);
|
|
||||||
assert(view);
|
|
||||||
|
|
||||||
await this.rescanBlock(entry, block, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rescanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the best block hash.
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async getState() {
|
|
||||||
const data = await this.db.get(layout.R.encode());
|
|
||||||
|
|
||||||
if (!data)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ChainState.fromRaw(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync the current chain state to tip.
|
|
||||||
* @param {BlockMeta} tip
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async setTip(tip) {
|
|
||||||
const b = this.db.batch();
|
|
||||||
const state = this.state.clone();
|
|
||||||
|
|
||||||
if (tip.height < state.height) {
|
|
||||||
// Hashes ahead of our new tip
|
|
||||||
// that we need to delete.
|
|
||||||
while (state.height !== tip.height) {
|
|
||||||
b.del(layout.h.encode(state.height));
|
|
||||||
state.height -= 1;
|
|
||||||
}
|
|
||||||
} else if (tip.height > state.height) {
|
|
||||||
assert(tip.height === state.height + 1, 'Bad chain sync.');
|
|
||||||
state.height += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.startHeight = tip.height;
|
|
||||||
|
|
||||||
// Save tip and state.
|
|
||||||
b.put(layout.h.encode(tip.height), tip.toHash());
|
|
||||||
b.put(layout.R.encode(), state.toRaw());
|
|
||||||
|
|
||||||
await b.write();
|
|
||||||
|
|
||||||
this.state = state;
|
|
||||||
this.height = state.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a index block meta.
|
* Get a index block meta.
|
||||||
* @param {Hash} hash
|
* @param {Hash} hash
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async getBlock(height) {
|
async getBlockMeta(height) {
|
||||||
const data = await this.db.get(layout.h.encode(height));
|
const data = await this.db.get(layout.h.encode(height));
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
@ -430,59 +229,157 @@ class Indexer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get index tip.
|
* Sync with the chain.
|
||||||
* @param {Hash} hash
|
* @param {ChainEntry} entry
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {CoinView} view
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async getTip() {
|
async sync(entry, block, view) {
|
||||||
const tip = await this.getBlock(this.state.height);
|
if (this.syncing)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!tip)
|
this.syncing = true;
|
||||||
throw new Error('Indexer: Tip not found!');
|
|
||||||
|
|
||||||
return tip;
|
const connected = await this._syncBlock(entry, block, view);
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
this.syncing = false;
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await this._syncChain(entry);
|
||||||
|
this.syncing = false;
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync with chain height.
|
* Sync with the chain with a block.
|
||||||
|
* @private
|
||||||
|
* @param {ChainEntry} entry
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {CoinView} view
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async _syncBlock(entry, block, view) {
|
||||||
|
// In the case that the next block is being
|
||||||
|
// connected or the current block disconnected
|
||||||
|
// use the block and view being passed directly,
|
||||||
|
// instead of reading that information again.
|
||||||
|
if (entry && block && view) {
|
||||||
|
if (entry.height === this.height + 1) {
|
||||||
|
await this._addBlock(entry, block, view);
|
||||||
|
return true;
|
||||||
|
} else if (entry.height === this.height) {
|
||||||
|
await this._removeBlock(entry, block, view);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync with the chain.
|
||||||
|
* @private
|
||||||
|
* @param {ChainEntry} entry
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async _syncChain(entry) {
|
||||||
|
let height = this.height;
|
||||||
|
|
||||||
|
// In the case that the indexer has never
|
||||||
|
// started, sync to the best height.
|
||||||
|
if (!height) {
|
||||||
|
await this._rollforward();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a re-org that might
|
||||||
|
// leave chain in a different state.
|
||||||
|
// Scan chain backwards until we
|
||||||
|
// find a common height.
|
||||||
|
for (;;) {
|
||||||
|
const tip = await this.getBlockMeta(height);
|
||||||
|
assert(tip);
|
||||||
|
|
||||||
|
if (await this.getEntry(tip.hash))
|
||||||
|
break;
|
||||||
|
|
||||||
|
assert(height !== 0);
|
||||||
|
height -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case that the chain is reset
|
||||||
|
// the entry will be less than the
|
||||||
|
// current height.
|
||||||
|
if (entry && entry.height < height)
|
||||||
|
height = entry.height;
|
||||||
|
|
||||||
|
if (height < this.height) {
|
||||||
|
await this._rollback(height);
|
||||||
|
await this._rollforward();
|
||||||
|
} else {
|
||||||
|
await this._rollforward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan blockchain to the best chain height.
|
||||||
|
* @private
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async _rollforward() {
|
||||||
|
this.logger.info('Indexing to best height.');
|
||||||
|
|
||||||
|
for (let i = this.height + 1; ; i++) {
|
||||||
|
const entry = await this.getEntry(i);
|
||||||
|
if (!entry)
|
||||||
|
break;
|
||||||
|
|
||||||
|
const block = await this.chain.getBlock(entry.hash);
|
||||||
|
assert(block);
|
||||||
|
|
||||||
|
const view = await this.chain.getBlockView(block);
|
||||||
|
assert(view);
|
||||||
|
|
||||||
|
await this._addBlock(entry, block, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback to a given chain height.
|
||||||
* @param {Number} height
|
* @param {Number} height
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async rollback(height) {
|
async _rollback(height) {
|
||||||
if (height > this.state.height)
|
if (height > this.height) {
|
||||||
throw new Error('Indexer: Cannot rollback to the future.');
|
this.logger.warning(
|
||||||
|
'Ignoring rollback to future height (%d).',
|
||||||
if (height === this.state.height) {
|
height);
|
||||||
this.logger.info('Rolled back to same height (%d).', height);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info('Rolling back to height %d.', height);
|
||||||
'Rolling back %d Indexer blocks to height %d.',
|
|
||||||
this.state.height - height, height);
|
|
||||||
|
|
||||||
const tip = await this.getBlock(height);
|
while (this.height > height) {
|
||||||
assert(tip);
|
const tip = await this.getBlockMeta(this.height);
|
||||||
|
assert(tip);
|
||||||
|
|
||||||
await this.revert(tip.height);
|
const entry = await this.chain.getEntry(tip.hash);
|
||||||
await this.setTip(tip);
|
assert(entry);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const block = await this.chain.getBlock(entry.hash);
|
||||||
* Add a block's transactions and write the new best hash.
|
assert(block);
|
||||||
* @param {ChainEntry} entry
|
|
||||||
* @param {Block} block
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async addBlock(entry, block, view) {
|
const view = await this.chain.getBlockView(block);
|
||||||
const unlock = await this.lock.lock();
|
assert(view);
|
||||||
try {
|
|
||||||
return await this._addBlock(entry, block, view);
|
await this._removeBlock(entry, block, view);
|
||||||
} finally {
|
|
||||||
unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,34 +394,31 @@ class Indexer extends EventEmitter {
|
|||||||
async _addBlock(entry, block, view) {
|
async _addBlock(entry, block, view) {
|
||||||
assert(block.hasRaw(), 'Expected raw data for block.');
|
assert(block.hasRaw(), 'Expected raw data for block.');
|
||||||
|
|
||||||
|
const start = util.bench();
|
||||||
|
|
||||||
|
if (entry.height !== this.height + 1)
|
||||||
|
throw new Error('Indexer: Can not add block.');
|
||||||
|
|
||||||
const tip = BlockMeta.fromEntry(entry);
|
const tip = BlockMeta.fromEntry(entry);
|
||||||
|
|
||||||
if (tip.height >= this.network.block.slowHeight && !this.rescanning)
|
// Start the batch write.
|
||||||
this.logger.debug('Adding block: %d.', tip.height);
|
this.start();
|
||||||
|
|
||||||
this.logger.spam('Adding block: %d.', entry.height);
|
|
||||||
|
|
||||||
if (tip.height === this.state.height) {
|
|
||||||
// We let blocks of the same height
|
|
||||||
// through specifically for rescans:
|
|
||||||
// we always want to rescan the last
|
|
||||||
// block since the state may have
|
|
||||||
// updated before the block was fully
|
|
||||||
// processed (in the case of a crash).
|
|
||||||
this.logger.warning('Already saw Indexer block (%d).', tip.height);
|
|
||||||
} else if (tip.height !== this.state.startHeight + 1) {
|
|
||||||
await this._rescan(this.state.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.spam('Indexing block: %d.', entry.height);
|
|
||||||
|
|
||||||
|
// Call the implemented indexer to add to
|
||||||
|
// the batch write.
|
||||||
await this.indexBlock(entry, block, view);
|
await this.indexBlock(entry, block, view);
|
||||||
|
|
||||||
// Sync the state to the new tip.
|
// Sync the height to the new tip.
|
||||||
await this.setTip(tip);
|
const height = await this._setTip(tip);
|
||||||
|
|
||||||
return;
|
// Commit the write batch to disk.
|
||||||
|
await this.commit();
|
||||||
|
|
||||||
|
// Update height _after_ successful commit.
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
// Log the current indexer status.
|
||||||
|
this.logStatus(start, block, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -551,32 +445,6 @@ class Indexer extends EventEmitter {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Revert db to an older state.
|
|
||||||
* @param {Number} target
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async revert(target) {
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unconfirm a block's transactions
|
|
||||||
* and write the new best hash (SPV version).
|
|
||||||
* @param {ChainEntry} entry
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
async removeBlock(entry, block, view) {
|
|
||||||
const unlock = await this.lock.lock();
|
|
||||||
try {
|
|
||||||
return await this._removeBlock(entry, block, view);
|
|
||||||
} finally {
|
|
||||||
unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unconfirm a block's transactions.
|
* Unconfirm a block's transactions.
|
||||||
* @private
|
* @private
|
||||||
@ -585,63 +453,94 @@ class Indexer extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async _removeBlock(entry, block, view) {
|
async _removeBlock(entry, block, view) {
|
||||||
|
const start = util.bench();
|
||||||
|
|
||||||
|
if (entry.height !== this.height)
|
||||||
|
throw new Error('Indexer: Can not remove block.');
|
||||||
|
|
||||||
const tip = BlockMeta.fromEntry(entry);
|
const tip = BlockMeta.fromEntry(entry);
|
||||||
|
|
||||||
this.logger.spam('Removing block: %d.', entry.height);
|
// Start the batch write.
|
||||||
|
this.start();
|
||||||
if (tip.height === 0)
|
|
||||||
throw new Error('Indexer: Bad disconnection (genesis block).');
|
|
||||||
|
|
||||||
if (tip.height > this.state.height) {
|
|
||||||
this.logger.warning(
|
|
||||||
'Indexer is disconnecting high blocks (%d).',
|
|
||||||
tip.height);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tip.height !== this.state.height)
|
|
||||||
throw new Error('Indexer: Bad disconnection (height mismatch).');
|
|
||||||
|
|
||||||
this.logger.spam('Unindexing block: %d.', entry.height);
|
|
||||||
|
|
||||||
|
// Call the implemented indexer to add to
|
||||||
|
// the batch write.
|
||||||
await this.unindexBlock(entry, block, view);
|
await this.unindexBlock(entry, block, view);
|
||||||
|
|
||||||
const prev = await this.getBlock(tip.height - 1);
|
const prev = await this.getBlockMeta(tip.height - 1);
|
||||||
assert(prev);
|
assert(prev);
|
||||||
|
|
||||||
// Sync the state to the previous tip.
|
// Sync the height to the previous tip.
|
||||||
await this.setTip(prev);
|
const height = await this._setTip(prev);
|
||||||
|
|
||||||
return;
|
// Commit the write batch to disk.
|
||||||
|
await this.commit();
|
||||||
|
|
||||||
|
// Update height _after_ successful commit.
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
// Log the current indexer status.
|
||||||
|
this.logStatus(start, block, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a chain reset.
|
* Update the current height to tip.
|
||||||
* @param {ChainEntry} entry
|
* @param {BlockMeta} tip
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async resetChain(entry) {
|
async _setTip(tip) {
|
||||||
const unlock = await this.lock.lock();
|
if (tip.height < this.height) {
|
||||||
try {
|
assert(tip.height === this.height - 1);
|
||||||
return await this._resetChain(entry);
|
this.del(layout.h.encode(this.height));
|
||||||
} finally {
|
} else if (tip.height > this.height) {
|
||||||
unlock();
|
assert(tip.height === this.height + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add to batch write to save tip and height.
|
||||||
|
this.put(layout.h.encode(tip.height), tip.toHash());
|
||||||
|
|
||||||
|
const raw = bio.write(4).writeU32(tip.height).render();
|
||||||
|
this.put(layout.R.encode(), raw);
|
||||||
|
|
||||||
|
return tip.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a chain reset without a lock.
|
* Test whether the indexer has reached its slow height.
|
||||||
* @private
|
* @private
|
||||||
* @param {ChainEntry} entry
|
* @returns {Boolean}
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async _resetChain(entry) {
|
isSlow() {
|
||||||
if (entry.height > this.state.height)
|
if (this.height === 1 || this.height % 20 === 0)
|
||||||
throw new Error('Indexer: Bad reset height.');
|
return true;
|
||||||
|
|
||||||
return this.rollback(entry.height);
|
if (this.height >= this.network.block.slowHeight)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the current indexer status.
|
||||||
|
* @private
|
||||||
|
* @param {Array} start
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {ChainEntry} entry
|
||||||
|
*/
|
||||||
|
|
||||||
|
logStatus(start, block, entry) {
|
||||||
|
if (!this.isSlow())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const elapsed = util.bench(start);
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
'Block (%d) added to indexer (txs=%d time=%d).',
|
||||||
|
entry.height,
|
||||||
|
block.txs.length,
|
||||||
|
elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,7 +563,6 @@ class IndexOptions {
|
|||||||
this.logger = Logger.global;
|
this.logger = Logger.global;
|
||||||
this.blocks = null;
|
this.blocks = null;
|
||||||
this.chain = null;
|
this.chain = null;
|
||||||
this.indexers = null;
|
|
||||||
|
|
||||||
this.prefix = null;
|
this.prefix = null;
|
||||||
this.location = null;
|
this.location = null;
|
||||||
@ -737,7 +635,7 @@ class IndexOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate chain options from object.
|
* Instantiate indexer options from object.
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @returns {IndexOptions}
|
* @returns {IndexOptions}
|
||||||
*/
|
*/
|
||||||
@ -747,22 +645,6 @@ class IndexOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fromU32
|
|
||||||
* read a 4 byte Uint32LE
|
|
||||||
* @param {Number} num number
|
|
||||||
* @returns {Buffer} buffer
|
|
||||||
*/
|
|
||||||
function fromU32(num) {
|
|
||||||
const data = Buffer.allocUnsafe(4);
|
|
||||||
data.writeUInt32LE(num, 0, true);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Expose
|
* Expose
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -14,74 +14,6 @@ const bio = require('bufio');
|
|||||||
const util = require('../utils/util');
|
const util = require('../utils/util');
|
||||||
const consensus = require('../protocol/consensus');
|
const consensus = require('../protocol/consensus');
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain State
|
|
||||||
* @alias module:indexer.ChainState
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ChainState {
|
|
||||||
/**
|
|
||||||
* Create a chain state.
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.startHeight = 0;
|
|
||||||
this.height = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone the state.
|
|
||||||
* @returns {ChainState}
|
|
||||||
*/
|
|
||||||
|
|
||||||
clone() {
|
|
||||||
const state = new ChainState();
|
|
||||||
state.startHeight = this.startHeight;
|
|
||||||
state.height = this.height;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject properties from serialized data.
|
|
||||||
* @private
|
|
||||||
* @param {Buffer} data
|
|
||||||
*/
|
|
||||||
|
|
||||||
fromRaw(data) {
|
|
||||||
const br = bio.read(data);
|
|
||||||
|
|
||||||
this.startHeight = br.readU32();
|
|
||||||
this.height = br.readU32();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate chain state from serialized data.
|
|
||||||
* @param {Buffer} data
|
|
||||||
* @returns {ChainState}
|
|
||||||
*/
|
|
||||||
|
|
||||||
static fromRaw(data) {
|
|
||||||
return new this().fromRaw(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize the chain state.
|
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
|
||||||
|
|
||||||
toRaw() {
|
|
||||||
const bw = bio.write(8);
|
|
||||||
|
|
||||||
bw.writeU32(this.startHeight);
|
|
||||||
bw.writeU32(this.height);
|
|
||||||
|
|
||||||
return bw.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block Meta
|
* Block Meta
|
||||||
* @alias module:indexer.BlockMeta
|
* @alias module:indexer.BlockMeta
|
||||||
@ -215,7 +147,6 @@ class BlockMeta {
|
|||||||
* Expose
|
* Expose
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.ChainState = ChainState;
|
|
||||||
exports.BlockMeta = BlockMeta;
|
exports.BlockMeta = BlockMeta;
|
||||||
|
|
||||||
module.exports = exports;
|
module.exports = exports;
|
||||||
|
|||||||
@ -129,8 +129,6 @@ class TXIndexer extends Indexer {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async indexBlock(entry, block, view) {
|
async indexBlock(entry, block, view) {
|
||||||
const b = this.db.batch();
|
|
||||||
|
|
||||||
for (let i = 0; i < block.txs.length; i++) {
|
for (let i = 0; i < block.txs.length; i++) {
|
||||||
const tx = block.txs[i];
|
const tx = block.txs[i];
|
||||||
|
|
||||||
@ -146,10 +144,8 @@ class TXIndexer extends Indexer {
|
|||||||
length: size
|
length: size
|
||||||
});
|
});
|
||||||
|
|
||||||
b.put(layout.t.encode(hash), txrecord.toRaw());
|
this.put(layout.t.encode(hash), txrecord.toRaw());
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,15 +157,11 @@ class TXIndexer extends Indexer {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async unindexBlock(entry, block, view) {
|
async unindexBlock(entry, block, view) {
|
||||||
const b = this.db.batch();
|
|
||||||
|
|
||||||
for (let i = 0; i < block.txs.length; i++) {
|
for (let i = 0; i < block.txs.length; i++) {
|
||||||
const tx = block.txs[i];
|
const tx = block.txs[i];
|
||||||
const hash = tx.hash();
|
const hash = tx.hash();
|
||||||
b.del(layout.t.encode(hash));
|
this.del(layout.t.encode(hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.write();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -410,6 +410,12 @@ class FullNode extends Node {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
startSync() {
|
startSync() {
|
||||||
|
if (this.txindex)
|
||||||
|
this.txindex.sync();
|
||||||
|
|
||||||
|
if (this.addrindex)
|
||||||
|
this.addrindex.sync();
|
||||||
|
|
||||||
return this.pool.startSync();
|
return this.pool.startSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,8 +90,8 @@ describe('Indexer', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.strictEqual(chain.height, 10);
|
assert.strictEqual(chain.height, 10);
|
||||||
assert.strictEqual(txindexer.state.startHeight, 10);
|
assert.strictEqual(txindexer.height, 10);
|
||||||
assert.strictEqual(addrindexer.state.startHeight, 10);
|
assert.strictEqual(addrindexer.height, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get txs by address', async () => {
|
it('should get txs by address', async () => {
|
||||||
@ -172,8 +172,8 @@ describe('Indexer', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.strictEqual(chain.height, 20);
|
assert.strictEqual(chain.height, 20);
|
||||||
assert.strictEqual(txindexer.state.startHeight, 20);
|
assert.strictEqual(txindexer.height, 20);
|
||||||
assert.strictEqual(addrindexer.state.startHeight, 20);
|
assert.strictEqual(addrindexer.height, 20);
|
||||||
|
|
||||||
const hashes = await addrindexer.getHashesByAddress(miner.getAddress());
|
const hashes = await addrindexer.getHashesByAddress(miner.getAddress());
|
||||||
assert.strictEqual(hashes.length, 20);
|
assert.strictEqual(hashes.length, 20);
|
||||||
@ -187,8 +187,8 @@ describe('Indexer', function() {
|
|||||||
it('should handle indexing a reorg', async () => {
|
it('should handle indexing a reorg', async () => {
|
||||||
await reorg(chain, cpu, 10);
|
await reorg(chain, cpu, 10);
|
||||||
|
|
||||||
assert.strictEqual(txindexer.state.startHeight, 31);
|
assert.strictEqual(txindexer.height, 31);
|
||||||
assert.strictEqual(addrindexer.state.startHeight, 31);
|
assert.strictEqual(addrindexer.height, 31);
|
||||||
|
|
||||||
const hashes = await addrindexer.getHashesByAddress(miner.getAddress());
|
const hashes = await addrindexer.getHashesByAddress(miner.getAddress());
|
||||||
assert.strictEqual(hashes.length, 31);
|
assert.strictEqual(hashes.length, 31);
|
||||||
@ -269,9 +269,9 @@ describe('Indexer', function() {
|
|||||||
'getnewaddress', ['default']);
|
'getnewaddress', ['default']);
|
||||||
|
|
||||||
const blocks = await nclient.execute(
|
const blocks = await nclient.execute(
|
||||||
'generatetoaddress', [120, coinbase]);
|
'generatetoaddress', [150, coinbase]);
|
||||||
|
|
||||||
assert.equal(blocks.length, 120);
|
assert.equal(blocks.length, 150);
|
||||||
|
|
||||||
// Send to the vector addresses for several blocks.
|
// Send to the vector addresses for several blocks.
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user