diff --git a/integration/bitcoind.js b/integration/bitcoind.js index 843111a..16cad9b 100644 --- a/integration/bitcoind.js +++ b/integration/bitcoind.js @@ -27,8 +27,8 @@ var blockHash = { 'testnet': '0000000058cc069d964711cd25083c0a709f4df2b34c8ff9302ce71fe5b45786' }; var stopBlock = { - 'livenet': '000000000000000006181d9d183e2191a5e704d6ed3513f29b0970198fb34d2e', - 'testnet': '000000003d594c41db49d5a8b850344943438620acf79ce8aa88177f5b35e337' + 'livenet': '00000000000000000b539ef570128acb953af3dbcfc19dd8e6066949672311a1', + 'testnet': '00000000d0bc4271bcefaa7eb25000e345910ba16b91eb375cd944b68624de9f' }; var txHash = { 'livenet': '22231e8219a0617a0ded618b5dc713fdf9b0db8ebd5bb3322d3011a703119d3b', @@ -87,7 +87,6 @@ describe('Integration with ' + network.name + ' bitcoind', function() { connect(function(peer) { peer.once('addr', function(message) { message.addresses.forEach(function(address) { - // console.log(address.ip.v4 + ':' + address.port); (address.time instanceof Date).should.equal(true); should.exist(address.ip); (address.services instanceof BN).should.equal(true); @@ -98,15 +97,13 @@ describe('Integration with ' + network.name + ' bitcoind', function() { peer.sendMessage(message); }); }); - it('can request inv detailed info', function(cb) { + it('requests inv detailed info', function(cb) { connect(function(peer) { peer.once('block', function(message) { - //console.log(message.block.toJSON()); should.exist(message.block); cb(); }); peer.once('tx', function(message) { - //console.log(message.transaction.toJSON()); should.exist(message.transaction); cb(); }); @@ -116,7 +113,7 @@ describe('Integration with ' + network.name + ' bitcoind', function() { }); }); }); - it('can send tx inv and receive getdata for that tx', function(cb) { + it('sends tx inv and receives getdata for that tx', function(cb) { connect(function(peer) { var type = Messages.Inventory.TYPE.TX; var inv = [{ @@ -133,7 +130,7 @@ describe('Integration with ' + network.name + ' bitcoind', function() { peer.sendMessage(message); }); }); - it('can request block data', function(cb) { + it('requests block data', function(cb) { connect(function(peer) { peer.once('block', function(message) { (message.block instanceof Block).should.equal(true); @@ -143,39 +140,37 @@ describe('Integration with ' + network.name + ' bitcoind', function() { peer.sendMessage(message); }); }); - it('can handle request tx data not found', function(cb) { + var fakeHash = 'e2dfb8afe1575bfacae1a0b4afc49af7ddda69285857267bae0e22be15f74a3a'; + it('handles request tx data not found', function(cb) { connect(function(peer) { - var hash = 'e2dfb8afe1575bfacae1a0b4afc49af7ddda69285857267bae0e22be15f74a3a'; - var expected = Messages.NotFound.forTransaction(hash); + var expected = Messages.NotFound.forTransaction(fakeHash); peer.once('notfound', function(message) { (message instanceof Messages.NotFound).should.equal(true); message.should.deep.equal(expected); cb(); }); - var message = Messages.GetData.forTransaction(hash); + var message = Messages.GetData.forTransaction(fakeHash); peer.sendMessage(message); }); }); var from = [blockHash[network.name]]; var stop = stopBlock[network.name]; - it('can get headers', function(cb) { + it('gets headers', function(cb) { connect(function(peer) { peer.once('headers', function(message) { (message instanceof Messages.Headers).should.equal(true); - message.headers.length.should.equal(2); + message.headers.length.should.equal(3); cb(); }); var message = new Messages.GetHeaders(from, stop); peer.sendMessage(message); }); }); - it.skip('can get blocks', function(cb) { + it('gets blocks', function(cb) { connect(function(peer) { - peer.on('inv', function(message) { + peer.once('inv', function(message) { (message instanceof Messages.Inventory).should.equal(true); - console.log('inv' + message.inventory.length); if (message.inventory.length === 2) { - console.log(message); message.inventory[0].type.should.equal(Messages.Inventory.TYPE.BLOCK); cb(); } @@ -184,4 +179,26 @@ describe('Integration with ' + network.name + ' bitcoind', function() { peer.sendMessage(message); }); }); + var testInvGetData = function(expected, message, cb) { + connect(function(peer) { + peer.once('getdata', function(message) { + (message instanceof Messages.GetData).should.equal(true); + message.should.deep.equal(expected); + cb(); + }); + peer.sendMessage(message); + }); + }; + it('sends block inv and receives getdata', function(cb) { + var randomHash = Random.getRandomBuffer(32); // needs to be random for repeatability + var expected = Messages.GetData.forBlock(randomHash); + var message = Messages.Inventory.forBlock(randomHash); + testInvGetData(expected, message, cb); + }); + it('sends tx inv and receives getdata', function(cb) { + var randomHash = Random.getRandomBuffer(32); // needs to be random for repeatability + var expected = Messages.GetData.forTransaction(randomHash); + var message = Messages.Inventory.forTransaction(randomHash); + testInvGetData(expected, message, cb); + }); }); diff --git a/lib/messages.js b/lib/messages.js index c7d795c..c621de5 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -72,6 +72,8 @@ var PAYLOAD_START = 16; * @returns{Message|undefined} A message or undefined if there is nothing to read. */ var parseMessage = function(network, dataBuffer) { + $.checkArgument(network); + $.checkArgument(dataBuffer); /* jshint maxstatements: 18 */ if (dataBuffer.length < 20) { return; @@ -112,13 +114,9 @@ module.exports.parseMessage = parseMessage; * @name P2P.Message#buildMessage */ Message.buildMessage = function(command, payload) { - try { - var CommandClass = Message.COMMANDS[command]; - return new CommandClass().fromBuffer(payload); - } catch (err) { - console.log('Error while parsing message', err); - throw err; - } + var CommandClass = Message.COMMANDS[command]; + $.checkState(CommandClass, 'Unsupported message command: ' + command); + return new CommandClass().fromBuffer(payload); }; /** @@ -169,6 +167,13 @@ Message.prototype.serialize = function(network) { return message.buffer(); }; +/** + * check if parser has no more extra data + */ +Message.prototype._checkFinished = function(parser) { + $.checkState(parser.finished(), 'data still available after parsing ' + this.constructor.name); +}; + module.exports.Message = Message; /** @@ -209,15 +214,29 @@ Version.prototype.fromBuffer = function(payload) { */ this.timestamp = new Date(parser.readUInt64LEBN().toNumber() * 1000); /** - * @type {Buffer} + * @type {object} * @desc IPv4/6 address of the interface used to connect to this peer */ - this.addr_me = parser.read(26); + var me_services = parser.readUInt64LEBN(); + var me_ip = Addresses.parseIP(parser); + var me_port = parser.readUInt16BE(); + this.addr_me = { + services: me_services, + ip: me_ip, + port: me_port + }; /** - * @type {Buffer} + * @type {object} * @desc IPv4/6 address of the peer */ - this.addr_you = parser.read(26); + var your_services = parser.readUInt64LEBN(); + var your_ip = Addresses.parseIP(parser); + var your_port = parser.readUInt16BE(); + this.addr_you = { + services: your_services, + ip: your_ip, + port: your_port + }; /** * @type {Buffer} * @desc A random number @@ -227,23 +246,30 @@ Version.prototype.fromBuffer = function(payload) { * @desc The node's user agent / subversion * @type {string} */ - this.subversion = parser.readVarintBuf().toString(); + this.subversion = parser.readVarLengthBuffer().toString(); /** * @desc The height of the last block accepted in the blockchain by this peer * @type {number} */ this.start_height = parser.readUInt32LE(); + /** + * @desc Whether the remote peer should announce relayed transactions or not, see BIP 0037 + * @type {boolean} + */ + this.relay = !!parser.readUInt8(); + + this._checkFinished(parser); return this; }; Version.prototype.getPayload = function() { var put = new Put(); - put.word32le(this.version); // version + put.word32le(this.version); put.word64le(1); // services put.word64le(Math.round(new Date().getTime() / 1000)); // timestamp - put.pad(26); // addr_me - put.pad(26); // addr_you + Addresses.writeAddr(this.addr_me, put); + Addresses.writeAddr(this.addr_you, put); put.put(this.nonce); put.varint(this.subversion.length); put.put(new Buffer(this.subversion, 'ascii')); @@ -315,6 +341,7 @@ Inventory.prototype.fromBuffer = function(payload) { this.inventory.push(Inventory.forItem(type, hash)); } + this._checkFinished(parser); return this; }; @@ -396,7 +423,10 @@ function Ping(nonce) { util.inherits(Ping, Message); Ping.prototype.fromBuffer = function(payload) { - this.nonce = new BufferReader(payload).read(8); + var parser = new BufferReader(payload); + this.nonce = parser.read(8); + + this._checkFinished(parser); return this; }; @@ -422,9 +452,6 @@ function Pong(nonce) { } util.inherits(Pong, Ping); -Pong.prototype.fromBuffer = function() { - return new Pong(); -}; module.exports.Pong = Message.COMMANDS.pong = Pong; /** @@ -443,26 +470,58 @@ function Addresses(addresses) { } util.inherits(Addresses, Message); +Addresses.writeAddr = function(addr, put) { + if (_.isUndefined(addr)) { + put.pad(26); + return; + } + put.word64le(addr.services); + Addresses.writeIP(addr.ip, put); + put.word16be(addr.port); +}; + +Addresses.writeIP = function(ip, put) { + $.checkArgument(ip.v6, 'Need ipv6 to write IP'); + var words = ip.v6.split(':').map(function(s) { + return new Buffer(s, 'hex'); + }); + for (var i = 0; i < words.length; i++) { + var word = words[i]; + put.put(word); + } +}; + +// http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses Addresses.parseIP = function(parser) { - // parse the ipv6 to a string var ipv6 = []; - for (var a = 0; a < 6; a++) { - ipv6.push(parser.read(2).toString('hex')); + var ipv4 = []; + for (var a = 0; a < 8; a++) { + var word = parser.read(2); + ipv6.push(word.toString('hex')); + if (a >= 6) { + ipv4.push(word[0]); + ipv4.push(word[1]); + } } ipv6 = ipv6.join(':'); - - // parse the ipv4 to a string - var ipv4 = []; - for (var b = 0; b < 4; b++) { - ipv4.push(parser.read(1)[0]); - } ipv4 = ipv4.join('.'); + return { v6: ipv6, v4: ipv4 }; }; +Addresses.parseAddr = function(parser) { + var services = parser.readUInt64LEBN(); + var ip = Addresses.parseIP(parser); + var port = parser.readUInt16BE(); + return { + services: services, + ip: ip, + port: port + }; +}; Addresses.prototype.fromBuffer = function(payload) { var parser = new BufferReader(payload); @@ -471,22 +530,15 @@ Addresses.prototype.fromBuffer = function(payload) { this.addresses = []; for (var i = 0; i < addrCount; i++) { // TODO: Time actually depends on the version of the other peer (>=31402) - var time = new Date(parser.readUInt32LE() * 1000); - var services = parser.readUInt64LEBN(); - var ip = Addresses.parseIP(parser); + var addr = Addresses.parseAddr(parser); + addr.time = time; - var port = parser.readUInt16BE(); - - this.addresses.push({ - time: time, - services: services, - ip: ip, - port: port - }); + this.addresses.push(addr); } + this._checkFinished(parser); return this; }; @@ -495,10 +547,10 @@ Addresses.prototype.getPayload = function() { put.varint(this.addresses.length); for (var i = 0; i < this.addresses.length; i++) { - put.word32le(this.addresses[i].time); - put.word64le(this.addresses[i].services); - put.put(this.addresses[i].ip); - put.word16be(this.addresses[i].port); + var addr = this.addresses[i]; + put.word32le(addr.time); + Addresses.writeAddr(addr, put); + break; } return put.buffer(); @@ -559,8 +611,9 @@ util.inherits(Alert, Message); Alert.prototype.fromBuffer = function(payload) { var parser = new BufferReader(payload); - this.payload = parser.readVarintBuf(); // TODO: Use current format - this.signature = parser.readVarintBuf(); + this.payload = parser.readVarLengthBuffer(); + this.signature = parser.readVarLengthBuffer(); + this._checkFinished(parser); return this; }; @@ -603,8 +656,11 @@ Headers.prototype.fromBuffer = function(payload) { for (var i = 0; i < count; i++) { var header = BlockHeaderModel.fromBufferReader(parser); this.headers.push(header); - } + var txn_count = parser.readUInt8(); + $.checkState(txn_count === 0, 'txn_count should always be 0'); + } + this._checkFinished(parser); return this; }; @@ -617,6 +673,7 @@ Headers.prototype.getPayload = function() { .headers[i] .toBuffer(); put.put(buffer); + put.varint(0); } return put.buffer(); @@ -730,6 +787,7 @@ GetBlocks.prototype.fromBuffer = function(payload) { this.starts.push(parser.read(32)); } this.stop = parser.read(32); + this._checkFinished(parser); return this; }; diff --git a/package.json b/package.json index 2f7a405..4277a7d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "url": "https://github.com/bitpay/bitcore-p2p.git" }, "dependencies": { - "bitcore": "^0.9.0", + "bitcore": "^0.9.5", "bufferput": "^0.1.2", "buffers": "^0.1.1", "socks5-client": "^0.3.6" diff --git a/test/data/messages.json b/test/data/messages.json index 0036144..21728f0 100644 --- a/test/data/messages.json +++ b/test/data/messages.json @@ -5,7 +5,7 @@ }, "ALERT": { "message": "", - "payload": "" + "payload": "73010000003766404f00000000b305434f00000000f2030000f1030000001027000048ee00000064000000004653656520626974636f696e2e6f72672f666562323020696620796f7520686176652074726f75626c6520636f6e6e656374696e67206166746572203230204665627275617279004730450221008389df45f0703f39ec8c1cc42c13810ffcae14995bb648340219e353b63b53eb022009ec65e1c1aaeec1fd334c6b684bde2b3f573060d5b70c3a46723326e4e8a4f1" }, "REJECT": { "message": "", @@ -20,8 +20,8 @@ "payload": "" }, "GETDATA": { - "message": "", - "payload": "" + "message": "f9beb4d967657464617461000000000025000000253fc50c01020000003c2a5e4dcb5a44daf5ebff14a89d26a99b8aa1c1d757694b03d7e6ca7eeda6ca", + "payload": "01020000003c2a5e4dcb5a44daf5ebff14a89d26a99b8aa1c1d757694b03d7e6ca7eeda6ca" }, "GETADDR": { "message": "", @@ -32,8 +32,8 @@ "payload": "" }, "HEADERS": { - "message": "f9beb4d9686561646572730000000000a1000000ffd6770b0202000000b91ddbbfc801b7fe6f470ce9528f98f01b496b53f23c411300000000000000004901c9d18d0a468b20cc62ddf75aee58cf410440ea390300bf7a5f6848be350508d4cb54c0a31a18b9f661ec0002000000a02d6472e3e6fc9a1cebeaad14a90208a715e2bd234ea00600000000000000006f596f650fbbd5478489c66d651c9e3ea56f394d1f1481f90975cf0c8dda45fd3ad4cb54c0a31a1872e262", - "payload": "0202000000b91ddbbfc801b7fe6f470ce9528f98f01b496b53f23c411300000000000000004901c9d18d0a468b20cc62ddf75aee58cf410440ea390300bf7a5f6848be350508d4cb54c0a31a18b9f661ec0002000000a02d6472e3e6fc9a1cebeaad14a90208a715e2bd234ea00600000000000000006f596f650fbbd5478489c66d651c9e3ea56f394d1f1481f90975cf0c8dda45fd3ad4cb54c0a31a1872e262" + "message": "f9beb4d9686561646572730000000000f400000043385d010302000000b91ddbbfc801b7fe6f470ce9528f98f01b496b53f23c411300000000000000004901c9d18d0a468b20cc62ddf75aee58cf410440ea390300bf7a5f6848be350508d4cb54c0a31a18b9f661ec0002000000a02d6472e3e6fc9a1cebeaad14a90208a715e2bd234ea00600000000000000006f596f650fbbd5478489c66d651c9e3ea56f394d1f1481f90975cf0c8dda45fd3ad4cb54c0a31a1872e262a200020000002e4db38f1970099bf21335edd604e7a591213e189d1d1806000000000000000031a30091f5bdbca8958d2c4ccc0bfa9df93e2a3ea4d00e03222a663179db90a756d6cb54c0a31a187947855000", + "payload": "0302000000b91ddbbfc801b7fe6f470ce9528f98f01b496b53f23c411300000000000000004901c9d18d0a468b20cc62ddf75aee58cf410440ea390300bf7a5f6848be350508d4cb54c0a31a18b9f661ec0002000000a02d6472e3e6fc9a1cebeaad14a90208a715e2bd234ea00600000000000000006f596f650fbbd5478489c66d651c9e3ea56f394d1f1481f90975cf0c8dda45fd3ad4cb54c0a31a1872e262a200020000002e4db38f1970099bf21335edd604e7a591213e189d1d1806000000000000000031a30091f5bdbca8958d2c4ccc0bfa9df93e2a3ea4d00e03222a663179db90a756d6cb54c0a31a187947855000" }, "TX": { "message": "", diff --git a/test/messages.js b/test/messages.js index 9222abe..204bfaf 100644 --- a/test/messages.js +++ b/test/messages.js @@ -4,11 +4,15 @@ var chai = require('chai'); var should = chai.should(); +var Buffers = require('buffers'); var bitcore = require('bitcore'); var Data = require('./data/messages'); var P2P = require('../'); var Messages = P2P.Messages; var Networks = bitcore.Networks; +var BufferUtils = bitcore.util.buffer; + +var network = Networks.livenet; describe('Messages', function() { @@ -31,7 +35,7 @@ describe('Messages', function() { NotFound: 'notfound' }; // TODO: add data for these - var noPayload = ['Alert', 'Reject', 'GetBlocks', 'GetHeaders', 'GetData']; + var noPayload = ['Reject', 'GetBlocks', 'GetHeaders']; var names = Object.keys(commands); describe('named', function() { names.forEach(function(name) { @@ -67,4 +71,49 @@ describe('Messages', function() { }); }); + var buildMessage = function(hex) { + var m = Buffers(); + m.push(new Buffer(hex, 'hex')); + return m; + }; + it('fails with invalid command', function() { + var invalidCommand = 'f9beb4d96d616c6963696f757300000025000000bd5e830c' + + '0102000000ec3995c1bf7269ff728818a65e53af00cbbee6b6eca8ac9ce7bc79d87' + + '7041ed8'; + var fails = function() { + Messages.parseMessage(network, buildMessage(invalidCommand)); + }; + fails.should.throw('Unsupported message command: malicious'); + }); + + it('ignores malformed messages', function() { + var malformed1 = 'd8c4c3d976657273696f6e000000000065000000fc970f1772110' + + '1000100000000000000ba6288540000000001000000000000000000000000000000' + + '0000ffffba8886dceab0010000000000000000000000000000000000ffff0509552' + + '2208de7e1c1ef80a1cea70f2f5361746f7368693a302e392e312fa317050001'; + var malformed2 = 'f9beb4d967657464617461000000000089000000d88134740102' + + '0000006308e4a380c949dbad182747b0f7b6a89e874328ca41f37287f74a81b8f84' + + '86d'; + var malformed3 = 'f9beb4d967657464617461000000000025000000616263640102' + + '00000069ebcbc34a4f9890da9aea0f773beba883a9afb1ab9ad7647dd4a1cd346c3' + + '728'; + [malformed1, malformed2, malformed3].forEach(function(malformed) { + var ret = Messages.parseMessage(network, buildMessage(malformed)); + should.not.exist(ret); + }); + }); + + it('Inventory#from family methods work', function() { + var hash = 'eb951630aba498b9a0d10f72b5ea9e39d5ff04b03dc2231e662f52057f948aa1'; + [Messages.Inventory, Messages.GetData, Messages.NotFound].forEach(function(clazz) { + var b = clazz.forBlock(hash); + (b instanceof clazz).should.equal(true); + var t = clazz.forTransaction(hash); + (t instanceof clazz).should.equal(true); + clazz.forBlock(BufferUtils.reverse(new Buffer(hash, 'hex'))).should.deep.equal(b); + clazz.forTransaction(BufferUtils.reverse(new Buffer(hash, 'hex'))).should.deep.equal(t); + }); + + }); + }); diff --git a/test/peer.js b/test/peer.js index bd11e14..fdc788a 100644 --- a/test/peer.js +++ b/test/peer.js @@ -12,15 +12,15 @@ var sinon = require('sinon'); var fs = require('fs'); var bitcore = require('bitcore'); -var P2P = require('../'); -var Peer = P2P.Peer; +var p2p = require('../'); +var Peer = p2p.Peer; var Networks = bitcore.Networks; describe('Peer', function() { describe('Integration test', function() { it('parses this stream of data from a connection', function(callback) { - var peer = new P2P.Peer(''); + var peer = new p2p.Peer(''); var stub = sinon.stub(); var dataCallback; var connectCallback;