'use strict'; var imports = require('soop').imports(); var TIMESTAMP_PREFIX = 'bts-'; // bts- => var PREV_PREFIX = 'bpr-'; // bpr- => var NEXT_PREFIX = 'bne-'; // bne- => var MAIN_PREFIX = 'bma-'; // bma- => (0 is unconnected) var TIP = 'bti-'; // bti = : last block on the chain var LAST_FILE_INDEX = 'file-'; // last processed file index // txid - blockhash mapping (only for confirmed txs, ONLY FOR BEST BRANCH CHAIN) var IN_BLK_PREFIX = 'btx-'; //btx- = var MAX_OPEN_FILES = 500; var CONCURRENCY = 5; var DFLT_REQUIRED_CONFIRMATIONS = 1; /** * Module dependencies. */ var levelup = require('levelup'), config = require('../config/config'); var db = imports.db || levelup(config.leveldb + '/blocks',{maxOpenFiles: MAX_OPEN_FILES} ); var Rpc = imports.rpc || require('./Rpc'); var async = require('async'); var logger = require('./logger').logger; var info = logger.info; var BlockDb = function(opts) { this.txDb = require('./TransactionDb').default(); this.safeConfirmations = config.safeConfirmations || DEFAULT_SAFE_CONFIRMATIONS; BlockDb.super(this, arguments); }; BlockDb.prototype.close = function(cb) { db.close(cb); }; BlockDb.prototype.drop = function(cb) { var path = config.leveldb + '/blocks'; db.close(function() { require('leveldown').destroy(path, function () { db = levelup(path,{maxOpenFiles: MAX_OPEN_FILES} ); return cb(); }); }); }; BlockDb.prototype._addBlockScript = function(b, height) { var time_key = TIMESTAMP_PREFIX + ( b.time || Math.round(new Date().getTime() / 1000) ); return [ { type: 'put', key: time_key, value: b.hash, }, { type: 'put', key: MAIN_PREFIX + b.hash, value: height, }, { type: 'put', key:PREV_PREFIX + b.hash, value: b.previousblockhash, }, ]; }; BlockDb.prototype._delTxsScript = function(txs) { var dbScript =[]; for(var ii in txs){ dbScript.push({ type: 'del', key: IN_BLK_PREFIX + txs[ii], }); } return dbScript; }; BlockDb.prototype._addTxsScript = function(txs, hash, height) { var dbScript =[]; for(var ii in txs){ dbScript.push({ type: 'put', key: IN_BLK_PREFIX + txs[ii], value: hash+':'+height, }); } return dbScript; }; // Returns blockHash and height for a given txId (If the tx is on the MAIN chain). BlockDb.prototype.getBlockForTx = function(txId, cb) { db.get(IN_BLK_PREFIX + txId,function (err, val) { if (err && err.notFound) return cb(); if (err) return cb(err); var v = val.split(':'); return cb(err,v[0],parseInt(v[1])); }); }; BlockDb.prototype._changeBlockHeight = function(hash, height, cb) { var self = this; var dbScript1 = this._setHeightScript(hash,height); logger.log('Getting TXS FROM %s to set it Main', hash); this.fromHashWithInfo(hash, function(err, bi) { if (!bi || !bi.info || !bi.info.tx) throw new Error('unable to get info for block:'+ hash); var dbScript2; if (height>=0) { dbScript2 = self._addTxsScript(bi.info.tx, hash, height); logger.info('\t%s %d Txs', 'Confirming', bi.info.tx.length); } else { dbScript2 = self._delTxsScript(bi.info.tx); logger.info('\t%s %d Txs', 'Unconfirming', bi.info.tx.length); } db.batch(dbScript2.concat(dbScript1),cb); }); }; BlockDb.prototype.setBlockMain = function(hash, height, cb) { this._changeBlockHeight(hash,height,cb); }; BlockDb.prototype.setBlockNotMain = function(hash, cb) { this._changeBlockHeight(hash,-1,cb); }; // adds a block (and its txs). Does not update Next pointer in // the block prev to the new block, nor TIP pointer // BlockDb.prototype.add = function(b, height, cb) { var txs = typeof b.tx[0] === 'string' ? b.tx : b.tx.map( function(o){ return o.txid; }); var dbScript = this._addBlockScript(b,height); dbScript = dbScript.concat(this._addTxsScript(txs, b.hash, height)); this.txDb.addMany(b.tx, function(err) { if (err) return cb(err); db.batch(dbScript, cb); }); }; BlockDb.prototype.getTip = function(cb) { if (this.cachedTip){ var v = this.cachedTip.split(':'); return cb(null,v[0], parseInt(v[1])); } var self = this; db.get(TIP, function(err, val) { if (!val) return cb(); self.cachedTip = val; var v = val.split(':'); return cb(err,v[0], parseInt(v[1])); }); }; BlockDb.prototype.setTip = function(hash, height, cb) { this.cachedTip = hash + ':' + height; db.put(TIP, this.cachedTip, function(err) { return cb(err); }); }; BlockDb.prototype.getDepth = function(hash, cb) { var v = this.cachedTip.split(':'); if (!v) throw new Error('getDepth called with not cachedTip'); this.getHeight(hash, function(err,h){ return cb(err,parseInt(v[1]) - h); }); }; //mainly for testing BlockDb.prototype.setPrev = function(hash, prevHash, cb) { db.put(PREV_PREFIX + hash, prevHash, function(err) { return cb(err); }); }; BlockDb.prototype.getPrev = function(hash, cb) { db.get(PREV_PREFIX + hash, function(err,val) { if (err && err.notFound) { err = null; val = null;} return cb(err,val); }); }; BlockDb.prototype.setLastFileIndex = function(idx, cb) { var self = this; if (this.lastFileIndexSaved === idx) return cb(); db.put(LAST_FILE_INDEX, idx, function(err) { self.lastFileIndexSaved = idx; return cb(err); }); }; BlockDb.prototype.getLastFileIndex = function(cb) { db.get(LAST_FILE_INDEX, function(err,val) { if (err && err.notFound) { err = null; val = null;} return cb(err,val); }); }; BlockDb.prototype.getNext = function(hash, cb) { db.get(NEXT_PREFIX + hash, function(err,val) { if (err && err.notFound) { err = null; val = null;} return cb(err,val); }); }; BlockDb.prototype.getHeight = function(hash, cb) { db.get(MAIN_PREFIX + hash, function(err, val) { if (err && err.notFound) { err = null; val = 0;} return cb(err,parseInt(val)); }); }; BlockDb.prototype._setHeightScript = function(hash, height) { logger.log('setHeight: %s #%d', hash,height); return ([{ type: 'put', key: MAIN_PREFIX + hash, value: height, }]); }; BlockDb.prototype.setNext = function(hash, nextHash, cb) { db.put(NEXT_PREFIX + hash, nextHash, function(err) { return cb(err); }); }; // Unused BlockDb.prototype.countConnected = function(cb) { var c = 0; console.log('Counting connected blocks. This could take some minutes'); db.createReadStream({start: MAIN_PREFIX, end: MAIN_PREFIX + '~' }) .on('data', function (data) { if (data.value !== 0) c++; }) .on('error', function (err) { return cb(err); }) .on('end', function () { return cb(null, c); }); }; // .has() return true orphans also BlockDb.prototype.has = function(hash, cb) { var k = PREV_PREFIX + hash; db.get(k, function (err) { var ret = true; if (err && err.notFound) { err = null; ret = false; } return cb(err, ret); }); }; BlockDb.prototype.fromHashWithInfo = function(hash, cb) { var self = this; Rpc.getBlock(hash, function(err, info) { if (err || !info) return cb(err); //TODO can we get this from RPC .height? self.getHeight(hash, function(err, height) { if (err) return cb(err); info.isMainChain = height>=0 ? true : false; return cb(null, { hash: hash, info: info, }); }); }); }; BlockDb.prototype.getBlocksByDate = function(start_ts, end_ts, limit, cb) { var list = []; var opts = { start: TIMESTAMP_PREFIX + end_ts, //Inverted since list is reversed end: TIMESTAMP_PREFIX + start_ts, limit: limit, reverse: 1, }; db.createReadStream(opts) .on('data', function (data) { var k = data.key.split('-'); list.push({ ts: k[1], hash: data.value, }); }) .on('error', function (err) { return cb(err); }) .on('end', function () { return cb(null, list.reverse()); }); }; BlockDb.prototype.blockIndex = function(height, cb) { return Rpc.blockIndex(height,cb); }; BlockDb.prototype._fillConfirmationsOneSpent = function(o, chainHeight, cb) { var self = this; if (!o.spentTxId) return cb(); if (o.multipleSpentAttempts) { async.eachLimit(o.multipleSpentAttempts, CONCURRENCY, function(oi, e_c) { // Only one will be confirmed self.getBlockForTx(oi.txid, function(err, hash, height) { if (err) return; if (height>=0) { o.spentTxId = oi.txid; o.index = oi.index; o.spentIsConfirmed = chainHeight >= height; o.spentConfirmations = chainHeight - height +1; } return e_c(); }); }, cb); } else { self.getBlockForTx(o.spentTxId, function(err, hash, height) { if (err) return cb(err); if (height >=0 ) { o.spentIsConfirmed = chainHeight >= height; o.spentConfirmations = chainHeight - height +1; } return cb(); }); } }; BlockDb.prototype._fillConfirmationsOne = function(o, chainHeight, cb) { var self = this; self.getBlockForTx(o.txid, function(err, hash, height) { if (err) return cb(err); if (height>=0) { o.isConfirmed = chainHeight >= height; o.confirmations = chainHeight - height +1; return self._fillConfirmationsOneSpent(o,chainHeight,cb); } else return cb(); }); }; BlockDb.prototype.fillConfirmations = function(txouts, cb) { var self = this; this.getTip(function(err, hash, height){ var txs = txouts.filter(function(x){ return !x.spentIsConfirmedCached // not 100%cached && !(x.isConfirmedCached && !x.spentTxId); // and not partial cached but not spent }); //console.log('[BlockDb.js.373:txs:]',txs.length, txs.slice(0,5)); //TODO async.eachLimit(txs, CONCURRENCY, function(txout, e_c) { if(txout.isConfirmedCached) { self._fillConfirmationsOneSpent(txout,height, e_c); } else { self._fillConfirmationsOne(txout,height, e_c); } }, cb); }); }; /* this is only for migration scripts */ BlockDb.prototype._runScript = function(script, cb) { db.batch(script,cb); }; BlockDb.prototype.migrateV02 = function(cb) { var k = 'txb-'; var dbScript = []; var c=0; var c2=0; var N=50000; this.txDb._db.createReadStream({ start: k, end: k + '~' }) .on('data', function(data) { var k = data.key.split('-'); var v = data.value.split(':'); dbScript.push({ type: 'put', key: IN_BLK_PREFIX + k[1], value: data.value, }); if (c++>N) { console.log('\t%dM txs processed', ((c2+=N)/1e6).toFixed(3)); db.batch(dbScript,function () { c=0; dbScript=[]; }); } }) .on('error', function(err) { return cb(err); }) .on('end', function (){ return cb(); }); }; BlockDb.prototype.migrateV02cleanup = function(cb) { var self = this; console.log('## deleting txb- from txs db'); //todo var k = 'txb-'; var d = this.txDb._db; d.createReadStream({ start: k, end: k + '~' }) .pipe(d.createWriteStream({type:'del'})) .on('close', function(err){ if (err) return cb(err); console.log('## deleting tx- from txs db'); //todo var k = 'tx-'; var d = self.txDb._db; d.createReadStream({ start: k, end: k + '~' }) .pipe(d.createWriteStream({type:'del'})) .on('close', function(err){ if (err) return cb(err); var k = 'txa-'; var d = self.txDb._db; d.createReadStream({ start: k, end: k + '~' }) .pipe(d.createWriteStream({type:'del'})) .on('close', cb); }); }); }; module.exports = require('soop')(BlockDb);