diff --git a/docs/services/bitcoind.md b/docs/services/bitcoind.md index 8251aea8..50f11064 100644 --- a/docs/services/bitcoind.md +++ b/docs/services/bitcoind.md @@ -13,7 +13,7 @@ The bitcoin service adds a native interface to Bitcoin Core for querying informa - `bitcoind.sendTransaction(transaction, allowAbsurdFees)` - Will attempt to add a transaction to the mempool and broadcast to peers. - `bitcoind.getTransaction(txid, queryMempool, callback)` - Get any tx asynchronously by reading it from disk, with an argument to optionally not include the mempool. - `bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback)` - Similar to getTransaction but will also include the block timestamp and height. -- `bitcoind.getMempoolOutputs(address)` - Will return an array of outputs that match an address from the mempool. +- `bitcoind.getMempoolTransactions()` - Will return an array of transaction buffers. - `bitcoind.getInfo()` - Basic information about the chain including total number of blocks. - `bitcoind.isSynced()` - Returns a boolean if the daemon is fully synced (not the initial block download) - `bitcoind.syncPercentage()` - Returns the current estimate of blockchain download as a percentage. diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 4fcbafa4..5d070b1d 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -661,5 +661,39 @@ describe('Node Functionality', function() { }); }); + + describe('Mempool Index', function() { + var unspentOutput; + before(function(done) { + node.services.address.getUnspentOutputs(address, false, function(err, results) { + if (err) { + throw err; + } + results.length.should.equal(1); + unspentOutput = results[0]; + done(); + }); + }); + + it('will update the mempool index after new tx', function(done) { + + var tx = new Transaction(); + tx.from(unspentOutput); + tx.to(address, unspentOutput.satoshis - 1000); + tx.fee(1000); + tx.sign(testKey); + + node.services.bitcoind.sendTransaction(tx.serialize()); + + setImmediate(function() { + var length = node.services.address.mempoolIndex[address].length; + length.should.equal(1); + should.exist(node.services.address.mempoolIndex[address]); + done(); + }); + + }); + + }); }); }); diff --git a/integration/regtest.js b/integration/regtest.js index 3f84b772..76582ea8 100644 --- a/integration/regtest.js +++ b/integration/regtest.js @@ -300,7 +300,7 @@ describe('Daemon Binding Functionality', function() { var serialized = tx.serialize(); - bitcoind.on('tx', function(result) { + bitcoind.once('tx', function(result) { result.buffer.toString('hex').should.equal(serialized); result.hash.should.equal(tx.hash); result.mempool.should.equal(true); @@ -357,6 +357,18 @@ describe('Daemon Binding Functionality', function() { tx.change(changeAddress); tx.sign(privateKey1); + var tx2; + var tx2Key; + + before(function() { + tx2 = bitcore.Transaction(); + tx2.from(utxos[3]); + tx2.change(privateKey.toAddress()); + tx2.to(destKey.toAddress(), utxos[3].amount * 1e8 - 1000); + tx2Key = bitcore.PrivateKey.fromWIF(utxos[3].privateKeyWIF); + tx2.sign(tx2Key); + }); + it('will add an unchecked transaction', function() { var added = bitcoind.addMempoolUncheckedTransaction(tx.serialize()); added.should.equal(true); @@ -370,18 +382,17 @@ describe('Daemon Binding Functionality', function() { }); - it('get outputs by address', function() { - var outputs = bitcoind.getMempoolOutputs(changeAddress); - var expected = [ - { - address: 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up', - script: '76a914073b7eae2823efa349e3b9155b8a735526463a0f88ac', - satoshis: 40000, - txid: tx.hash, - outputIndex: 1 - } - ]; - outputs.should.deep.equal(expected); + it('get one transaction', function() { + var transactions = bitcoind.getMempoolTransactions(); + transactions[0].toString('hex').should.equal(tx.serialize()); + }); + + it('get multiple transactions', function() { + bitcoind.sendTransaction(tx2.serialize()); + var transactions = bitcoind.getMempoolTransactions(); + var expected = [tx.serialize(), tx2.serialize()]; + expected.should.contain(transactions[0].toString('hex')); + expected.should.contain(transactions[1].toString('hex')); }); }); diff --git a/lib/services/address/index.js b/lib/services/address/index.js index 60a935ac..8dc8ed81 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -6,6 +6,7 @@ var async = require('async'); var index = require('../../'); var log = index.log; var errors = index.errors; +var Transaction = require('../../transaction'); var bitcore = require('bitcore'); var $ = bitcore.util.preconditions; var _ = bitcore.deps._; @@ -25,6 +26,8 @@ var AddressService = function(options) { this.node.services.bitcoind.on('tx', this.transactionHandler.bind(this)); + this.mempoolIndex = {}; + }; inherits(AddressService, BaseService); @@ -128,11 +131,67 @@ AddressService.prototype.transactionHandler = function(txInfo) { this.transactionOutputHandler(messages, tx, i, !txInfo.mempool); } + // Update mempool index + if (txInfo.mempool) { + this.updateMempoolIndex(tx); + } + for (var key in messages) { this.transactionEventHandler(messages[key]); } }; +AddressService.prototype.updateMempoolIndex = function(tx) { + var outputLength = tx.outputs.length; + for (var outputIndex = 0; outputIndex < outputLength; outputIndex++) { + var output = tx.outputs[outputIndex]; + if (!output.script) { + continue; + } + var addressInfo = this._extractAddressInfoFromScript(output.script); + if (!addressInfo) { + continue; + } + + var addressStr = bitcore.Address({ + hashBuffer: addressInfo.hashBuffer, + type: addressInfo.addressType, + network: this.node.network + }).toString(); + + if (!this.mempoolIndex[addressStr]) { + this.mempoolIndex[addressStr] = []; + } + + this.mempoolIndex[addressStr].push({ + txid: tx.hash, // TODO use buffer + outputIndex: outputIndex, + satoshis: output.satoshis, + script: output._scriptBuffer.toString('hex') //TODO use a buffer + }); + + } + +}; + +AddressService.prototype.resetMempoolIndex = function(callback) { + log.info('Starting reset of mempool index for address service.'); + var self = this; + var transactionBuffers = self.node.services.bitcoind.getMempoolTransactions(); + this.mempoolIndex = {}; + async.each(transactionBuffers, function(txBuffer, next) { + var tx = Transaction.fromBuffer(txBuffer); + this.updateMempoolIndex(tx); + setImmediate(next); + }, function(err) { + if (err) { + return callback(err); + } + log.info('Mempool index update with address size:', Object.keys(self.mempoolIndex).length); + callback(); + }); +}; + AddressService.prototype._extractAddressInfoFromScript = function(script) { var hashBuffer; var addressType; @@ -635,11 +694,11 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { var output = { address: addressStr, - txid: key.txid.toString('hex'), + txid: key.txid.toString('hex'), //TODO use a buffer outputIndex: key.outputIndex, height: key.height, satoshis: value.satoshis, - script: value.scriptBuffer.toString('hex'), + script: value.scriptBuffer.toString('hex'), //TODO use a buffer confirmations: self.node.services.db.tip.__height - key.height + 1 }; @@ -661,7 +720,17 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { } if(options.queryMempool) { - outputs = outputs.concat(self.node.services.bitcoind.getMempoolOutputs(addressStr)); + var mempoolOutputs = self.mempoolIndex[addressStr]; + if (mempoolOutputs) { + for(var i = 0; i < mempoolOutputs.length; i++) { + // TODO copy + var newOutput = mempoolOutputs[i]; + newOutput.address = addressStr; + newOutput.height = -1; + newOutput.confirmations = 0; + outputs.push(newOutput); + } + } } callback(null, outputs); }); diff --git a/lib/services/bitcoind.js b/lib/services/bitcoind.js index 2eca3cb4..f0ddecd0 100644 --- a/lib/services/bitcoind.js +++ b/lib/services/bitcoind.js @@ -1,12 +1,13 @@ 'use strict'; +var fs = require('fs'); var util = require('util'); var bindings = require('bindings')('bitcoind.node'); var mkdirp = require('mkdirp'); -var fs = require('fs'); +var async = require('async'); var bitcore = require('bitcore'); +var Transaction = require('../transaction'); var $ = bitcore.util.preconditions; -var _ = bitcore.deps._; var index = require('../'); var log = index.log; var Service = require('../service'); @@ -215,8 +216,8 @@ Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, cal return bindings.getTransactionWithBlockInfo(txid, queryMempool, callback); }; -Bitcoin.prototype.getMempoolOutputs = function(address) { - return bindings.getMempoolOutputs(address); +Bitcoin.prototype.getMempoolTransactions = function() { + return bindings.getMempoolTransactions(); }; Bitcoin.prototype.addMempoolUncheckedTransaction = function(txBuffer) { diff --git a/lib/services/db.js b/lib/services/db.js index faf42c8b..99f80443 100644 --- a/lib/services/db.js +++ b/lib/services/db.js @@ -408,6 +408,20 @@ DB.prototype.disconnectBlock = function(block, callback) { this.runAllBlockHandlers(block, false, callback); }; +DB.prototype.runAllMempoolIndexes = function(callback) { + async.eachSeries( + this.node.services, + function(service, next) { + if (service.resetMempoolIndex) { + service.resetMempoolIndex(next); + } else { + setImmediate(next); + } + }, + callback + ); +}; + /** * Will collect all database operations for a block from other services * and save to the database. @@ -670,15 +684,22 @@ DB.prototype.sync = function() { return self.node.emit('error', err); } - self.bitcoindSyncing = false; - if(self.node.stopping) { return; } - // If bitcoind is completely synced if (self.node.services.bitcoind.isSynced()) { - self.node.emit('synced'); + self.runAllMempoolIndexes(function(err) { + if (err) { + Error.captureStackTrace(err); + return self.node.emit('error', err); + } + + self.bitcoindSyncing = false; + self.node.emit('synced'); + }); + } else { + self.bitcoindSyncing = false; } }); diff --git a/src/libbitcoind.cc b/src/libbitcoind.cc index 5f94d9e7..6759b9f3 100644 --- a/src/libbitcoind.cc +++ b/src/libbitcoind.cc @@ -1531,84 +1531,38 @@ NAN_METHOD(SendTransaction) { } /** - * GetMempoolOutputs - * bitcoindjs.getMempoolOutputs() - * Will return outputs by address from the mempool. + * GetMempoolTransactions + * bitcoind.getMempoolTransactions() + * Will return an array of transaction buffers. */ -NAN_METHOD(GetMempoolOutputs) { - Isolate* isolate = Isolate::GetCurrent(); +NAN_METHOD(GetMempoolTransactions) { + Isolate* isolate = args.GetIsolate(); HandleScope scope(isolate); - // Instatiate an empty array that we will fill later - // with matching outputs. - Local outputs = Array::New(isolate); + Local transactions = Array::New(isolate); int arrayIndex = 0; - // Decode the input address into the hash bytes - // that we can then match to the scriptPubKeys data - v8::String::Utf8Value param1(args[0]->ToString()); - std::string *input = new std::string(*param1); - const char* psz = input->c_str(); - std::vector vAddress; - DecodeBase58(psz, vAddress); - vector hashBytes(vAddress.begin()+1, vAddress.begin()+21); + { + LOCK(mempool.cs); - // Iterate through the entire mempool - std::map mapTx = mempool.mapTx; + // Iterate through the entire mempool + std::map mapTx = mempool.mapTx; - for(std::map::iterator it = mapTx.begin(); it != mapTx.end(); it++) { - - uint256 txid = it->first; - CTxMemPoolEntry entry = it->second; - const CTransaction tx = entry.GetTx(); - - int outputIndex = 0; - - // Iterate through each output - BOOST_FOREACH(const CTxOut& txout, tx.vout) { - - CScript script = txout.scriptPubKey; - - txnouttype type; - std::vector > hashResults; - - if (Solver(script, type, hashResults)) { - - // See if the script is any of the standard address types - if (type == TX_PUBKEYHASH || type == TX_SCRIPTHASH) { - - vector scripthashBytes = hashResults.front(); - - // Compare the hash bytes with the input hash bytes - if(equal(hashBytes.begin(), hashBytes.end(), scripthashBytes.begin())) { - - Local output = NanNew(); - - output->Set(NanNew("address"), NanNew(psz)); - - std::string scriptHex = HexStr(script.begin(), script.end()); - output->Set(NanNew("script"), NanNew(scriptHex)); - - uint64_t satoshis = txout.nValue; - output->Set(NanNew("satoshis"), NanNew(satoshis)); // can't go above 2 ^ 53 -1 - output->Set(NanNew("txid"), NanNew(txid.GetHex())); - - output->Set(NanNew("outputIndex"), NanNew(outputIndex)); - - // We have a match and push the results to the array - // that is returned as the result - outputs->Set(arrayIndex, output); - arrayIndex++; - - } - } - } - - outputIndex++; + for(std::map::iterator it = mapTx.begin(); + it != mapTx.end(); + it++) { + CTxMemPoolEntry entry = it->second; + const CTransaction tx = entry.GetTx(); + CDataStream dataStreamTx(SER_NETWORK, PROTOCOL_VERSION); + dataStreamTx << tx; + std::string txString = dataStreamTx.str(); + Local txBuffer = node::Buffer::New(isolate, txString.c_str(), txString.size()); + transactions->Set(arrayIndex, txBuffer); + arrayIndex++; } } - NanReturnValue(outputs); + NanReturnValue(transactions); } @@ -1668,7 +1622,7 @@ init(Handle target) { NODE_SET_METHOD(target, "isSpent", IsSpent); NODE_SET_METHOD(target, "getBlockIndex", GetBlockIndex); NODE_SET_METHOD(target, "isMainChain", IsMainChain); - NODE_SET_METHOD(target, "getMempoolOutputs", GetMempoolOutputs); + NODE_SET_METHOD(target, "getMempoolTransactions", GetMempoolTransactions); NODE_SET_METHOD(target, "addMempoolUncheckedTransaction", AddMempoolUncheckedTransaction); NODE_SET_METHOD(target, "sendTransaction", SendTransaction); NODE_SET_METHOD(target, "estimateFee", EstimateFee); diff --git a/src/libbitcoind.h b/src/libbitcoind.h index 2d05341f..45010446 100644 --- a/src/libbitcoind.h +++ b/src/libbitcoind.h @@ -29,7 +29,7 @@ NAN_METHOD(GetTransaction); NAN_METHOD(GetInfo); NAN_METHOD(IsSpent); NAN_METHOD(GetBlockIndex); -NAN_METHOD(GetMempoolOutputs); +NAN_METHOD(GetMempoolTransactions); NAN_METHOD(AddMempoolUncheckedTransaction); NAN_METHOD(SendTransaction); NAN_METHOD(EstimateFee);