diff --git a/integration/regtest-node.js b/integration/regtest-node.js index 8b55db7f..fb7ff6bf 100644 --- a/integration/regtest-node.js +++ b/integration/regtest-node.js @@ -726,8 +726,9 @@ describe('Node Functionality', function() { node.services.bitcoind.sendTransaction(tx.serialize()); setImmediate(function() { - var hashBuffer = bitcore.Address(address).hashBuffer; - node.services.address._getOutputsMempool(address, hashBuffer, function(err, outs) { + var addrObj = node.services.address._getAddressInfo(address); + node.services.address._getOutputsMempool(address, addrObj.hashBuffer, + addrObj.hashTypeBuffer, function(err, outs) { if (err) { throw err; } diff --git a/lib/services/address/index.js b/lib/services/address/index.js index a557ba24..ef7e3e2c 100644 --- a/lib/services/address/index.js +++ b/lib/services/address/index.js @@ -70,6 +70,27 @@ AddressService.MEMPREFIXES = { SPENTSMAP: new Buffer('03', 'hex') // Query mempool for the input that spends an output }; +// To save space, we're only storing the PubKeyHash or ScriptHash in our index. +// To avoid intentional unspendable collisions, which have been seen on the blockchain, +// we must store the hash type (PK or Script) as well. +AddressService.HASH_TYPES = { + PUBKEY: new Buffer('01', 'hex'), + REDEEMSCRIPT: new Buffer('02', 'hex') +}; + +// Translates from our enum type back into the hash types returned by +// bitcore-lib/address. +AddressService.HASH_TYPES_READABLE = { + '01': 'pubkeyhash', + '02': 'scripthash' +}; + +// Trnaslates from address types to our enum type. +AddressService.HASH_TYPES_MAP = { + 'pubkeyhash': AddressService.HASH_TYPES.PUBKEY, + 'scripthash': AddressService.HASH_TYPES.REDEEMSCRIPT +}; + AddressService.SPACER_MIN = new Buffer('00', 'hex'); AddressService.SPACER_MAX = new Buffer('ff', 'hex'); @@ -303,6 +324,7 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { var outKey = Buffer.concat([ AddressService.MEMPREFIXES.OUTPUTS, addressInfo.hashBuffer, + addressInfo.hashTypeBuffer, txidBuffer, outputIndexBuffer ]); @@ -355,16 +377,20 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { // Update input index var inputHashBuffer; + var inputHashType; if (input.script.isPublicKeyHashIn()) { inputHashBuffer = Hash.sha256ripemd160(input.script.chunks[1].buf); + inputHashType = AddressService.HASH_TYPES.PUBKEY; } else if (input.script.isScriptHashIn()) { inputHashBuffer = Hash.sha256ripemd160(input.script.chunks[input.script.chunks.length - 1].buf); + inputHashType = AddressService.HASH_TYPES.REDEEMSCRIPT; } else { continue; } var inputKey = Buffer.concat([ AddressService.MEMPREFIXES.SPENTS, inputHashBuffer, + inputHashType, input.prevTxId, inputOutputIndexBuffer ]); @@ -389,7 +415,6 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { } this.mempoolIndex.batch(operations, callback); - }; /** @@ -401,16 +426,20 @@ AddressService.prototype.updateMempoolIndex = function(tx, add, callback) { AddressService.prototype._extractAddressInfoFromScript = function(script) { var hashBuffer; var addressType; + var hashTypeBuffer; if (script.isPublicKeyHashOut()) { hashBuffer = script.chunks[2].buf; + hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; addressType = Address.PayToPublicKeyHash; } else if (script.isScriptHashOut()) { hashBuffer = script.chunks[1].buf; + hashTypeBuffer = AddressService.HASH_TYPES.REDEEMSCRIPT; addressType = Address.PayToScriptHash; } else if (script.isPublicKeyOut()) { var pubkey = script.chunks[0].buf; var address = Address.fromPublicKey(new PublicKey(pubkey), this.node.network); hashBuffer = address.hashBuffer; + hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; // pay-to-publickey doesn't have an address, however for compatibility // purposes, we can create an address addressType = Address.PayToPublicKeyHash; @@ -419,6 +448,7 @@ AddressService.prototype._extractAddressInfoFromScript = function(script) { } return { hashBuffer: hashBuffer, + hashTypeBuffer: hashTypeBuffer, addressType: addressType }; }; @@ -474,7 +504,8 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { // can have a time that is previous to the previous block (however not // less than the mean of the 11 previous blocks) and not greater than 2 // hours in the future. - var key = this._encodeOutputKey(addressInfo.hashBuffer, height, txidBuffer, outputIndex); + var key = this._encodeOutputKey(addressInfo.hashBuffer, addressInfo.hashTypeBuffer, + height, txidBuffer, outputIndex); var value = this._encodeOutputValue(output.satoshis, output._scriptBuffer); operations.push({ type: action, @@ -514,11 +545,14 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { var input = inputs[inputIndex]; var inputHash; + var inputHashType; if (input.script.isPublicKeyHashIn()) { inputHash = Hash.sha256ripemd160(input.script.chunks[1].buf); + inputHashType = AddressService.HASH_TYPES.PUBKEY; } else if (input.script.isScriptHashIn()) { inputHash = Hash.sha256ripemd160(input.script.chunks[input.script.chunks.length - 1].buf); + inputHashType = AddressService.HASH_TYPES.REDEEMSCRIPT; } else { continue; } @@ -526,7 +560,7 @@ AddressService.prototype.blockHandler = function(block, addOutput, callback) { var prevTxIdBuffer = new Buffer(input.prevTxId, 'hex'); // To be able to query inputs by address and spent height - var inputKey = this._encodeInputKey(inputHash, height, prevTxIdBuffer, input.outputIndex); + var inputKey = this._encodeInputKey(inputHash, inputHashType, height, prevTxIdBuffer, input.outputIndex); var inputValue = this._encodeInputValue(txidBuffer, inputIndex); operations.push({ @@ -563,7 +597,7 @@ AddressService.prototype._encodeSpentIndexSyncKey = function(txidBuffer, outputI return key.toString('binary'); }; -AddressService.prototype._encodeOutputKey = function(hashBuffer, height, txidBuffer, outputIndex) { +AddressService.prototype._encodeOutputKey = function(hashBuffer, hashTypeBuffer, height, txidBuffer, outputIndex) { var heightBuffer = new Buffer(4); heightBuffer.writeUInt32BE(height); var outputIndexBuffer = new Buffer(4); @@ -571,6 +605,7 @@ AddressService.prototype._encodeOutputKey = function(hashBuffer, height, txidBuf var key = Buffer.concat([ AddressService.PREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, heightBuffer, txidBuffer, @@ -583,6 +618,7 @@ AddressService.prototype._decodeOutputKey = function(buffer) { var reader = new BufferReader(buffer); var prefix = reader.read(1); var hashBuffer = reader.read(20); + var hashTypeBuffer = reader.read(1); var spacer = reader.read(1); var height = reader.readUInt32BE(); var txid = reader.read(32); @@ -590,6 +626,7 @@ AddressService.prototype._decodeOutputKey = function(buffer) { return { prefix: prefix, hashBuffer: hashBuffer, + hashTypeBuffer: hashTypeBuffer, height: height, txid: txid, outputIndex: outputIndex @@ -611,7 +648,7 @@ AddressService.prototype._decodeOutputValue = function(buffer) { }; }; -AddressService.prototype._encodeInputKey = function(hashBuffer, height, prevTxIdBuffer, outputIndex) { +AddressService.prototype._encodeInputKey = function(hashBuffer, hashTypeBuffer, height, prevTxIdBuffer, outputIndex) { var heightBuffer = new Buffer(4); heightBuffer.writeUInt32BE(height); var outputIndexBuffer = new Buffer(4); @@ -619,6 +656,7 @@ AddressService.prototype._encodeInputKey = function(hashBuffer, height, prevTxId return Buffer.concat([ AddressService.PREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, heightBuffer, prevTxIdBuffer, @@ -630,6 +668,7 @@ AddressService.prototype._decodeInputKey = function(buffer) { var reader = new BufferReader(buffer); var prefix = reader.read(1); var hashBuffer = reader.read(20); + var hashTypeBuffer = reader.read(1); var spacer = reader.read(1); var height = reader.readUInt32BE(); var prevTxId = reader.read(32); @@ -637,6 +676,7 @@ AddressService.prototype._decodeInputKey = function(buffer) { return { prefix: prefix, hashBuffer: hashBuffer, + hashTypeBuffer: hashTypeBuffer, height: height, prevTxId: prevTxId, outputIndex: outputIndex @@ -698,6 +738,17 @@ AddressService.prototype._decodeInputValueMap = function(buffer) { }; }; +AddressService.prototype._getAddressInfo = function(addressStr) { + var addrObj = bitcore.Address(addressStr); + var hashTypeBuffer = AddressService.HASH_TYPES_MAP[addrObj.type]; + + return { + hashBuffer: addrObj.hashBuffer, + hashTypeBuffer: hashTypeBuffer, + hashTypeReadable: addrObj.type + }; +}; + /** * This function is responsible for emitting events to any subscribers to the * `address/transaction` event. @@ -902,6 +953,7 @@ AddressService.prototype.getInputForOutput = function(txid, outputIndex, options /** * Will give inputs that spend previous outputs for an address as an object with: * address - The base58check encoded address + * hashType - The type of the address, e.g. 'pubkeyhash' or 'scripthash' * txid - A string of the transaction hash * outputIndex - A number of corresponding transaction input * height - The height of the block the transaction was included, will be -1 for mempool transactions @@ -921,7 +973,12 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { var inputs = []; var stream; - var hashBuffer = bitcore.Address(addressStr).hashBuffer; + var addrObj = this._getAddressInfo(addressStr); + var hashBuffer = addrObj.hashBuffer; + var hashTypeBuffer = addrObj.hashTypeBuffer; + if (!hashTypeBuffer) { + return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr)); + } if (options.start && options.end) { @@ -935,12 +992,14 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { gte: Buffer.concat([ AddressService.PREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, endBuffer ]), lte: Buffer.concat([ AddressService.PREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, startBuffer ]), @@ -948,7 +1007,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { keyEncoding: 'binary' }); } else { - var allKey = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer]); + var allKey = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer]); stream = this.node.services.db.store.createReadStream({ gte: Buffer.concat([allKey, AddressService.SPACER_MIN]), lte: Buffer.concat([allKey, AddressService.SPACER_MAX]), @@ -964,6 +1023,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { var input = { address: addressStr, + hashType: addrObj.hashTypeReadable, txid: value.txid.toString('hex'), inputIndex: value.inputIndex, height: key.height, @@ -988,7 +1048,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { } if(options.queryMempool) { - self._getInputsMempool(addressStr, hashBuffer, function(err, mempoolInputs) { + self._getInputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolInputs) { if (err) { return callback(err); } @@ -1005,7 +1065,7 @@ AddressService.prototype.getInputs = function(addressStr, options, callback) { }; -AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, callback) { +AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { var self = this; var mempoolInputs = []; @@ -1013,11 +1073,13 @@ AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, ca gte: Buffer.concat([ AddressService.MEMPREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN ]), lte: Buffer.concat([ AddressService.MEMPREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MAX ]), valueEncoding: 'binary', @@ -1029,6 +1091,7 @@ AddressService.prototype._getInputsMempool = function(addressStr, hashBuffer, ca var inputIndex = data.value.readUInt32BE(32); var output = { address: addressStr, + hashType: AddressService.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], txid: txid.toString('hex'), //TODO use a buffer inputIndex: inputIndex, height: -1, @@ -1082,6 +1145,7 @@ AddressService.prototype._getSpentMempool = function(txidBuffer, outputIndex, ca /** * Will give outputs for an address as an object with: * address - The base58check encoded address + * hashType - The type of the address, e.g. 'pubkeyhash' or 'scripthash' * txid - A string of the transaction hash * outputIndex - A number of corresponding transaction output * height - The height of the block the transaction was included, will be -1 for mempool transactions @@ -1101,7 +1165,12 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { $.checkArgument(_.isObject(options), 'Second argument is expected to be an options object.'); $.checkArgument(_.isFunction(callback), 'Third argument is expected to be a callback function.'); - var hashBuffer = bitcore.Address(addressStr).hashBuffer; + var addrObj = this._getAddressInfo(addressStr); + var hashBuffer = addrObj.hashBuffer; + var hashTypeBuffer = addrObj.hashTypeBuffer; + if (!hashTypeBuffer) { + return callback(new Error('Unknown address type: ' + addrObj.hashTypeReadable + ' for address: ' + addressStr)); + } var outputs = []; var stream; @@ -1117,12 +1186,14 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { gte: Buffer.concat([ AddressService.PREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, endBuffer ]), lte: Buffer.concat([ AddressService.PREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN, startBuffer ]), @@ -1130,7 +1201,7 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { keyEncoding: 'binary' }); } else { - var allKey = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer]); + var allKey = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer]); stream = this.node.services.db.store.createReadStream({ gte: Buffer.concat([allKey, AddressService.SPACER_MIN]), lte: Buffer.concat([allKey, AddressService.SPACER_MAX]), @@ -1146,6 +1217,7 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { var output = { address: addressStr, + hashType: addrObj.hashTypeReadable, txid: key.txid.toString('hex'), //TODO use a buffer outputIndex: key.outputIndex, height: key.height, @@ -1172,7 +1244,7 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { } if(options.queryMempool) { - self._getOutputsMempool(addressStr, hashBuffer, function(err, mempoolOutputs) { + self._getOutputsMempool(addressStr, hashBuffer, hashTypeBuffer, function(err, mempoolOutputs) { if (err) { return callback(err); } @@ -1188,7 +1260,7 @@ AddressService.prototype.getOutputs = function(addressStr, options, callback) { }; -AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, callback) { +AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, hashTypeBuffer, callback) { var self = this; var mempoolOutputs = []; @@ -1196,11 +1268,13 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, c gte: Buffer.concat([ AddressService.MEMPREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MIN ]), lte: Buffer.concat([ AddressService.MEMPREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, AddressService.SPACER_MAX ]), valueEncoding: 'binary', @@ -1208,12 +1282,13 @@ AddressService.prototype._getOutputsMempool = function(addressStr, hashBuffer, c }); stream.on('data', function(data) { - // Format of data: prefix: 1, hashBuffer: 20, txid: 32, outputIndex: 4 - var txid = data.key.slice(21, 53); - var outputIndex = data.key.readUInt32BE(53); + // Format of data: prefix: 1, hashBuffer: 20, hashTypeBuffer: 1, txid: 32, outputIndex: 4 + var txid = data.key.slice(22, 54); + var outputIndex = data.key.readUInt32BE(54); var value = self._decodeOutputValue(data.value); var output = { address: addressStr, + hashType: AddressService.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')], txid: txid.toString('hex'), //TODO use a buffer outputIndex: outputIndex, height: -1, diff --git a/test/services/address/index.unit.js b/test/services/address/index.unit.js index e7660649..e7460a1d 100644 --- a/test/services/address/index.unit.js +++ b/test/services/address/index.unit.js @@ -343,7 +343,9 @@ describe('Address Service', function() { }); am.node.network = Networks.livenet; var address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - var hashHex = bitcore.Address(address).hashBuffer.toString('hex'); + var addrObj = bitcore.Address(address); + var hashHex = addrObj.hashBuffer.toString('hex'); + var hashType = addrObj.type; var messages = {}; am.transactionOutputHandler(messages, tx, 0, true); should.exist(messages[hashHex]); @@ -351,6 +353,7 @@ describe('Address Service', function() { message.tx.should.equal(tx); message.outputIndexes.should.deep.equal([0]); message.addressInfo.hashBuffer.toString('hex').should.equal(hashHex); + message.addressInfo.addressType.should.equal(hashType); message.addressInfo.hashHex.should.equal(hashHex); message.rejected.should.equal(true); }); @@ -446,16 +449,16 @@ describe('Address Service', function() { should.not.exist(err); operations.length.should.equal(151); operations[0].type.should.equal('put'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); + operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); operations[3].type.should.equal('put'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); + operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[4].type.should.equal('put'); operations[4].key.toString('hex').should.equal('053d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[4].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[121].type.should.equal('put'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); + operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); done(); }); @@ -472,13 +475,13 @@ describe('Address Service', function() { should.not.exist(err); operations.length.should.equal(151); operations[0].type.should.equal('del'); - operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); + operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b0100000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000'); operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac'); operations[3].type.should.equal('del'); - operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); + operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e0100000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020'); operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000'); operations[121].type.should.equal('del'); - operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); + operations[121].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c75280100000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001'); operations[121].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac'); done(); }); @@ -818,6 +821,7 @@ describe('Address Service', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 @@ -866,8 +870,9 @@ describe('Address Service', function() { end: 12, queryMempool: true }; - am._getInputsMempool = sinon.stub().callsArgWith(2, null, { + am._getInputsMempool = sinon.stub().callsArgWith(3, null, { address: address, + hashType: 'pubkeyhash', height: -1, confirmations: 0 }); @@ -890,9 +895,11 @@ describe('Address Service', function() { var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { - var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('000000000c', 'hex')]); + var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gte.toString('hex').should.equal(gte.toString('hex')); - var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('0000000010', 'hex')]); + var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, + hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lte.toString('hex').should.equal(lte.toString('hex')); createReadStreamCallCount++; return testStream; @@ -901,7 +908,7 @@ describe('Address Service', function() { am.node.services.bitcoind = { getMempoolInputs: sinon.stub().returns([]) }; - am._getInputsMempool = sinon.stub().callsArgWith(2, null, []); + am._getInputsMempool = sinon.stub().callsArgWith(3, null, []); am.getInputs(address, args, function(err, inputs) { should.not.exist(err); inputs.length.should.equal(1); @@ -913,7 +920,7 @@ describe('Address Service', function() { }); createReadStreamCallCount.should.equal(1); var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), + key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') }; testStream.emit('data', data); @@ -927,9 +934,9 @@ describe('Address Service', function() { var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { - var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('00', 'hex')]); + var gte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('00', 'hex')]); ops.gte.toString('hex').should.equal(gte.toString('hex')); - var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, new Buffer('ff', 'hex')]); + var lte = Buffer.concat([AddressService.PREFIXES.SPENTS, hashBuffer, hashTypeBuffer, new Buffer('ff', 'hex')]); ops.lte.toString('hex').should.equal(lte.toString('hex')); createReadStreamCallCount++; return testStream; @@ -949,7 +956,7 @@ describe('Address Service', function() { }); createReadStreamCallCount.should.equal(1); var data = { - key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), + key: new Buffer('33038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000000', 'hex') }; testStream.emit('data', data); @@ -979,6 +986,7 @@ describe('Address Service', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 @@ -1005,7 +1013,7 @@ describe('Address Service', function() { am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getInputsMempool(address, hashBuffer, function(err, outputs) { + am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); @@ -1021,11 +1029,13 @@ describe('Address Service', function() { am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getInputsMempool(address, hashBuffer, function(err, outputs) { + am._getInputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(1); outputs[0].address.should.equal(address); outputs[0].txid.should.equal(txid); + outputs[0].hashType.should.equal('pubkeyhash'); + outputs[0].hashType.should.equal(AddressService.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); outputs[0].inputIndex.should.equal(5); outputs[0].height.should.equal(-1); outputs[0].confirmations.should.equal(0); @@ -1099,6 +1109,7 @@ describe('Address Service', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W').hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 @@ -1135,20 +1146,22 @@ describe('Address Service', function() { var createReadStreamCallCount = 0; am.node.services.db.store = { createReadStream: function(ops) { - var gte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, new Buffer('000000000c', 'hex')]); + var gte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); ops.gte.toString('hex').should.equal(gte.toString('hex')); - var lte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, new Buffer('0000000010', 'hex')]); + var lte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); ops.lte.toString('hex').should.equal(lte.toString('hex')); createReadStreamCallCount++; return testStream; } }; - am._getOutputsMempool = sinon.stub().callsArgWith(2, null, []); + am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); am.getOutputs(address, args, function(err, outputs) { should.not.exist(err); outputs.length.should.equal(1); outputs[0].address.should.equal(address); outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); + outputs[0].hashType.should.equal('pubkeyhash'); + outputs[0].hashType.should.equal(AddressService.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); outputs[0].outputIndex.should.equal(1); outputs[0].satoshis.should.equal(4527773864); outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); @@ -1157,7 +1170,7 @@ describe('Address Service', function() { }); createReadStreamCallCount.should.equal(1); var data = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), + key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68701000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; testStream.emit('data', data); @@ -1170,10 +1183,11 @@ describe('Address Service', function() { createReadStream: sinon.stub().returns(readStream1) }; - am._getOutputsMempool = sinon.stub().callsArgWith(2, null, [ + am._getOutputsMempool = sinon.stub().callsArgWith(3, null, [ { address: address, height: -1, + hashType: 'pubkeyhash', confirmations: 0, txid: 'aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371', satoshis: 307627737, @@ -1186,18 +1200,21 @@ describe('Address Service', function() { should.not.exist(err); outputs.length.should.equal(3); outputs[0].address.should.equal(address); + outputs[0].hashType.should.equal('pubkeyhash'); outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); outputs[0].outputIndex.should.equal(1); outputs[0].satoshis.should.equal(4527773864); outputs[0].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); outputs[0].height.should.equal(345000); outputs[1].address.should.equal(address); + outputs[1].hashType.should.equal('pubkeyhash'); outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7'); outputs[1].outputIndex.should.equal(2); outputs[1].satoshis.should.equal(10000); outputs[1].script.should.equal('76a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac'); outputs[1].height.should.equal(345004); outputs[2].address.should.equal(address); + outputs[2].hashType.should.equal('pubkeyhash'); outputs[2].txid.should.equal('aa2db23f670596e96ed94c405fd11848c8f236d266ee96da37ecd919e53b4371'); outputs[2].script.should.equal('76a914f6db95c81dea3d10f0ff8d890927751bf7b203c188ac'); outputs[2].height.should.equal(-1); @@ -1206,12 +1223,12 @@ describe('Address Service', function() { }); var data1 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), + key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; var data2 = { - key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'), + key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b6870100000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'), value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex') }; @@ -1237,12 +1254,98 @@ describe('Address Service', function() { readStream2.emit('close'); }); }); + + it('should print outputs for a p2sh address', function(done) { + // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, + // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. + // See https://github.com/bitpay/bitcore-node/issues/377 + var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; + var hashBuffer = bitcore.Address(address).hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.REDEEMSCRIPT; + var testStream = new EventEmitter(); + var args = { + start: 15, + end: 12, + queryMempool: true + }; + var createReadStreamCallCount = 0; + am.node.services.db.store = { + createReadStream: function(ops) { + var gte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); + ops.gte.toString('hex').should.equal(gte.toString('hex')); + var lte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); + ops.lte.toString('hex').should.equal(lte.toString('hex')); + createReadStreamCallCount++; + return testStream; + } + }; + am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); + am.getOutputs(address, args, function(err, outputs) { + should.not.exist(err); + outputs.length.should.equal(1); + outputs[0].address.should.equal(address); + outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87'); + outputs[0].hashType.should.equal('scripthash'); + outputs[0].hashType.should.equal(AddressService.HASH_TYPES_READABLE[hashTypeBuffer.toString('hex')]); + outputs[0].outputIndex.should.equal(1); + outputs[0].satoshis.should.equal(4527773864); + outputs[0].script.should.equal('a914038a213afdfc551fc658e9a2a58a86e98d69b68787'); + outputs[0].height.should.equal(15); + done(); + }); + createReadStreamCallCount.should.equal(1); + var data = { + // note '68702', '02' meaning p2sh redeemScript, not p2pkh + // value is also the p2sh script, not p2pkh + key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68702000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'), + value: new Buffer('41f0de058a800000a914038a213afdfc551fc658e9a2a58a86e98d69b68787', 'hex') + }; + testStream.emit('data', data); + testStream.emit('close'); + }); + + it('should not print outputs for a p2pkh address, if the output was sent to a p2sh redeemScript', function(done) { + // This address has the redeemScript 0x038a213afdfc551fc658e9a2a58a86e98d69b687, + // which is the same as the pkhash for the address 1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W. + // See https://github.com/bitpay/bitcore-node/issues/377 + var address = '321jRYeWBrLBWr2j1KYnAFGico3GUdd5q7'; + var hashBuffer = bitcore.Address(address).hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.REDEEMSCRIPT; + var testStream = new EventEmitter(); + var args = { + start: 15, + end: 12, + queryMempool: true + }; + var createReadStreamCallCount = 0; + + // Verifying that the db query is looking for a redeemScript, *not* a p2pkh + am.node.services.db.store = { + createReadStream: function(ops) { + var gte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('000000000c', 'hex')]); + ops.gte.toString('hex').should.equal(gte.toString('hex')); + var lte = Buffer.concat([AddressService.PREFIXES.OUTPUTS, hashBuffer, hashTypeBuffer, new Buffer('0000000010', 'hex')]); + ops.lte.toString('hex').should.equal(lte.toString('hex')); + createReadStreamCallCount++; + return testStream; + } + }; + am._getOutputsMempool = sinon.stub().callsArgWith(3, null, []); + am.getOutputs(address, args, function(err, outputs) { + should.not.exist(err); + outputs.length.should.equal(0); + done(); + }); + createReadStreamCallCount.should.equal(1); + testStream.emit('close'); + }); }); describe('#_getOutputsMempool', function() { var am; var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W'; var hashBuffer = bitcore.Address(address).hashBuffer; + var hashTypeBuffer = AddressService.HASH_TYPES.PUBKEY; var db = { tip: { __height: 1 @@ -1268,7 +1371,7 @@ describe('Address Service', function() { var testStream = new EventEmitter(); am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getOutputsMempool(address, hashBuffer, function(err, outputs) { + am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { should.exist(err); err.message.should.equal('readstreamerror'); done(); @@ -1283,12 +1386,13 @@ describe('Address Service', function() { am.mempoolIndex = {}; am.mempoolIndex.createReadStream = sinon.stub().returns(testStream); - am._getOutputsMempool(address, hashBuffer, function(err, outputs) { + am._getOutputsMempool(address, hashBuffer, hashTypeBuffer, function(err, outputs) { if (err) { throw err; } outputs.length.should.equal(1); outputs[0].address.should.equal(address); + outputs[0].hashType.should.equal('pubkeyhash'); outputs[0].txid.should.equal(txid); outputs[0].outputIndex.should.equal(outputIndex); outputs[0].height.should.equal(-1); @@ -1304,8 +1408,9 @@ describe('Address Service', function() { var outputIndexBuffer = new Buffer(4); outputIndexBuffer.writeUInt32BE(outputIndex); var keyData = Buffer.concat([ - new Buffer('01', 'hex'), + AddressService.MEMPREFIXES.OUTPUTS, hashBuffer, + hashTypeBuffer, txidBuffer, outputIndexBuffer ]);