UNPKG

digiassets-full-node

Version:
820 lines (756 loc) 26.8 kB
var async = require('async') var DATransaction = require('digiasset-transaction') var getAssetsOutputs = require('digiasset-get-assets-outputs') var bitcoinjs = require('bitcoinjs-lib') var bufferReverse = require('buffer-reverse') var _ = require('lodash') var toposort = require('toposort') var redisClient = require('redis') var digibyteRpc = require('bitcoin-async') var events = require('events') var mainnetFirstColoredBlock = 8420000 var testnetFirstColoredBlock = 10000 var blockStates = { NOT_EXISTS: 0, GOOD: 1, FORKED: 2 } var label = 'da-full-node' module.exports = function (args) { args = args || {} var network = args.network || 'testnet' var digibyteNetwork = (network === 'mainnet') ? bitcoinjs.networks.bitcoin : bitcoinjs.networks.testnet var redisOptions = { host: args.redisHost || 'localhost', port: args.redisPort || '6379', prefix: 'dafullnode:' + network + ':' } var redis = redisClient.createClient(redisOptions) var digibyteOptions = { host: args.digibyteHost || 'localhost', port: args.digibytePort || '14023', user: args.digibyteUser || 'rpcuser', pass: args.digibytePass || 'rpcpass', path: args.digibytePath || '/', timeout: args.digibyteTimeout || 30000 } var digibyte = new digibyteRpc.Client(digibyteOptions) var emitter = new events.EventEmitter() var info = { digibytedbusy: true } var waitForDigiByted = function (cb) { if (!info.digibytedbusy) return cb() return setTimeout(function() { console.log('Waiting for digibyte...') digibyte.cmd('getblockchaininfo', [], function (err) { if (err) { info.error = {} if (err.code) { info.error.code = err.code } if (err.message) { info.error.message = err.message } if (!err.code && !err.message) { info.error = err } return waitForDigiByted(cb) } delete info.error info.digibytedbusy = false cb() }) }, 5000) } var getNextBlockHeight = function (cb) { redis.hget('blocks', 'lastBlockHeight', function (err, lastBlockHeight) { if (err) return cb(err) lastBlockHeight = lastBlockHeight || ((network === 'mainnet' ? mainnetFirstColoredBlock : testnetFirstColoredBlock) - 1) lastBlockHeight = parseInt(lastBlockHeight); cb(null, lastBlockHeight + 1) }) } var getNextBlock = function (height, cb) { digibyte.cmd('getblockhash', [height], function (err, hash) { if (err) { if (err.code && err.code === -8) { return cb(null, null) } return cb(err) } digibyte.cmd('getblock', [hash, false], function (err, rawBlock) { if (err) return cb(err) var block = bitcoinjs.Block.fromHex(rawBlock) block.height = height block.hash = hash block.previousblockhash = bufferReverse(block.prevHash).toString('hex') block.transactions = block.transactions.map(function (transaction) { return decodeRawTransaction(transaction) }) cb(null, block) }) }) } var checkNextBlock = function (block, cb) { if (!block) return cb(null, blockStates.NOT_EXISTS, block) redis.hget('blocks', block.height - 1, function (err, hash) { if (!hash || hash === block.previousblockhash) return cb(null, blockStates.GOOD, block) cb(null, blockStates.FORKED, block) }) } var revertBlock = function (blockHeight, cb) { console.log('forking block', blockHeight) updateLastBlock(blockHeight - 1, cb) } var conditionalParseNextBlock = function (state, block, cb) { if (state === blockStates.NOT_EXISTS) { return mempoolParse(cb) } // console.log('block', block.hash, block.height, 'txs:', block.transactions.length, 'state', state) if (state === blockStates.GOOD) { return parseNewBlock(block, cb) } if (state === blockStates.FORKED) { return revertBlock(block.height - 1, cb) } cb('Unknown block state') } var checkVersion = function (hex) { var version = hex.toString('hex').substring(0, 4) return (version.toLowerCase() === '4441') } var getColoredData = function (transaction) { var coloredData = null transaction.vout.some(function (vout) { if (!vout.scriptPubKey || !vout.scriptPubKey.type === 'nulldata') return null var hex = vout.scriptPubKey.asm.substring('OP_RETURN '.length) if (checkVersion(hex)) { try { coloredData = DATransaction.fromHex(hex).toJson() } catch (e) { console.log('Invalid DA transaction.') } } return coloredData }) return coloredData } var getPreviousOutputs = function(transaction, cb) { var prevTxs = [] transaction.vin.forEach(function(vin) { prevTxs.push(vin) }) var prevOutsBatch = prevTxs.map(function(vin) { return { 'method': 'getrawtransaction', 'params': [vin.txid] } }) digibyte.cmd(prevOutsBatch, function (rawTransaction, cb) { var prevTx = decodeRawTransaction(bitcoinjs.Transaction.fromHex(rawTransaction)) var txid = prevTx.id prevTxs.forEach(function(vin) { vin.previousOutput = prevTx.vout[vin.vout] if(vin.previousOutput && vin.previousOutput.scriptPubKey && vin.previousOutput.scriptPubKey.addresses) { vin.previousOutput.addresses = vin.previousOutput.scriptPubKey.addresses } }) cb() }, function(err) { if (err) return cb(err) transaction.fee = transaction.vin.reduce(function(sum, vin) { if (vin.previousOutput) { return sum + vin.previousOutput.value } return sum }, 0) - transaction.vout.reduce(function(sum, vout) { return sum + vout.value }, 0) transaction.totalsent = transaction.vin.reduce(function(sum, vin) { if (vin.previousOutput) { return sum + vin.previousOutput.value } return sum }, 0) cb(null, transaction) }) } var parseTransaction = function (transaction, utxosChanges, blockHeight, cb) { async.each(transaction.vin, function (input, cb) { var previousOutput = input.txid + ':' + input.vout if (utxosChanges.unused[previousOutput]) { input.assets = JSON.parse(utxosChanges.unused[previousOutput]) return process.nextTick(cb) } redis.hget('utxos', previousOutput, function (err, assets) { if (err) return cb(err) input.assets = assets && JSON.parse(assets) || [] if (input.assets.length) { utxosChanges.used[previousOutput] = assets } cb() }) }, function (err) { if (err) return cb(err) var outputsAssets = getAssetsOutputs(transaction) outputsAssets.forEach(function (assets, outputIndex) { if (assets && assets.length) { utxosChanges.unused[transaction.txid + ':' + outputIndex] = JSON.stringify(assets) } }) emitter.emit('newdatransaction', transaction) emitter.emit('newtransaction', transaction) cb() }) } var setTxos = function (utxos, cb) { async.each(Object.keys(utxos), function (utxo, cb) { var assets = utxos[utxo] redis.hmset('utxos', utxo, assets, cb) }, cb) } var updateLastBlock = function (blockHeight, blockHash, timestamp, cb) { if (typeof blockHash === 'function') { return redis.hmset('blocks', 'lastBlockHeight', blockHeight, blockHash) } redis.hmset('blocks', blockHeight, blockHash, 'lastBlockHeight', blockHeight, 'lastTimestamp', timestamp, function (err) { cb(err) }) } var updateUtxosChanges = function (block, utxosChanges, cb) { async.waterfall([ function (cb) { setTxos(utxosChanges.unused, cb) }, function (cb) { updateLastBlock(block.height, block.hash, block.timestamp, cb) } ], cb) } var updateParsedMempoolTxids = function (txids, cb) { async.waterfall([ function (cb) { redis.hget('mempool', 'parsed', cb) }, function (parsedMempool, cb) { parsedMempool = JSON.parse(parsedMempool || '[]') parsedMempool = parsedMempool.concat(txids) parsedMempool = _.uniq(parsedMempool) redis.hmset('mempool', 'parsed', JSON.stringify(parsedMempool), cb) } ], function (err) { cb(err) }) } var updateMempoolTransactionUtxosChanges = function (txid, utxosChanges, cb) { async.waterfall([ function (cb) { setTxos(utxosChanges.unused, cb) }, function (cb) { updateParsedMempoolTxids([txid], cb) } ], cb) } var decodeRawTransaction = function (tx) { var r = {} r['txid'] = tx.getId() r['version'] = tx.version r['locktime'] = tx.lock_time r['hex'] = tx.toHex() r['vin'] = [] r['vout'] = [] tx.ins.forEach(function (txin) { var txid = txin.hash.reverse().toString('hex') var n = txin.index var seq = txin.sequence var hex = txin.script.toString('hex') if (n == 4294967295) { r['vin'].push({'txid': txid, 'vout': n, 'coinbase' : hex, 'sequence' : seq}) } else { var asm = bitcoinjs.script.toASM(txin.script) r['vin'].push({'txid': txid, 'vout': n, 'scriptSig' : {'asm': asm, 'hex': hex}, 'sequence':seq}) } }) tx.outs.forEach(function (txout, i) { var value = txout.value var hex = txout.script.toString('hex') var asm = bitcoinjs.script.toASM(txout.script) var type = bitcoinjs.script.classifyOutput(txout.script) var addresses = [] if (~['pubkeyhash', 'scripthash'].indexOf(type)) { addresses.push(bitcoinjs.address.fromOutputScript(bitcoinjs.script.decompile(txout.script), digibyteNetwork)) } var answer = {'value' : value, 'n': i, 'scriptPubKey': {'asm': asm, 'hex': hex, 'addresses': addresses, 'type': type}} r['vout'].push(answer) }) var dadata = getColoredData(r) if (dadata) { r['dadata'] = [dadata] r['colored'] = true } return r } var parseNewBlock = function (block, cb) { info.datimestamp = block.timestamp info.daheight = block.height var utxosChanges = { used: {}, unused: {}, txids: [] } async.eachSeries(block.transactions, function (transaction, cb) { utxosChanges.txids.push(transaction.txid) var coloredData = getColoredData(transaction) if (!coloredData) { emitter.emit('newtransaction', transaction) return process.nextTick(cb) } transaction.dadata = [coloredData] parseTransaction(transaction, utxosChanges, block.height, cb) }, function (err) { if (err) return cb(err) updateUtxosChanges(block, utxosChanges, function (err) { if (err) return cb(err) block.transactions = block.transactions.map(transaction => transaction.txid) emitter.emit('newblock', block) cb() }) }) } var getMempoolTxids = function (cb) { digibyte.cmd('getrawmempool', [], cb) } var getNewMempoolTxids = function (mempoolTxids, cb) { redis.hget('mempool', 'parsed', function (err, mempool) { if (err) return cb(err) mempool = mempool || '[]' var parsedMempoolTxids = JSON.parse(mempool) newMempoolTxids = _.difference(mempoolTxids, parsedMempoolTxids) cb(null, newMempoolTxids) }) } var getNewMempoolTransaction = function (newMempoolTxids, cb) { var commandsArr = newMempoolTxids.map(function (txid) { return { method: 'getrawtransaction', params: [txid, 0]} }) var newMempoolTransactions = [] digibyte.cmd(commandsArr, function (rawTransaction, cb) { var newMempoolTransaction = decodeRawTransaction(bitcoinjs.Transaction.fromHex(rawTransaction)) newMempoolTransactions.push(newMempoolTransaction) cb() }, function (err) { cb(err, newMempoolTransactions) }) } var orderByDependencies = function (transactions) { var txids = {} transactions.forEach(function (transaction) { txids[transaction.txid] = transaction }) var edges = [] transactions.forEach(function (transaction) { transaction.vin.forEach(function (input) { if (txids[input.txid]) { edges.push([input.txid, transaction.txid]) } }) }) var sortedTxids = toposort.array(Object.keys(txids), edges) return sortedTxids.map(function (txid) { return txids[txid] } ) } var parseNewMempoolTransactions = function (newMempoolTransactions, cb) { newMempoolTransactions = orderByDependencies(newMempoolTransactions) var nonColoredTxids = [] async.eachSeries(newMempoolTransactions, function (newMempoolTransaction, cb) { var utxosChanges = { used: {}, unused: {} } var coloredData = getColoredData(newMempoolTransaction) if (!coloredData) { nonColoredTxids.push(newMempoolTransaction.txid) emitter.emit('newtransaction', newMempoolTransaction) return process.nextTick(cb) } newMempoolTransaction.dadata = [coloredData] parseTransaction(newMempoolTransaction, utxosChanges, -1, function (err) { if (err) return cb(err) updateMempoolTransactionUtxosChanges(newMempoolTransaction.txid, utxosChanges, cb) }) }, function (err) { if (err) return cb(err) updateParsedMempoolTxids(nonColoredTxids, cb) }) } var updateInfo = function (cb) { //console.log(info.daheight && info.datimestamp, info.daheight, info.datimestamp) if (info.daheight && info.datimestamp) { return process.nextTick(cb) } redis.hmget('blocks', 'lastBlockHeight', 'lastTimestamp', function (err, arr) { //console.log(err, arr); if (err) return cb(err) if (!arr || arr.length < 2) return process.nextTick(cb) info.daheight = arr[0] info.datimestamp = arr[1] cb() }) } var mempoolParse = function (cb) { // console.log('parsing mempool') async.waterfall([ updateInfo, getMempoolTxids, getNewMempoolTxids, getNewMempoolTransaction, parseNewMempoolTransactions ], cb) } var finishParsing = function (err) { if (err) console.error(err) parseProcedure() } var importAddresses = function (args, cb) { var addresses = args.addresses var reindex = args.reindex === 'true' || args.reindex === true var newAddresses var importedAddresses var ended = false var endFunc = function () { if (!ended) { ended = true return cb(null, { addresses: addresses, reindex: reindex, }) } } async.waterfall([ function (cb) { redis.hget('addresses', 'imported', cb) }, function (_importedAddresses, cb) { importedAddresses = _importedAddresses || '[]' importedAddresses = JSON.parse(importedAddresses) newAddresses = _.difference(addresses, importedAddresses) if (reindex && newAddresses.length < 2 || !newAddresses.length) return process.nextTick(cb) var commandsArr = newAddresses.splice(0, newAddresses.length - (reindex ? 1 : 0)).map(function (address) { return { method: 'importaddress', params: [address, label, false] } }) digibyte.cmd(commandsArr, function (ans, cb) { return process.nextTick(cb)}, cb) }, function (cb) { reindex = false if (!newAddresses.length) return process.nextTick(cb) reindex = true info.digibytedbusy = true digibyte.cmd('importaddress', [newAddresses[0], label, true], function (err) { waitForDigiByted(cb) }) endFunc() }, function (cb) { newAddresses = _.difference(addresses, importedAddresses) if (!newAddresses.length) return process.nextTick(cb) importedAddresses = importedAddresses.concat(newAddresses) redis.hmset('addresses', 'imported', JSON.stringify(importedAddresses), function (err) { cb(err) }) } ], function (err) { if (err) return cb(err) endFunc() }) } var parse = function (addresses, progressCallback) { if (typeof addresses === 'function') { progressCallback = addresses addresses = null } setInterval(function () { emitter.emit('info', info) if (progressCallback) { progressCallback(info) } }, 5000); if (!addresses || !Array.isArray(addresses)) return parseProcedure() importAddresses({addresses: addresses, reindex: true}, parseProcedure) } var infoPopulate = function (cb) { getDigiBytedInfo(function (err, newInfo) { if (err) return cb(err) info = newInfo cb() }) } var parseProcedure = function () { async.waterfall([ waitForDigiByted, infoPopulate, getNextBlockHeight, getNextBlock, checkNextBlock, conditionalParseNextBlock ], finishParsing) } var getAddressesUtxos = function (args, cb) { var addresses = args.addresses var numOfConfirmations = args.numOfConfirmations || 0 digibyte.cmd('getblockcount', [], function(err, count) { if (err) return cb(err) digibyte.cmd('listunspent', [numOfConfirmations, 99999999, addresses], function (err, utxos) { if (err) return cb(err) async.each(utxos, function (utxo, cb) { redis.hget('utxos', utxo.txid + ':' + utxo.vout, function (err, assets) { if (err) return cb(err) utxo.assets = assets && JSON.parse(assets) || [] if (utxo.confirmations) { utxo.blockheight = count - utxo.confirmations + 1 } else { utxo.blockheight = -1 } cb() }) }, function (err) { if (err) return cb(err) cb(null, utxos) }) }) }) } var getUtxos = function (args, cb) { var reqUtxos = args.utxos var numOfConfirmations = args.numOfConfirmations || 0 digibyte.cmd('getblockcount', [], function(err, count) { if (err) return cb(err) digibyte.cmd('listunspent', [numOfConfirmations, 99999999], function (err, utxos) { if (err) return cb(err) utxos = utxos.filter(utxo => reqUtxos.findIndex(reqUtxo => reqUtxo.txid === utxo.txid && reqUtxo.index === utxo.vout) !== -1) async.each(utxos, function (utxo, cb) { redis.hget('utxos', utxo.txid + ':' + utxo.vout, function (err, assets) { if (err) return cb(err) utxo.assets = assets && JSON.parse(assets) || [] if (utxo.confirmations) { utxo.blockheight = count - utxo.confirmations + 1 } else { utxo.blockheight = -1 } cb() }) }, function (err) { if (err) return cb(err) cb(null, utxos) }) }) }) } var transmit = function (args, cb) { var txHex = args.txHex digibyte.cmd('sendrawtransaction', [txHex], function(err, res) { if (err) { return cb(err) } var transaction = decodeRawTransaction(bitcoinjs.Transaction.fromHex(txHex)) var txsToParse = [transaction] var txsToCheck = [transaction] async.whilst( function() { return txsToCheck.length > 0 }, function(callback) { var txids = txsToCheck.map(function(tx) { return tx.vin.map(function(vin) { return vin.txid}) }) txids = [].concat.apply([], txids) txids = [...new Set(txids)] txsToCheck = [] getNewMempoolTxids(txids, function(err, txids) { if (err) return callback(err) if (txids.length == 0) return callback() var batch = txids.map(function(txid) { return { 'method': 'getrawtransaction', 'params': [txid] } }) digibyte.cmd( batch, function (rawTransaction, cb) { var tx = decodeRawTransaction(bitcoinjs.Transaction.fromHex(rawTransaction)) txsToCheck.push(tx) txsToParse.unshift(tx) }, function(err) { if (err) return callback(err) return callback() } ) }) }, function (err) { if (err) return cb(null, '{ "txid": "' + res + '" }') parseNewMempoolTransactions(txsToParse, function(err) { if (err) return cb(null, '{ "txid": "' + res + '" }') return cb(null, '{ "txid": "' + res + '" }') }) } ) }) } var addColoredInputs = function (transaction, cb) { async.each(transaction.vin, function (input, cb) { redis.hget('utxos', input.txid + ':' + input.vout, function (err, assets) { if (err) return cb(err) assets = assets && JSON.parse(assets) || [] input.assets = assets cb() }) }, function (err) { if (err) return cb(err) cb(null, transaction) }) } var addColoredOutputs = function (transaction, cb) { async.each(transaction.vout, function (output, cb) { redis.hget('utxos', transaction.txid + ':' + output.n, function (err, assets) { if (err) return cb(err) assets = assets && JSON.parse(assets) || [] output.assets = assets cb() }) }, function (err) { if (err) return cb(err) cb(null, transaction) }) } var addColoredIOs = function (transaction, cb) { async.waterfall([ function (cb) { addColoredInputs(transaction, cb) }, function (transaction, cb) { addColoredOutputs(transaction, cb) } ], cb) } var getAddressesTransactions = function (args, cb) { var addresses = args.addresses var next = true var txs = {} var txids = [] var skip = 0 var count = 10 var transactions = {} async.whilst(function () { return next }, function (cb) { digibyte.cmd('listtransactions', [label, count, skip, true], function (err, transactions) { if (err) return cb(err) skip+=count transactions.forEach(function (transaction) { if (~addresses.indexOf(transaction.address) && !~txids.indexOf(transaction.txid)) { txs[transaction.txid] = transaction txids.push(transaction.txid) } }) if (transactions.length < count) { next = false } cb() }) }, function (err) { if (err) return cb(err) var batch = txids.map(function(txid) { return { 'method': 'getrawtransaction', 'params': [txid] } }) digibyte.cmd('getblockcount', [], function(err, count) { if (err) return cb(err) digibyte.cmd(batch, function (rawTransaction, cb) { var transaction = decodeRawTransaction(bitcoinjs.Transaction.fromHex(rawTransaction)) var tx = txs[transaction.txid] addColoredIOs(transaction, function(err) { transaction.confirmations = tx.confirmations if (transaction.confirmations) { transaction.blockheight = count - transaction.confirmations + 1 transaction.blocktime = tx.blocktime * 1000 } else { transaction.blockheight = -1 transaction.blocktime = tx.timereceived * 1000 } transactions[transaction.txid] = transaction cb() }) }, function(err) { if (err) return cb(err) var prevOutputIndex = {} Object.values(transactions).forEach(function(tx) { tx.vin.forEach(function(vin) { prevOutputIndex[vin.txid] = prevOutputIndex[vin.txid] || [] prevOutputIndex[vin.txid].push(vin) }) }) var prevOutsBatch = Object.keys(prevOutputIndex).map(function(txid) { return { 'method': 'getrawtransaction', 'params': [txid] } }) digibyte.cmd(prevOutsBatch, function (rawTransaction, cb) { var transaction = decodeRawTransaction(bitcoinjs.Transaction.fromHex(rawTransaction)) var txid = transaction.id prevOutputIndex[transaction.txid].forEach(function(vin) { vin.previousOutput = transaction.vout[vin.vout] if(vin.previousOutput.scriptPubKey && vin.previousOutput.scriptPubKey.addresses) { vin.previousOutput.addresses = vin.previousOutput.scriptPubKey.addresses } }) cb() }, function(err) { if (err) return cb(err) Object.values(transactions).forEach(function(tx) { tx.fee = tx.vin.reduce(function(sum, vin) { return sum + vin.previousOutput.value }, 0) - tx.vout.reduce(function(sum, vout) { return sum+ vout.value }, 0) tx.totalsent = tx.vin.reduce(function(sum, vin) { return sum + vin.previousOutput.value }, 0) }) cb(null, Object.values(transactions)) }) }) }) }) } var getDigiBytedInfo = function (cb) { var btcInfo async.waterfall([ function (cb) { digibyte.cmd('getblockchaininfo', [], cb) }, function (_btcInfo, cb) { if (typeof _btcInfo === 'function') { cb = _btcInfo _btcInfo = null } if (!_btcInfo) return cb('No reply from getinfo') btcInfo = _btcInfo digibyte.cmd('getblockhash', [btcInfo.blocks], cb) }, function (lastBlockHash, cb) { digibyte.cmd('getblock', [lastBlockHash], cb) } ], function (err, lastBlockInfo) { if (err) return cb(err) btcInfo.timestamp = lastBlockInfo.time btcInfo.datimestamp = info.datimestamp btcInfo.daheight = info.daheight cb(null, btcInfo) }) } var getInfo = function (args, cb) { if (typeof args === 'function') { cb = args args = null } cb(null, info) } var injectColoredUtxos = function (method, params, ans, cb) { // TODO cb(null, ans) } var proxyDigiByteD = function (method, params, cb) { digibyte.cmd(method, params, function (err, ans) { if (err) return cb(err) injectColoredUtxos(method, params, ans, cb) }) } return { parse: parse, importAddresses: importAddresses, getAddressesUtxos: getAddressesUtxos, getUtxos: getUtxos, getAddressesTransactions: getAddressesTransactions, transmit: transmit, getInfo: getInfo, proxyDigiByteD: proxyDigiByteD, emitter: emitter } }