From 40225584fd3bc6ba1ab3e472613c66da108952af Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 24 Dec 2014 13:30:55 -0300 Subject: [PATCH] Initial commit for bitcore-p2p --- .gitignore | 12 + .jsdoc.conf | 36 +++ .jshintrc | 44 +++ .travis.yml | 13 + CONTRIBUTING.md | 3 + LICENSE | 19 ++ README.md | 16 ++ gulpfile.js | 92 ++++++ index.js | 4 + karma.conf.js | 13 + lib/index.js | 8 + lib/messages.js | 602 ++++++++++++++++++++++++++++++++++++++++ lib/peer.js | 195 +++++++++++++ lib/pool.js | 277 ++++++++++++++++++ package.json | 54 ++++ test/data/messages.json | 22 ++ test/index.html | 18 ++ test/messages.js | 354 +++++++++++++++++++++++ test/mocha.opts | 1 + test/peer.js | 76 +++++ test/pool.js | 91 ++++++ 21 files changed, 1950 insertions(+) create mode 100644 .gitignore create mode 100644 .jsdoc.conf create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 gulpfile.js create mode 100644 index.js create mode 100644 karma.conf.js create mode 100644 lib/index.js create mode 100644 lib/messages.js create mode 100644 lib/peer.js create mode 100644 lib/pool.js create mode 100644 package.json create mode 100644 test/data/messages.json create mode 100644 test/index.html create mode 100644 test/messages.js create mode 100644 test/mocha.opts create mode 100644 test/peer.js create mode 100644 test/pool.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a5185b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.sw[a-z] +coverage +node_modules +bitcore-scaffolding.js +bitcore-scaffolding.min.js + +lib/errors/index.js +npm-debug.log + +bower_components +report +.DS_Store diff --git a/.jsdoc.conf b/.jsdoc.conf new file mode 100644 index 0000000..a6a59aa --- /dev/null +++ b/.jsdoc.conf @@ -0,0 +1,36 @@ +{ +"tags": { + "allowUnknownTags": true +}, +"source": { + "include": ["docs/README.md"], + "exclude": [], + "includePattern": "lib/.+\\.js(doc)?$", + "excludePattern": "(^|\\/|\\\\)_" +}, +"plugins": ["plugins/markdown"], +"templates": { + "cleverLinks": false, + "monospaceLinks": false +}, +"opts": { + "template": "node_modules/ink-docstrap/template", + "encoding": "utf8", + "destination": "./apiref/", + "recurse": true, + "query": "value", + "private": true, + "lenient": true +}, +"templates": { + "systemName": "bitcore", + "copyright": "© 2013-2015, BitPay Inc.", + "navType": "vertical", + "theme": "journal", + "linenums": true, + "collapseSymbols": false, + "inverseNav": false, + "outputSourceFiles": true +} + +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..82597a3 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,44 @@ +{ + "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). + "browser": true, // Standard browser globals e.g. `window`, `document`. + "camelcase": false, // Permit only camelcase for `var` and `object indexes`. + "curly": true, // Require {} for every new block or scope. + "devel": false, // Allow development statements e.g. `console.log();`. + "eqeqeq": true, // Require triple equals i.e. `===`. + "esnext": true, // Allow ES.next specific features such as `const` and `let`. + "freeze": true, // Forbid overwriting prototypes of native objects such as Array, Date and so on. + "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` + "indent": 2, // Specify indentation spacing + "latedef": true, // Prohibit variable use before definition. + "newcap": false, // Require capitalization of all constructor functions e.g. `new F()`. + "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. + "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. + "noempty": true, // Prohibit use of empty blocks. + "nonew": true, // Prohibits the use of constructor functions for side-effects + "quotmark": "single", // Define quotes to string values. + "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. + "smarttabs": false, // Supress warnings about mixed tabs and spaces + "strict": true, // Require `use strict` pragma in every file. + "trailing": true, // Prohibit trailing whitespaces. + "undef": true, // Require all non-global variables be declared before they are used. + "unused": true, // Warn unused variables. + + "maxparams": 4, // Maximum number of parameters for a function + "maxstatements": 15, // Maximum number of statements in a function + "maxcomplexity": 6, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) + "maxdepth": 4, // Maximum depth of nested control structures + "maxlen": 120, // Maximum number of cols in a line + + "predef": [ // Extra globals. + "after", + "afterEach", + "before", + "beforeEach", + "define", + "describe", + "exports", + "it", + "module", + "require" + ] +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..85c1de6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: +- '0.10' +before_install: + - npm install -g bower + - npm install -g grunt-cli + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +install: + - bower install + - npm install +after_script: + - gulp coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e5fcc1d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Please see [CONTRIBUTING.md](https://github.com/bitpay/bitcore/blob/master/CONTRIBUTING.md) on the main bitcore repo. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f258a81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 BitPay, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0019b96 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +bitcore-p2p +======= + +bitcore-p2p adds support for connecting to the bitcoin p2p network for node. + +See [the main bitcore repo](https://github.com/bitpay/bitcore) for more information. + +## Contributing + +See [CONTRIBUTING.md](https://github.com/bitpay/bitcore) on the main bitcore repo for information about how to contribute. + +## License + +Code released under [the MIT license](https://github.com/bitpay/bitcore/blob/master/LICENSE). + +Copyright 2013-2015 BitPay, Inc. Bitcore is a trademark maintained by BitPay, Inc. diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..95b91ae --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,92 @@ +/** + * @file gulpfile.js + * + * Defines tasks that can be run on gulp. + * + * Summary: + */ +'use strict'; + +var gulp = require('gulp'); +var coveralls = require('gulp-coveralls'); +var jshint = require('gulp-jshint'); +var mocha = require('gulp-mocha'); +var shell = require('gulp-shell'); + +var files = ['lib/**/*.js']; +var tests = ['test/**/*.js']; +var alljs = files.concat(tests); + + +function ignoreError() { + /* jshint ignore:start */ // using `this` in this context is weird + this.emit('end'); + /* jshint ignore:end */ +} + +var testMocha = function() { + return gulp.src(tests).pipe(new mocha({ + reporter: 'spec' + })); +}; + +/** + * Testing + */ + +gulp.task('test', testMocha); + +gulp.task('test:nofail', function() { + return testMocha().on('error', ignoreError); +}); + +/** + * Code quality and documentation + */ + +gulp.task('lint', function() { + return gulp.src(alljs) + .pipe(jshint()) + .pipe(jshint.reporter('default')); +}); + +gulp.task('plato', shell.task(['plato -d report -r -l .jshintrc -t bitcore lib'])); + +gulp.task('coverage', shell.task(['node_modules/.bin/./istanbul cover node_modules/.bin/_mocha -- --recursive'])); + +gulp.task('coveralls', ['coverage'], function() { + gulp.src('coverage/lcov.info').pipe(coveralls()); +}); + +/** + * Watch tasks + */ + +gulp.task('watch:test', function() { + // TODO: Only run tests that are linked to file changes by doing + // something smart like reading through the require statements + return gulp.watch(alljs, ['test']); +}); + +gulp.task('watch:coverage', function() { + // TODO: Only run tests that are linked to file changes by doing + // something smart like reading through the require statements + return gulp.watch(alljs, ['coverage']); +}); + +gulp.task('watch:lint', function() { + // TODO: Only lint files that are linked to file changes by doing + // something smart like reading through the require statements + return gulp.watch(alljs, ['lint']); +}); + +/* Default task */ +gulp.task('default', ['lint', 'coverage'], function() { }); diff --git a/index.js b/index.js new file mode 100644 index 0000000..3cfe346 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +var bitcore = require('bitcore-base'); +bitcore.P2P = require('./lib'); + +module.exports = bitcore.P2P; diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..cc5620a --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,13 @@ +'use strict'; + +// karma.conf.js +module.exports = function(config) { + config.set({ + frameworks: ['mocha'], + browsers: ['Chrome', 'Firefox'], + singleRun: true, + files: [ + 'browser/tests.js' + ] + }); +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..37dd492 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,8 @@ +/** + * @namespace P2P + */ +module.exports = { + Messages: require('./messages'), + Peer: require('./peer'), + Pool: require('./pool') +}; diff --git a/lib/messages.js b/lib/messages.js new file mode 100644 index 0000000..3e063ce --- /dev/null +++ b/lib/messages.js @@ -0,0 +1,602 @@ +'use strict'; +/** + * @namespace P2P.Message + */ +/* jshint curly: false */ + +var Buffers = require('buffers'); +var Put = require('bufferput'); +var util = require('util'); + +var bitcore = require('bitcore-base'); + +var BlockHeaderModel = bitcore.BlockHeader; +var BlockModel = bitcore.Block; +var BufferReader = bitcore.encoding.BufferReader; +var BufferUtil = bitcore.util.buffer; +var Hash = bitcore.crypto.Hash; +var Random = bitcore.crypto.Random; +var TransactionModel = bitcore.Transaction; + +var CONNECTION_NONCE = Random.getPseudoRandomBuffer(8); +var PROTOCOL_VERSION = 70000; + +/** + * Static helper for consuming a data buffer until the next message. + * + * @name P2P.Message#parseMessage + * @param{Network} network - the network object + * @param{Buffer} dataBuffer - the buffer to read from + * @returns{Message|undefined} A message or undefined if there is nothing to read. + */ +var parseMessage = function(network, dataBuffer) { + if (dataBuffer.length < 20) return; + + // Search the next magic number + if (!discardUntilNextMessage(network, dataBuffer)) return; + + var PAYLOAD_START = 16; + var payloadLen = (dataBuffer.get(PAYLOAD_START)) + + (dataBuffer.get(PAYLOAD_START + 1) << 8) + + (dataBuffer.get(PAYLOAD_START + 2) << 16) + + (dataBuffer.get(PAYLOAD_START + 3) << 24); + + var messageLength = 24 + payloadLen; + if (dataBuffer.length < messageLength) return; + + var command = dataBuffer.slice(4, 16).toString('ascii').replace(/\0+$/, ''); + var payload = dataBuffer.slice(24, messageLength); + var checksum = dataBuffer.slice(20, 24); + + var checksumConfirm = Hash.sha256sha256(payload).slice(0, 4); + if (!BufferUtil.equals(checksumConfirm, checksum)) { + dataBuffer.skip(messageLength); + return; + } + + dataBuffer.skip(messageLength); + return Message.buildMessage(command, payload); +}; + +module.exports.parseMessage = parseMessage; + +/** + * @desc Internal function that discards data until founds the next message. + * @name P2P.Message#discardUntilNextMessage + */ +function discardUntilNextMessage(network, dataBuffer) { + var magicNumber = network.networkMagic; + + var i = 0; + for (;;) { + // check if it's the beginning of a new message + var packageNumber = dataBuffer.slice(0, 4); + if (BufferUtil.equals(packageNumber, magicNumber)) { + dataBuffer.skip(i); + return true; + } + + // did we reach the end of the buffer? + if (i > (dataBuffer.length - 4)) { + dataBuffer.skip(i); + return false; + } + + i++; // continue scanning + } +} + +/** + * Abstract Message that knows how to parse and serialize itself. + * Concret subclases should implement {fromBuffer} and {getPayload} methods. + * @name P2P.Message + */ +function Message() {} + +/** + * @value + * @name P2P.Message.COMMANDS + */ +Message.COMMANDS = {}; + +/** + * Look up a message type by command name and instantiate the correct Message + * @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); + } +}; + +/** + * Parse instance state from buffer. + * + * @param{Buffer} payload - the buffer to read from + * @returns{Message} The same message instance + */ +Message.prototype.fromBuffer = function(payload) { + /* jshint unused: false */ + return this; +}; + +/** + * Serialize the payload into a buffer. + * + * @returns{Buffer} the serialized payload + */ +Message.prototype.getPayload = function() { + return BufferUtil.EMPTY_BUFFER; +}; + +/** + * Serialize the message into a buffer. + * + * @returns{Buffer} the serialized message + */ +Message.prototype.serialize = function(network) { + var magic = network.networkMagic; + var commandBuf = new Buffer(this.command, 'ascii'); + if (commandBuf.length > 12) throw 'Command name too long'; + + var payload = this.getPayload(); + var checksum = Hash.sha256sha256(payload).slice(0, 4); + + // -- HEADER -- + var message = new Put(); + message.put(magic); + message.put(commandBuf); + message.pad(12 - commandBuf.length); // zero-padded + message.word32le(payload.length); + message.put(checksum); + + // -- BODY -- + message.put(payload); + + return message.buffer(); +} + +/** + * Version Message + * + * @name P2P.Message.Version + * @param{string} subversion - version of the client + * @param{Buffer} nonce - a random 8 bytes buffer + */ +function Version(subversion, nonce) { + this.command = 'version'; + this.version = PROTOCOL_VERSION; + this.subversion = subversion || '/BitcoinX:0.1/'; + this.nonce = nonce || CONNECTION_NONCE; +} +util.inherits(Version, Message); + +Version.prototype.fromBuffer = function(payload) { + var parser = new BufferReader(payload); + this.version = parser.readUInt32LE(); + this.services = parser.readUInt64LEBN(); + this.timestamp = parser.readUInt64LEBN(); + this.addr_me = parser.read(26); + this.addr_you = parser.read(26); + this.nonce = parser.read(8); + this.subversion = parser.readVarintBuf().toString(); + this.start_height = parser.readUInt32LE(); + + return this; +}; + +Version.prototype.getPayload = function() { + var put = new Put(); + put.word32le(this.version); // version + put.word64le(1); // services + put.word64le(Math.round(new Date().getTime() / 1000)); // timestamp + put.pad(26); // addr_me + put.pad(26); // addr_you + put.put(this.nonce); + put.varint(this.subversion.length); + put.put(new Buffer(this.subversion, 'ascii')); + put.word32le(0); + + return put.buffer(); +}; + +module.exports.Version = Message.COMMANDS.version = Version; + +/** + * Inv Message + * + * @name P2P.Message.Inventory + * @param{Array} inventory - reported elements + */ +function Inventory(inventory) { + this.command = 'inv'; + this.inventory = inventory || []; +} +util.inherits(Inventory, Message); + +Inventory.prototype.fromBuffer = function(payload) { + var parser = new BufferReader(payload); + var count = parser.readVarintNum(); + for (var i = 0; i < count; i++) { + this.inventory.push({ + type: parser.readUInt32LE(), + hash: parser.read(32) + }); + } + + return this; +}; + +Inventory.prototype.getPayload = function() { + var put = new Put(); + + put.varint(this.inventory.length); + this.inventory.forEach(function(value) { + put.word32le(value.type); + put.put(value.hash); + }); + + return put.buffer(); +}; + +module.exports.Inventory = Message.COMMANDS.inv = Inventory; + +/** + * Getdata Message + * + * @name P2P.Message.GetData + * @param{Array} inventory - requested elements + */ +function GetData(inventory) { + this.command = 'getdata'; + this.inventory = inventory || []; +} + +util.inherits(GetData, Inventory); +module.exports.GetData = GetData; + +/** + * Ping Message + * + * @name P2P.Message.Ping + * @param{Buffer} nonce - a random 8 bytes buffer + */ +function Ping(nonce) { + this.command = 'ping'; + this.nonce = nonce || CONNECTION_NONCE; +} +util.inherits(Ping, Message); + +Ping.prototype.fromBuffer = function(payload) { + this.nonce = new BufferReader(payload).read(8); + return this; +}; + +Ping.prototype.getPayload = function() { + return this.nonce; +}; + +module.exports.Ping = Message.COMMANDS.ping = Ping; + +/** + * Pong Message + * + * @name P2P.Message.Pong + * @param{Buffer} nonce - a random 8 bytes buffer + */ +function Pong(nonce) { + this.command = 'pong'; + this.nonce = nonce || CONNECTION_NONCE; +} + +util.inherits(Pong, Ping); +module.exports.Pong = Message.COMMANDS.pong = Pong; + +/** + * Addr Message + * + * @name P2P.Message.Addressess + * @param{Array} addresses - array of know addresses + */ +function Addresses(addresses) { + this.command = 'addr'; + this.addresses = addresses || []; +} +util.inherits(Addresses, Message); + +Addresses.prototype.fromBuffer = function(payload) { + var parser = new BufferReader(payload); + var addrCount = Math.min(parser.readVarintNum(), 1000); + + this.addresses = []; + for (var i = 0; i < addrCount; i++) { + // TODO: Time actually depends on the version of the other peer (>=31402) + + var time = parser.readUInt32LE(); + var services = parser.readUInt64LEBN(); + + // parse the ipv6 to a string + var ipv6 = []; + for (var a = 0; a < 6; a++) { + ipv6.push(parser.read(2).toString('hex')); + } + 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('.'); + + var port = parser.readUInt16BE(); + + this.addresses.push({ + time: time, + services: services, + ip: { v6: ipv6, v4: ipv4 }, + port: port + }); + } + + return this; +}; + +Addresses.prototype.getPayload = function() { + var put = new Put(); + 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); + } + + return put.buffer(); +}; + +module.exports.Addresses = Message.COMMANDS.addr = Addresses; + +/** + * GetAddr Message + * + * @name P2P.Message.GetAddresses + */ +function GetAddresses() { + this.command = 'getaddr'; +} + +util.inherits(GetAddresses, Message); +module.exports.GetAddresses = Message.COMMANDS.getaddr = GetAddresses; + +/** + * Verack Message + * + * @name P2P.Message.VerAck + */ +function VerAck() { + this.command = 'verack'; +} + +util.inherits(VerAck, Message); +module.exports.VerAck = Message.COMMANDS.verack = VerAck; + +/** + * Reject Message + * + * @name P2P.Message.Reject + */ +function Reject() { + this.command = 'reject'; +} +util.inherits(Reject, Message); + +// TODO: Parse REJECT message + +module.exports.Reject = Message.COMMANDS.reject = Reject; + +/** + * Alert Message + * + * @name P2P.Message.Alert + */ +function Alert(payload, signature) { + this.command = 'alert'; + this.payload = payload || new Buffer(32); + this.signature = signature || new Buffer(32); +} +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(); + return this; +}; + +Alert.prototype.getPayload = function() { + var put = new Put(); + put.varint(this.payload.length); + put.put(this.payload); + + put.varint(this.signature.length); + put.put(this.signature); + + return put.buffer(); +}; + +module.exports.Alert = Message.COMMANDS.alert = Alert; + +/** + * Headers Message + * + * @name P2P.Message.Headers + * @param{Array} blockheaders - array of block headers + */ +function Headers(blockheaders) { + this.command = 'headers'; + this.headers = blockheaders || []; +} +util.inherits(Headers, Message); + +Headers.prototype.fromBuffer = function(payload) { + var parser = new BufferReader(payload); + var count = parser.readVarintNum(); + + this.headers = []; + for (var i = 0; i < count; i++) { + var header = BlockHeaderModel._fromBufferReader(parser); + this.headers.push(header); + } + + return this; +}; + +Headers.prototype.getPayload = function() { + var put = new Put(); + put.varint(this.headers.length); + + for (var i = 0; i < this.headers.length; i++) { + var buffer = this.headers[i].toBuffer(); + put.put(buffer); + } + + return put.buffer(); +}; + +module.exports.Headers = Message.COMMANDS.headers = Headers; + +/** + * Block Message + * + * @name P2P.Message.Block + * @param{Block} block + */ +function Block(block) { + this.command = 'block'; + this.block = block; +} +util.inherits(Block, Message); + +Block.prototype.fromBuffer = function(payload) { + this.block = BlockModel(payload); + return this; +}; + +Block.prototype.getPayload = function() { + return this.block.toBuffer(); +}; + +module.exports.Block = Message.COMMANDS.block = Block; + +/** + * Tx Message + * + * @name P2P.Message.Transaction + * @param{Transaction} transaction + */ +function Transaction(transaction) { + this.command = 'tx'; + this.transaction = transaction; +} +util.inherits(Transaction, Message); + +Transaction.prototype.fromBuffer = function(payload) { + this.transaction = TransactionModel(payload); + return this; +}; + +Transaction.prototype.getPayload = function() { + return this.transaction.toBuffer(); +}; + +module.exports.Transaction = Message.COMMANDS.tx = Transaction; + +/** + * Getblocks Message + * + * @name P2P.Message.GetBlocks + * @param{Array} starts - array of buffers with the starting block hashes + * @param{Buffer} [stop] - hash of the last block + */ +function GetBlocks(starts, stop) { + this.command = 'getblocks'; + this.version = PROTOCOL_VERSION; + this.starts = starts || []; + this.stop = stop || BufferUtil.NULL_HASH; +} +util.inherits(GetBlocks, Message); + +GetBlocks.prototype.fromBuffer = function(payload) { + var parser = new BufferReader(payload); + this.version = parser.readUInt32LE(); + + var startCount = Math.min(parser.readVarintNum(), 500); + this.starts = []; + for (var i = 0; i < startCount; i++) { + this.starts.push(parser.read(32)); + } + this.stop = parser.read(32); + + return this; +}; + +GetBlocks.prototype.getPayload = function() { + var put = new Put(); + put.word32le(this.version); + put.varint(this.starts.length); + + for (var i = 0; i < this.starts.length; i++) { + if (this.starts[i].length !== 32) { + throw new Error('Invalid hash length'); + } + put.put(this.starts[i]); + } + + if (this.stop.length !== 32) { + throw new Error('Invalid hash length'); + } + put.put(this.stop); + + return put.buffer(); +}; + +module.exports.GetBlocks = Message.COMMANDS.getblocks = GetBlocks; + +/** + * Getheaders Message + * + * @name P2P.Message.GetHeaders + * @param{Array} starts - array of buffers with the starting block hashes + * @param{Buffer} [stop] - hash of the last block + */ +function GetHeaders(starts, stop) { + this.command = 'getheaders'; + this.version = PROTOCOL_VERSION; + this.starts = starts || []; + this.stop = stop || BufferUtil.NULL_HASH; +} + +util.inherits(GetHeaders, GetBlocks); +module.exports.GetHeaders = Message.COMMANDS.getheaders = GetHeaders; + + +// TODO: Remove this PATCH (yemel) +Buffers.prototype.skip = function (i) { + if (i === 0) return; + + if (i === this.length) { + this.buffers = []; + this.length = 0; + return; + } + + var pos = this.pos(i); + this.buffers = this.buffers.slice(pos.buf); + this.buffers[0] = new Buffer(this.buffers[0].slice(pos.offset)); + this.length -= i; +}; diff --git a/lib/peer.js b/lib/peer.js new file mode 100644 index 0000000..678919e --- /dev/null +++ b/lib/peer.js @@ -0,0 +1,195 @@ +'use strict'; + +var Buffers = require('buffers'); +var EventEmitter = require('events').EventEmitter; +var Net = require('net'); +var Socks5Client = require('socks5-client'); +var util = require('util'); + +var bitcore = require('bitcore-base'); +var Networks = bitcore.Networks; +var Messages = require('./messages'); + +var MAX_RECEIVE_BUFFER = 10000000; + +/** + * A Peer instance represents a remote bitcoin node and allows to communicate + * with it using the standar messages of the bitcoin p2p protocol. + * + * @example + * ```javascript + * + * var peer = new Peer('127.0.0.1').setProxy('127.0.0.1', 9050); + * peer.on('tx', function(tx) { + * console.log('New transaction: ', tx.id); + * }); + * peer.connect(); + * ``` + * + * @param {String} host - IP address of the remote host + * @param {Number} [port] - Port number of the remote host + * @param {Network} [network] - The context for this communication + * @returns {Peer} A new instance of Peer. + * @constructor + */ +function Peer(host, port, network) { + if (!(this instanceof Peer)) { + return new Peer(host, port, network); + } + + // overloading stuff + if (port instanceof Object && !network) { + network = port; + port = undefined; + } + + this.host = host; + this.status = Peer.STATUS.DISCONNECTED; + this.network = network || Networks.livenet; + this.port = port || this.network.port; + + this.dataBuffer = new Buffers(); + + this.version = 0; + this.bestHeight = 0; + this.subversion = null; + + // set message handlers + var self = this; + this.on('verack', function() { + self.status = Peer.STATUS.READY; + self.emit('ready'); + }); + + this.on('version', function(message) { + self.version = message.version; + self.subversion = message.subversion; + self.bestHeight = message.start_height + }); + + this.on('ping', function(message) { + self._sendPong(message.nonce); + }); + +} +util.inherits(Peer, EventEmitter); + +Peer.STATUS = { + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + CONNECTED: 'connected', + READY: 'ready' +}; + +/** + * Set a socks5 proxy for the connection. Enables the use of the TOR network. + * + * @param {String} host - IP address of the proxy + * @param {Number} port - Port number of the proxy + * @returns {Peer} The same Peer instance. + */ +Peer.prototype.setProxy = function(host, port) { + if (this.status != Peer.STATUS.DISCONNECTED) { + throw Error('Invalid State'); + } + + this.proxy = { + host: host, + port: port + }; + return this; +}; + +/** + * Init the connection with the remote peer. + * + * @returns {Socket} The same peer instance. + */ +Peer.prototype.connect = function() { + this.socket = this._getSocket(); + this.status = Peer.STATUS.CONNECTING; + + var self = this; + this.socket.on('connect', function(ev) { + self.status = Peer.STATUS.CONNECTED; + self.emit('connect'); + self._sendVersion(); + }); + + this.socket.on('error', self.disconnect.bind(this)); + this.socket.on('end', self.disconnect.bind(this)); + + this.socket.on('data', function(data) { + self.dataBuffer.push(data); + + if (self.dataBuffer.length > MAX_RECEIVE_BUFFER) return self.disconnect(); + self._readMessage(); + }); + + this.socket.connect(this.port, this.host); + return this; +}; + +/** + * Disconnects the remote connection. + * + * @returns {Socket} The same peer instance. + */ +Peer.prototype.disconnect = function() { + this.status = Peer.STATUS.DISCONNECTED; + this.socket.destroy(); + this.emit('disconnect'); + return this; +}; + +/** + * Send a Message to the remote peer. + * + * @param {Message} message - A message instance + */ +Peer.prototype.sendMessage = function(message) { + this.socket.write(message.serialize(this.network)); +}; + +/** + * Internal function that sends VERSION message to the remote peer. + */ +Peer.prototype._sendVersion = function() { + var message = new Messages.Version(); + this.sendMessage(message); +}; + +/** + * Send a PONG message to the remote peer. + */ +Peer.prototype._sendPong = function(nonce) { + var message = new Messages.Pong(nonce); + this.sendMessage(message); +}; + +/** + * Internal function that tries to read a message from the data buffer + */ +Peer.prototype._readMessage = function() { + var message = Messages.parseMessage(this.network, this.dataBuffer); + + if (message) { + this.emit(message.command, message); + this._readMessage(); + } +}; + +/** + * Internal function that creates a socket using a proxy if neccesary. + * + * @returns {Socket} A Socket instance not yet connected. + */ +Peer.prototype._getSocket = function() { + if (this.proxy) { + return new Socks5Client(this.proxy.host, this.proxy.port); + } + + return new Net.Socket(); +}; + +module.exports = Peer; diff --git a/lib/pool.js b/lib/pool.js new file mode 100644 index 0000000..0c8d073 --- /dev/null +++ b/lib/pool.js @@ -0,0 +1,277 @@ +'use strict'; + +var dns = require('dns'); +var EventEmitter = require('events').EventEmitter; + +var bitcore = require('bitcore-base'); +var Networks = bitcore.Networks; +var sha256 = bitcore.crypto.Hash.sha256; +var Peer = require('./peer'); +var util = require('util'); + +function now() { + return Math.floor(new Date().getTime() / 1000); +} + +/** + * A pool is a collection of Peers. A pool will discover peers from DNS seeds, and + * collect information about new peers in the network. When a peer disconnects the pool + * will connect to others that are available to maintain a max number of + * ongoing peer connections. Peer events are relayed to the pool. + * + * @example + * ```javascript + * + * var pool = new Pool(Networks.livenet); + * pool.on('peerinv', function(peer, message) { + * // do something with the inventory announcement + * }); + * pool.connect(); + * ``` + * + * @param {Network|String} network - The network to connect + * @returns {Pool} + * @constructor + */ +function Pool(network) { + + var self = this; + + this.network = Networks.get(network) || Networks.defaultNetwork; + this.keepalive = false; + this._connectedPeers = {}; + this._addrs = []; + + this.on('peeraddr', function peerAddrEvent(peer, message) { + var addrs = message.addresses; + var length = addrs.length; + for (var i = 0; i < length; i++) { + var addr = addrs[i]; + // In case of an invalid time, assume "5 days ago" + if (addr.time <= 100000000 || addr.time > (now() + 10 * 60)) { + addr.time = now() - 5 * 24 * 60 * 60; + } + this._addAddr(addr); + } + }); + + this.on('seed', function seedEvent(ips) { + ips.forEach(function(ip) { + self._addAddr({ + ip: { + v4: ip + } + }); + }); + if (self.keepalive) { + self._fillConnections(); + } + }); + + this.on('peerdisconnect', function peerDisconnectEvent(peer, addr) { + self._deprioritizeAddr(addr); + self._removeConnectedPeer(addr); + if (self.keepalive) { + self._fillConnections(); + } + }); + + return this; + +} + +util.inherits(Pool, EventEmitter); + +Pool.MaxConnectedPeers = 8; +Pool.RetrySeconds = 30; +Pool.PeerEvents = ['version', 'inv', 'getdata', 'ping', 'ping', 'addr', + 'getaddr', 'verack', 'reject', 'alert', 'headers', 'block', + 'tx', 'getblocks', 'getheaders' +]; + + +/** + * Will initiatiate connection to peers, if available peers have been added to + * the pool, it will connect to those, otherwise will use DNS seeds to find + * peers to connect. When a peer disconnects it will add another. + */ +Pool.prototype.connect = function connect() { + this.keepalive = true; + var self = this; + if (self._addrs.length === 0) { + self._addAddrsFromSeeds(); + } else { + self._fillConnections(); + } + return this; +}; + + +/** + * Will disconnect all peers that are connected. + */ +Pool.prototype.disconnect = function disconnect() { + this.keepalive = false; + for (var i in this._connectedPeers) { + this._connectedPeers[i].disconnect(); + } + return this; +}; + +/** + * @returns {Number} The number of peers currently connected. + */ +Pool.prototype.numberConnected = function numberConnected() { + return Object.keys(this._connectedPeers).length; +}; + +/** + * Will fill the conneted peers to the maximum amount. + */ +Pool.prototype._fillConnections = function _fillConnections() { + var length = this._addrs.length; + for (var i = 0; i < length; i++) { + if (this.numberConnected() >= Pool.MaxConnectedPeers) { + break; + } + var addr = this._addrs[i]; + if (!addr.retryTime || now() > addr.retryTime) { + this._connectPeer(addr); + } + } + return this; +}; + +/** + * Will remove a peer from the list of connected peers. + * @param {Object} addr - An addr from the list of addrs + */ +Pool.prototype._removeConnectedPeer = function _removeConnectedPeer(addr) { + if (this._connectedPeers[addr.hash].status !== Peer.STATUS.DISCONNECTED) { + this._connectedPeers[addr.hash].disconnect(); + } else { + delete this._connectedPeers[addr.hash]; + } + return this; +}; + +/** + * Will connect a peer and add to the list of connected peers. + * @param {Object} addr - An addr from the list of addrs + */ +Pool.prototype._connectPeer = function _connectPeer(addr) { + var self = this; + + function addConnectedPeer(addr) { + var port = addr.port || self.network.port; + var ip = addr.ip.v4 || addr.ip.v6; + var peer = new Peer(ip, port, self.network); + peer.on('disconnect', function peerDisconnect() { + self.emit('peerdisconnect', peer, addr); + }); + peer.on('ready', function peerReady() { + self.emit('peerready', peer, addr); + }); + Pool.PeerEvents.forEach(function addPeerEvents(event) { + peer.on(event, function peerEvent(message) { + self.emit('peer' + event, peer, message); + }); + }); + peer.connect(); + self._connectedPeers[addr.hash] = peer; + } + + if (!this._connectedPeers[addr.hash]) { + addConnectedPeer(addr); + } + + return this; +}; + +/** + * Will deprioritize an addr in the list of addrs by moving it to the end + * of the array, and setting a retryTime + * @param {Object} addr - An addr from the list of addrs + */ +Pool.prototype._deprioritizeAddr = function _deprioritizeAddr(addr) { + for (var i = 0; i < this._addrs.length; i++) { + if (this._addrs[i].hash === addr.hash) { + var middle = this._addrs[i]; + middle.retryTime = now() + Pool.RetrySeconds; + var beginning = this._addrs.splice(0, i); + var end = this._addrs.splice(i + 1, this._addrs.length); + var combined = beginning.concat(end); + this._addrs = combined.concat([middle]); + } + } + return this; +}; + +/** + * Will add an addr to the beginning of the addrs array + * @param {Object} + */ +Pool.prototype._addAddr = function _addAddr(addr) { + + // make a unique key + addr.hash = sha256(new Buffer(addr.ip.v6 + addr.ip.v4 + addr.port)).toString('hex'); + + var length = this._addrs.length; + var exists = false; + for (var i = 0; i < length; i++) { + if (this._addrs[i].hash === addr.hash) { + exists = true; + } + } + if (!exists) { + this._addrs.unshift(addr); + } + return this; +}; + +/** + * Will add addrs to the list of addrs from a DNS seed + * @param {String} seed - A domain name to resolve known peers + * @param {Function} done + */ +Pool.prototype._addAddrsFromSeed = function _addAddrsFromSeed(seed) { + var self = this; + dns.resolve(seed, function(err, ips) { + if (err) { + self.emit('seederror', err); + return; + } + if (!ips || !ips.length) { + self.emit('seederror', new Error('No IPs found from seed lookup.')); + return; + } + // announce to pool + self.emit('seed', ips); + }); + return this; +}; + +/** + * Will add addrs to the list of addrs from network DNS seeds + * @param {Function} done + */ +Pool.prototype._addAddrsFromSeeds = function _addAddrsFromSeeds() { + var self = this; + var seeds = this.network.dnsSeeds; + seeds.forEach(function(seed) { + self._addAddrsFromSeed(seed); + }); + return this; +}; + +/** + * @returns {String} A string formatted for the console + */ +Pool.prototype.inspect = function inspect() { + return ''; +}; + +module.exports = Pool; diff --git a/package.json b/package.json new file mode 100644 index 0000000..162873d --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "bitcore-p2p", + "version": "0.8.5", + "description": "Interface to the bitcoin P2P network for bitcore", + "author": "BitPay ", + "main": "index.js", + "scripts": { + "lint": "gulp lint", + "test": "gulp test", + "coverage": "gulp coverage", + "build": "gulp", + "postinstall": "node ./lib/errors/build.js" + }, + "contributors": [ + { + "name": "Esteban Ordano", + "email": "eordano@gmail.com" + } + ], + "keywords": [ + "bitcoin", + "bitcore" + ], + "repository": { + "type": "git", + "url": "https://github.com/bitpay/bitcore-p2p.git" + }, + "dependencies": { + "bitcore-base": "~0.8.5" + }, + "devDependencies": { + "browserify": "~6.3.3", + "chai": "~1.10.0", + "gulp": "^3.8.10", + "gulp-bump": "^0.1.11", + "gulp-coveralls": "^0.1.3", + "gulp-git": "^0.5.5", + "gulp-jshint": "^1.9.0", + "gulp-mocha": "^2.0.0", + "gulp-rename": "^1.2.0", + "gulp-shell": "^0.2.10", + "gulp-uglify": "^1.0.2", + "gulp-util": "=3.0.1", + "istanbul": "^0.3.5", + "karma": "^0.12.28", + "karma-firefox-launcher": "^0.1.3", + "karma-mocha": "^0.1.9", + "mocha": "~2.0.1", + "plato": "^1.3.0", + "run-sequence": "^1.0.2", + "sinon": "^1.12.2" + }, + "license": "MIT" +} diff --git a/test/data/messages.json b/test/data/messages.json new file mode 100644 index 0000000..cc1fa33 --- /dev/null +++ b/test/data/messages.json @@ -0,0 +1,22 @@ +{ + "VERSION": { + "message": "f9beb4d976657273696f6e000000000065000000fc970f17721101000100000000000000ba62885400000000010000000000000000000000000000000000ffffba8886dceab0010000000000000000000000000000000000ffff05095522208de7e1c1ef80a1cea70f2f5361746f7368693a302e392e312fa317050001", + "payload": "721101000100000000000000ba62885400000000010000000000000000000000000000000000ffffba8886dceab0010000000000000000000000000000000000ffff05095522208de7e1c1ef80a1cea70f2f5361746f7368693a302e392e312fa317050001" + }, + "VERACK": { + "message": "f9beb4d976657261636b000000000000000000005df6e0e2", + "payload": "" + }, + "INV": { + "message": "f9beb4d9696e76000000000000000000890200006e4f18431201000000f97347795bf7490ddeba98c129086f54e06633936a49a2a398defb49e5edbb00010000009cb17394db9280b2d5e7d9f01456358384e7453300d48138ca1590e8cd86632f010000000f810b6071b9808117c4b82619b4ae0a7994ed5a0f2c050e50fd5742c3b4e90901000000962798bce9cbfcee9a8c6b5df4cf8bed944eb31ec975e8c1be79b7ab010a1cb501000000baa905bcfc9d2641ecd759724f796a7614e91c7492507b418ba8e077a6e57054010000009a5ac6dae146392edf8b6e96c30951ed7752f9095fab61728207db0f135b18a30100000050e5f153e3f0d89ea432d9a26f088a89b3ca38dbf3f0247ddf15f80779915da801000000c255841510a284b88ba696ed9edfeb4cdb34966271c30bc3bce9fd881106850f0100000000e45af8455894f72cccc4053e1f5e6076a673982b1f4ca5b2abc7b6496823ab01000000c7e6dc049e5b0fdc0f8fac8934e84e9b2f0c3036c7c02638cc05ee2575d6be1701000000aef6435c03c99ee83702aaaf106dc853dee5bcb6025f3af67d1ec72202c437ad010000005369fc3dc81403fe52f766ef585245af908404be1b46e2ef67c93b748ef467e3010000002400f16a63c411ae5e336175afc26515e548c7267c0debdb6aa46830399ba35f01000000ceb4fa6a8ca2713baee2214422b73da47a20934c83d25017bd80053e65ef3091010000000d899968e591703ddd6fd6ce073837588209f8b3b245ecf1bf93e77b9806a6c501000000bddeb56581ad7882d8f2abc78e5a48b3de02d923cd1cfc7525d2dfe80470248a01000000f286188c0947b023f0ba5dd1ea596751d50f67fbe31e64ead39dd711d3585b5801000000e17ad100ebffd2d5a630d37fd2496b2f5ab6ae5c8812da3c642fb6b8dd37f5fd", + "payload": "1201000000f97347795bf7490ddeba98c129086f54e06633936a49a2a398defb49e5edbb00010000009cb17394db9280b2d5e7d9f01456358384e7453300d48138ca1590e8cd86632f010000000f810b6071b9808117c4b82619b4ae0a7994ed5a0f2c050e50fd5742c3b4e90901000000962798bce9cbfcee9a8c6b5df4cf8bed944eb31ec975e8c1be79b7ab010a1cb501000000baa905bcfc9d2641ecd759724f796a7614e91c7492507b418ba8e077a6e57054010000009a5ac6dae146392edf8b6e96c30951ed7752f9095fab61728207db0f135b18a30100000050e5f153e3f0d89ea432d9a26f088a89b3ca38dbf3f0247ddf15f80779915da801000000c255841510a284b88ba696ed9edfeb4cdb34966271c30bc3bce9fd881106850f0100000000e45af8455894f72cccc4053e1f5e6076a673982b1f4ca5b2abc7b6496823ab01000000c7e6dc049e5b0fdc0f8fac8934e84e9b2f0c3036c7c02638cc05ee2575d6be1701000000aef6435c03c99ee83702aaaf106dc853dee5bcb6025f3af67d1ec72202c437ad010000005369fc3dc81403fe52f766ef585245af908404be1b46e2ef67c93b748ef467e3010000002400f16a63c411ae5e336175afc26515e548c7267c0debdb6aa46830399ba35f01000000ceb4fa6a8ca2713baee2214422b73da47a20934c83d25017bd80053e65ef3091010000000d899968e591703ddd6fd6ce073837588209f8b3b245ecf1bf93e77b9806a6c501000000bddeb56581ad7882d8f2abc78e5a48b3de02d923cd1cfc7525d2dfe80470248a01000000f286188c0947b023f0ba5dd1ea596751d50f67fbe31e64ead39dd711d3585b5801000000e17ad100ebffd2d5a630d37fd2496b2f5ab6ae5c8812da3c642fb6b8dd37f5fd" + }, + "ADDR": { + "message": "f9beb4d9616464720000000000000000b93a0000480bab8afdf5016816fa53010000000000000000000000000000000000ffff51403eea208ddb2a8854010000000000000000000000000000000000ffff707c60d9208d31413a54010000000000000000000000000000000000ffff5ed5fd13208d182c8854010000000000000000000000000000000000ffff505f3f81208d65ecb853010000000000000000000000000000000000ffff54d72104208dcca94054010000000000000000000000000000000000ffff40bbe1f2208d73238854010000000000000000000000000000000000ffff55d683cd208d5d258854010000000000000000000000000000000000ffffd523a66d208dcbea87540100000000000000200100005ef579fd2c9d47be4b56d671208db3e58754010000000000000000000000000000000000ffffae373156208d11e58754810000000000000000000000000000000000ffff51071276208d38f48754010000000000000000000000000000000000ffff6d4372e0208d4f448854010000000000000000000000000000000000ffffc556cbea208deb208854010000000000000000000000000000000000ffff450eb059208d97336054010000000000000000000000000000000000ffff4d60444a208dad608754010000000000000000000000000000000000ffff01abd3c3208d2cbe8754010000000000000000000000000000000000ffff7963d8b8208d9dfa8754010000000000000000000000000000000000ffffcebe862c208da7f66554010000000000000000000000000000000000ffff62f2a82e208d430d8854010000000000000000000000000000000000ffff6deb6956208d08148854010000000000000000000000000000000000ffff4b9804ef208d950e8854010000000000000000000000000000000000ffff6b8dd2e1208d1cfc8754010000000000000000000000000000000000ffffb2fe1dab208de7458754010000000000000000000000000000000000ffff32cccb82208d12625654010000000000000000000000000000000000ffff55e6f35a208dd7908354010000000000000000000000000000000000ffff4c642e6f208d17398854010000000000000000000000000000000000ffff45ea14df208dc72d8854010000000000000000000000000000000000ffffdaa48b96208d57fb8654010000000000000000000000000000000000ffff5f0080a9208d4e9a6d54010000000000000000000000000000000000ffff6d94e741208d7a418854010000000000000000000000000000000000ffff6d43b0c1208d5c098854010000000000000000000000000000000000ffff9c11e7ea208d442d8854010000000000000000000000000000000000ffffd0424482208db142fb53010000000000000000000000000000000000ffff54344a4a208d70158854010000000000000000000000000000000000ffff538f8201208d39288854010000000000000000000000000000000000ffff57daab71208d262f8854010000000000000000000000000000000000ffff57e045b1208d91430654010000000000000000000000000000000000ffff4212def5208d7c468854010000000000000000000000000000000000ffff62a4f53b208d9c3e8854010000000000000000000000000000000000ffff5b736bcc208d4d074f54010000000000000000000000000000000000ffff43af9d9b208deaca8754010000000000000000000000000000000000ffff4a8084a0208d74f68754010000000000000000000000000000000000ffff46304541208d5d098854010000000000000000000000000000000000ffff6eabb626208d08228854010000000000000000000000000000000000ffff1f07b030208d959a8254010000000000000000000000000000000000ffff4fa996b3208d4bac8754010000000000000000000000000000000000ffff43a0605b208de4038854010000000000000000000000000000000000ffff4cb41a5b208ddc308854010000000000000000000000000000000000ffffb72e6072208deac86f54010000000000000000000000000000000000ffffd5b815e2208d1b118854010000000000000000000000000000000000ffff3ff8d0a4208db1258854010000000000000000000000000000000000ffff5edd3a6a208d20268754010000000000000000000000000000000000ffff59ed3cfc208d83ed8754010000000000000000000000000000000000ffff4460baea208d71278854010000000000000000000000000000000000ffff4583b547208da7968754010000000000000000000000000000000000ffffd1c34463208d9d428854010000000000000000000000000000000000ffff43c15bc5208d33448854010000000000000000000000000000000000ffff2e0d8a5c208dfa2e7154010000000000000000000000000000000000ffffceff2bca208d47288854010000000000000000000000000000000000ffff5b8d0190208d0e847954010000000000000000000000000000000000ffff5501b8c2208d552f8854010000000000000000000000000000000000ffff5cf3b604208d8c2e8854010000000000000000000000000000000000ffff58666b0d208dcbdd8754010000000000000000000000000000000000ffff59a85cf9208d04208854010000000000000000000000000000000000ffff5f60abe0208dba238854010000000000000000000000000000000000ffff2e77ce67208d62c68754010000000000000000000000000000000000ffffcbae5658208d41108854010000000000000000000000000000000000ffff49329ec8208ddf1d8854010000000000000000000000000000000000ffff603ada12208df90c8854010000000000000000000000000000000000ffff51e0c21c208d5dff8754010000000000000000000000000000000000ffff904ca539208de0118854010000000000000000000000000000000000ffff5c0cecf7208d1b2d88540100000000000000200100005ef579fb18f317b2aba4edfc208dfaa27f54010000000000000000000000000000000000ffff54c65a7c208dc2db8754010000000000000000000000000000000000ffffad0b7981208d88a58754010000000000000000000000000000000000ffff188a1995208de0d78754010000000000000000000000000000000000ffff62a7a6bd208d18f66b54010000000000000000000000000000000000ffffdb4fbe80208dfcf78754010000000000000000000000000000000000ffff67145102208d3d3a8854010000000000000000000000000000000000ffff64259bd6208dcaa98654010000000000000000000000000000000000ffff32c737e1208d3a6a5c54010000000000000000000000000000000000ffff6d793f75208d22d38754010000000000000000000000000000000000ffff0595cc0d208da9ea8754010000000000000000000000000000000000ffff62ca37d3208d440e8854010000000000000000000000000000000000ffff123e1c11208dab428854010000000000000000000000000000000000ffff461df51f208d0f0b8854010000000000000000000000000000000000ffff59d36752208d631f8854010000000000000000000000000000000000ffff5ef8c640208dbc1d8854010000000000000000000000000000000000ffff5f5dc582208d04bf8754010000000000000000000000000000000000ffff0e6797e3208db8028854010000000000000000000000000000000000ffff3cf0c4a1208d6a0c8854010000000000000000000000000000000000ffff58bb5c55208d4f438854010000000000000000000000000000000000ffff5d326005208df6068854010000000000000000000000000000000000ffff5d5a5850208d40eb8754010000000000000000000000000000000000ffff5751ed86208dbad18754010000000000000000000000000000000000ffff63e51608208d99108854010000000000000000000000000000000000ffff36e299cd208d9a1e88540100000000000000200100005ef579fd38a43785a5d441b7208d13258854010000000000000000000000000000000000ffffb90de279208db3778754010000000000000000000000000000000000ffff67fcc80c208d3d358854010000000000000000000000000000000000ffffd00c40fc208dfd0d8854010000000000000000000000000000000000ffff5e89c2f3208dd6428854010000000000000000000000000000000000ffff1863e14a208d89e78754010000000000000000000000000000000000ffff43a11d76208d89ce8754010000000000000000000000000000000000ffff3edb6281208d03298854010000000000000000000000000000000000ffff6d5b26dd208d16df6e54010000000000000000000000000000000000ffffd9c3d176208d7c028854010000000000000000000000000000000000ffffbcc2db1f208d67fc8754010000000000000000000000000000000000ffff59910667208d86b18754010000000000000000000000000000000000ffff6c1482b9208d790b8854010000000000000000000000000000000000ffff0263150d208d89c44554010000000000000000000000000000000000ffff72fcd6b8208d331c8854010000000000000000000000000000000000ffff4c7dcfa8208d9fe98754010000000000000000000000000000000000ffffad08fd05208d0ecd8754010000000000000000000000000000000000ffff7c0fec89208d4af98754010000000000000000000000000000000000ffff2ea6a167208d270b8854010000000000000000000000000000000000ffff59d40fca208d11cf4054010000000000000000000000000000000000ffffb52ac058208d63358254010000000000000000000000000000000000ffff6bd9a130208dbde48754010000000000000000000000000000000000ffff4f915dc7208dece78754010000000000000000000000000000000000ffff5f8423ef208dba308854010000000000000000000000000000000000ffff904cba8c208d953d8854010000000000000000000000000000000000ffff8d1421e5208d5f188854010000000000000000000000000000000000ffff4e1bbfb6208d36fc8754010000000000000000000000000000000000ffff3205cfb6208d5e458854010000000000000000000000000000000000ffff493092c2208dd0878754010000000000000000000000000000000000ffff97caacde208dd99d8754010000000000000000000000000000000000ffffc05f3c38208da7018854010000000000000000000000000000000000ffffdcf5f82d208df0f42254010000000000000000000000000000000000ffff58bf997f208d94228854010000000000000000000000000000000000ffffbc8ee72d208de3448854010000000000000000000000000000000000ffff6d780f99208d32448854010000000000000000000000000000000000ffffbc64c76e208d0b3d8854010000000000000000000000000000000000ffff6c414312208d49138854010000000000000000000000000000000000ffff407e52c6208d2d128854010000000000000000000000000000000000ffff6dc987d8208d4acf8754010000000000000000000000000000000000ffffae151a25208de9dd8754010000000000000000000000000000000000ffff522d2d10208d17df8754010000000000000000000000000000000000ffff4d294737208ddbd48754010000000000000000000000000000000000ffff443703fd208d79108854010000000000000000000000000000000000ffff4bb5a154208d1df28753010000000000000000000000000000000000ffff4c44fc51208d5d496a53010000000000000000000000000000000000ffff530924b0208d180b8854010000000000000000000000000000000000ffffc15fced3208d53128854010000000000000000000000000000000000ffff5bc5b9e4208de21f8854010000000000000000000000000000000000ffff2e05fe1f208d31ab6554010000000000000000000000000000000000ffffae865915208d09248854010000000000000000000000000000000000ffff7d71a4b0208d120b8854010000000000000000000000000000000000ffff93e40150208d25d98754010000000000000000000000000000000000ffffc19f7a86208d99e78754010000000000000000000000000000000000ffff4d675993208d2b218854010000000000000000000000000000000000ffff5144776c208d5a208854010000000000000000000000000000000000ffff2e1cce5820f69e8f8154010000000000000000000000000000000000ffff58718848208d9edd6554010000000000000000000000000000000000ffff2ea758c5208d28c98754010000000000000000000000000000000000ffffc60bd694208d181d8854010000000000000000000000000000000000ffffba6b689e208d77868754010000000000000000000000000000000000ffff182016aa208de1698054010000000000000000000000000000000000ffff5cf5daa9208d823d8854010000000000000000000000000000000000ffffbe4d61c8208d2c3b8854010000000000000000000000000000000000ffff71647df2208da2248854010000000000000000000000000000000000ffff4ff359ca208da2dd8754010000000000000000000000000000000000ffff59f1e52b208da6e60d54010000000000000000000000000000000000ffff579596231159ad218854010000000000000000000000000000000000ffff5895f42d208d5ba78754010000000000000000000000000000000000ffff458ae8b0208ddc9b8754010000000000000000000000000000000000ffff18b507a5208d20eb5754010000000000000000000000000000000000ffff5181a537208daf1d8854010000000000000000000000000000000000ffffd5f51eba208d07f58754010000000000000000000000000000000000ffff6dec54bc208d4b3b8854010000000000000000000000000000000000ffff5c3e1991208d13b54054010000000000000000000000000000000000ffffb273845a208d4f338854010000000000000000000000000000000000ffffcfac797b208d4d898754010000000000000000000000000000000000ffffadaf880d208de72f8854010000000000000000000000000000000000ffffd956ef58208dc1e98754010000000000000000000000000000000000ffff980711cf208dfd8f7154010000000000000000000000000000000000ffff50aba98f208d59fe8754010000000000000000000000000000000000ffff5f2ae582208d3ffa8754010000000000000000000000000000000000ffffbca836db208d389b0054010000000000000000000000000000000000ffffded10925208deb154354010000000000000000000000000000000000ffff717532da208dadde7654010000000000000000000000000000000000ffff93afd0e5208d40458854010000000000000000000000000000000000ffff54ee8cb0208d08fa8754010000000000000000000000000000000000ffff3658eba9208d0df58754010000000000000000000000000000000000ffff72c6876e208d5d408854010000000000000000000000000000000000ffffa7582d7c208de8338854010000000000000000000000000000000000ffffad40d7ad208d6c2a8854010000000000000000000000000000000000ffffbc1833a3208d0fc28754010000000000000000000000000000000000ffffcbceb676208d98448854010000000000000000000000000000000000ffff0121ce6e208d77b58754010000000000000000000000000000000000ffff4b40d938208d853b8854010000000000000000000000000000000000ffff5648d643208d1c9c7854010000000000000000000000000000000000ffff6c3230db208d4e028754010000000000000000000000000000000000ffffd8e33c5d208daf738754010000000000000000000000000000000000ffffad4e9d56208dca438854010000000000000000000000000000000000ffff4f8a0353208d7b408854010000000000000000000000000000000000ffff0599e92a208dc62f8854010000000000000000000000000000000000ffff46236049208dc4d58754010000000000000000000000000000000000ffff4400384d208d0ddb4c54010000000000000000000000000000000000ffff71653b5a208d53388854010000000000000000000000000000000000ffff43bddf66208d7c388854010000000000000000000000000000000000ffff47edca96208d8c068854010000000000000000000000000000000000ffffdded3c2c208d89f60554010000000000000000000000000000000000ffff4e60d8d6208da5ff1154010000000000000000000000000000000000ffff55983c68208d72f58754010000000000000000000000000000000000ffffce47f56a208d7e2d8854010000000000000000000000000000000000ffff17e25c12208da7ca8754010000000000000000000000000000000000ffff051d0baa208d19af8754010000000000000000000000000000000000ffffad42d883208dffd68754010000000000000000000000000000000000ffff5418ff88208d87bf8754010000000000000000000000000000000000ffff4c1fe0dc208deef18754010000000000000000000000000000000000ffff4c71d406208d25868754010000000000000000000000000000000000ffffda67a475208d4d348854010000000000000000000000000000000000ffff5d561269208da5918754010000000000000000000000000000000000ffff42b163b3208dbfac3754010000000000000000000000000000000000ffffad4e01eb208d5c408854010000000000000000000000000000000000ffffcc0bb91e208d4a928754010000000000000000000000000000000000ffff531dba30208db9fd8754010000000000000000000000000000000000ffffa3f72b33208dade58754010000000000000000000000000000000000ffffd8b93aeb208db75d4d54010000000000000000000000000000000000ffffae1d5595208de8d18754010000000000000000000000000000000000ffff4405665c208d88e78754010000000000000000000000000000000000ffff6b96180a208dae667354010000000000000000000000000000000000ffff531f490a208d92f98754010000000000000000000000000000000000ffffd447e857208db1428854010000000000000000000000000000000000ffff36c6b4bb208dd42c8854010000000000000000000000000000000000ffff4b4aff14208df71acd53010000000000000000000000000000000000ffff62d17930208da0b88754010000000000000000000000000000000000ffff974bf913208d5faa7953010000000000000000000000000000000000ffff7419d269208d6c1e8854010000000000000000000000000000000000ffff48a72343208d9c2b8854010000000000000000000000000000000000ffff64431e87208de8908754010000000000000000000000000000000000ffff44660d45208d410b7d54010000000000000000000000000000000000ffff43a3340d208d5b298854010000000000000000000000000000000000ffff9ffda678208de7238854010000000000000000000000000000000000ffff189a3759208da5745354010000000000000000000000000000000000ffff57026e31208df23c885401000000000000002a010488006710000523fbe100000001208dddf48754010000000000000000000000000000000000ffffc1531c5b208d61888754010000000000000000000000000000000000ffff42cd8bc1208df92e8854010000000000000000000000000000000000ffff36e1e970208d0cf75754010000000000000000000000000000000000ffffad4e2420208d0f028854010000000000000000000000000000000000ffff6ec61b02208de6228854010000000000000000000000000000000000ffffdcaa807c208d1cac8754010000000000000000000000000000000000ffff6179416c208db6068854010000000000000000000000000000000000ffffd46121e4208d7ea58754010000000000000000000000000000000000ffff80a41679208d91216254010000000000000000000000000000000000ffff69ece091208d0bf78754010000000000000000000000000000000000ffff45320c0b208dd03c8854010000000000000000000000000000000000ffff4859a2a5208d8e308854010000000000000000000000000000000000ffff2e1ce15a208d30f36354010000000000000000000000000000000000ffff4b61ee15208d7aa05553010000000000000000000000000000000000ffffb129186b208dbb238854010000000000000000000000000000000000ffff581a7f6f208d41248754010000000000000000000000000000000000ffffb009055b208dee218854010000000000000000000000000000000000ffff2e7771f9208db4e78754010000000000000000000000000000000000ffff411bf6ee208db5178854010000000000000000000000000000000000ffffacff001b208dbcf487540100000000000000200100009d386ab83061cfabfe3513a3208d76048854010000000000000000000000000000000000ffff2e35dbc2208d83d28754010000000000000000000000000000000000ffffd91bb1d5208d5c3d8854010000000000000000000000000000000000ffff32a47969208df4bd875401000000000000002a0104f8012032250000000000000002208d1bfa8754010000000000000000000000000000000000ffff53a2f4b6208d141c4554010000000000000000000000000000000000ffffb27b8281208d580c7e54010000000000000000000000000000000000ffffc9ea1340208df7228854010000000000000000000000000000000000ffffd90a268d208d41e18754010000000000000000000000000000000000ffff555b8809208d66358854010000000000000000000000000000000000ffff5280ff23208deecd8754010000000000000000000000000000000000ffff84c6a102208df9bc8754010000000000000000000000000000000000ffffbbc96dae208d9d0a8854010000000000000000000000000000000000ffffb2a2d18a208da9458854010000000000000000000000000000000000ffffca16c30e208d2ca88754010000000000000000000000000000000000ffff6baab63e208d923b8854010000000000000000000000000000000000ffffd5b87bee208d12258854010000000000000000000000000000000000ffff59ee408b208d980e8854010000000000000000000000000000000000ffff2edfb0c4208d33378854010000000000000000000000000000000000ffff6caa7b42208d41176d53010000000000000000000000000000000000ffff1fa2df68208dd1c57f54010000000000000000000000000000000000ffffbeab679a208d17c78754010000000000000000000000000000000000ffff3f983f51208dbb278854010000000000000000000000000000000000ffff568dbc28208d5d5ad953010000000000000000000000000000000000ffff0ec112b7208da1b78754010000000000000000000000000000000000ffffbc287698208d99348854010000000000000000000000000000000000ffff5ff1a302208d23ef8454010000000000000000000000000000000000ffff7ba55d22208d90e98754010000000000000000000000000000000000ffff5ae14503208d58f48754010000000000000000000000000000000000ffffd45a3cae208d530c8854010000000000000000000000000000000000ffff18fb80c4208dc6138854010000000000000000000000000000000000ffff4e2fd6eb208d97208854010000000000000000000000000000000000ffff55ddd5b8208d74f88654010000000000000000000000000000000000ffff1809ad86208dc0278854010000000000000000000000000000000000ffff6ef2dfb6208d84338854010000000000000000000000000000000000ffff58d8114e208dcae78754010000000000000000000000000000000000ffffc21c474c208d61e78754010000000000000000000000000000000000ffffd5b3fc7a208d7a078854010000000000000000000000000000000000ffff32740193208de7547b54010000000000000000000000000000000000ffff8046aac3208db5328854010000000000000000000000000000000000ffff5b0c5366208d5f208854010000000000000000000000000000000000ffff6caa054a208d1d682854010000000000000000000000000000000000ffff69e3eb9d208d179e6b54010000000000000000000000000000000000ffff54aac628208ded2e8854010000000000000000000000000000000000ffff4166d226208d8e148854010000000000000000000000000000000000ffff54305af6208d1ed88754010000000000000000000000000000000000ffff3ed2b2a4208d78ec8554010000000000000000000000000000000000ffff6dd2e575208ddd3e7854010000000000000000000000000000000000ffff1b83a102208d421e8854010000000000000000000000000000000000ffff2eb51bf1208dc01f8854010000000000000000000000000000000000ffff58c66033208d08ce8754010000000000000000000000000000000000ffff5d57b84c208df22e8854010000000000000000000000000000000000ffffbf213e3d208d89cb8754010000000000000000000000000000000000ffff53d7ece1208da9d68754010000000000000000000000000000000000ffff71bef412208d38268854010000000000000000000000000000000000ffff84fc8a6f208d4f107254010000000000000000000000000000000000ffff57a072d9208d990f88540100000000000000200100009d3890d71029db21863020f9208df3c36854010000000000000000000000000000000000ffffbba6a6c4208d3ea71f54010000000000000000000000000000000000ffff3e61237b208d60a18754010000000000000000000000000000000000ffff53a56b4b208db2328854010000000000000000000000000000000000ffff44eef265208d2c0f8854010000000000000000000000000000000000ffff566553c0208dec268854010000000000000000000000000000000000ffff57a69ecd208d93148854010000000000000000000000000000000000ffff9b8f4433208d741b8854010000000000000000000000000000000000ffffae17c74c208ddab94f54010000000000000000000000000000000000ffffab19c63a208d5e886f54010000000000000000000000000000000000ffffbc67a037208d5c3ccc53010000000000000000000000000000000000ffff0e77c86e208d26fb8754010000000000000000000000000000000000ffffc1eae172208d61278854010000000000000000000000000000000000ffff5ce15439208d60df8754010000000000000000000000000000000000ffff2599fb3b208d52418854010000000000000000000000000000000000ffff028710e6208dae448854010000000000000000000000000000000000ffff18152eee208d6ff18754010000000000000000000000000000000000ffff542e396d208d8c248854010000000000000000000000000000000000ffff5519d6d8208d01e33e54010000000000000000000000000000000000ffff5027d563208d05cd8754010000000000000000000000000000000000ffff45a5a9c5208d7c1e8854010000000000000000000000000000000000ffff68c813c8208d38a48754010000000000000000000000000000000000ffff3a60a934208d73ec8754010000000000000000000000000000000000ffffae3db8f3208d096c8754010000000000000000000000000000000000ffff4cb88802208d55d38754010000000000000000000000000000000000ffffd31f082e208d0d8a8754010000000000000000000000000000000000ffff6c22c9bb208d81ba7754010000000000000000000000000000000000ffffca5f88da208d5df88754010000000000000000000000000000000000ffffc31abc05208db1d58754010000000000000000000000000000000000ffff5cf614c8208d2b257c54010000000000000000000000000000000000ffff45f9baa0208d38d78754010000000000000000000000000000000000ffff490e0608208d27e88754010000000000000000000000000000000000ffff17f1ccd0208d501c8854010000000000000000000000000000000000ffff5be834a7208d860b8854010000000000000000000000000000000000ffff640173bc208deada8754010000000000000000000000000000000000ffffdc98f8db208d983d3c54010000000000000000000000000000000000ffff021ab398208d99f28754010000000000000000000000000000000000ffff59a9e417208d19468854010000000000000000000000000000000000ffff91ff01a1208ddbf68754010000000000000000000000000000000000ffff50048949208d89118854010000000000000000000000000000000000ffffc8366923208dea3b8854010000000000000000000000000000000000ffffbcbbb5a9208d364a8854010000000000000000000000000000000000ffffc654bd58208dd52ef953010000000000000000000000000000000000ffff7cabb7bc208db086cc53010000000000000000000000000000000000ffff6c4004ea208d0ff48754010000000000000000000000000000000000ffff0536a3cb208d1c0b8854010000000000000000000000000000000000ffffd90bfef3208dbf188854010000000000000000000000000000000000ffff0252431f208dd1c78754010000000000000000000000000000000000ffff25732b19208dd6fa3954010000000000000000000000000000000000ffff6c1131dc208da6108854010000000000000000000000000000000000ffff1fb9b748208daf368854010000000000000000000000000000000000ffff1f2a2981208d9e2f8854010000000000000000000000000000000000ffffbc658482208d6a458854010000000000000000000000000000000000ffff568b843a208dcd428854010000000000000000000000000000000000ffff5d820794208d38258854010000000000000000000000000000000000ffff6440c630208d1cda8754010000000000000000000000000000000000ffff6e9f7be5208ddd388854010000000000000000000000000000000000ffff9f080244208d2e2d8854010000000000000000000000000000000000ffffbb70cf3d208d3e0c8854010000000000000000000000000000000000ffff3ec27a16208d89098854010000000000000000000000000000000000ffffb55fb2fb208d13468854010000000000000000000000000000000000ffff180ab1e5208dfc2a8854010000000000000000000000000000000000ffffd58a5c0e208db9188854010000000000000000000000000000000000ffff81ce808d208d03d06b54010000000000000000000000000000000000ffff4b496858208d319e8454010000000000000000000000000000000000ffff531fac9f208dc8d98754010000000000000000000000000000000000ffffc113e4ea208d06395754010000000000000000000000000000000000ffff7cabf0d6208d722b8854010000000000000000000000000000000000ffff545db4cd208dd91f8854010000000000000000000000000000000000ffff5f12a597208dfbd57054010000000000000000000000000000000000ffff3cf654e6208dd7358854010000000000000000000000000000000000ffffb4998e79208dd7018854010000000000000000000000000000000000ffffb45cc220208d86df5554010000000000000000000000000000000000ffff7660d462208decd28754010000000000000000000000000000000000ffff5b7bdfe2208dafe28754010000000000000000000000000000000000ffff2e76899e208d0c318854010000000000000000000000000000000000ffffc7bcb183208d35258854010000000000000000000000000000000000ffff3ab2d076208db5c38754010000000000000000000000000000000000ffff48b24d30208ddfd18754010000000000000000000000000000000000ffff904cb00c208d40478854010000000000000000000000000000000000ffff02a3b787208deb318854010000000000000000000000000000000000ffffd2564026208de63f8854010000000000000000000000000000000000ffff615d16c0208dbf1d8854010000000000000000000000000000000000ffffc6fff6f0208d4aee8754010000000000000000000000000000000000ffff56c74bc8208d68bc8754010000000000000000000000000000000000ffff31b581f9208ddcca8754010000000000000000000000000000000000ffff0e97397a208db2318854010000000000000000000000000000000000ffffc766348d208de1bd8754010000000000000000000000000000000000ffffd5927a3d208d2aff8754010000000000000000000000000000000000ffff5152d0a5208da7feb753010000000000000000000000000000000000ffff53fe1694208de7418854010000000000000000000000000000000000ffff189f3d99208de7b38754010000000000000000000000000000000000ffff64004655208dacabb653010000000000000000000000000000000000ffff55413231208d43b88754010000000000000000000000000000000000ffff796e1029208d03788754010000000000000000000000000000000000ffff49b90ff6208d04be8754010000000000000000000000000000000000ffffc327ce1c208d33648254010000000000000000000000000000000000ffff2e896478208d9e308854010000000000000000000000000000000000ffffd395d9bf208df13e8854010000000000000000000000000000000000ffff51bb88ed208dec8c5653010000000000000000000000000000000000ffff557f146e208df4538854010000000000000000000000000000000000ffff48d05928208d41bd8154010000000000000000000000000000000000ffff47384162208df6428854010000000000000000000000000000000000ffffc1566314208d753e8854010000000000000000000000000000000000ffff440c9b4f208d2f078854010000000000000000000000000000000000ffff56021a19208d7b0f4754010000000000000000000000000000000000ffff71653a84208dcee88754010000000000000000000000000000000000ffff5401a332208df4db8754010000000000000000000000000000000000ffff5a9db0e3208df8e38754010000000000000000000000000000000000ffff5d5208fa208d2e298854010000000000000000000000000000000000ffff4c61c0bc208d42ea8754010000000000000000000000000000000000ffff4c405ae7208deade8754010000000000000000000000000000000000ffffc009c823208d99358854010000000000000000000000000000000000ffff6cc2b493208df3fd8754010000000000000000000000000000000000ffff904c6007208d1fde8754010000000000000000000000000000000000ffff92732ae8208d666e8354010000000000000000000000000000000000ffff6daa9d16208d18f78754010000000000000000000000000000000000ffff4d6d8d8a208ddea9ce53010000000000000000000000000000000000ffff17e26f7d208d1ae48754010000000000000000000000000000000000ffffc32ebb89208dee258854010000000000000000000000000000000000ffff4a0f10e1208d290a8854010000000000000000000000000000000000ffff42731593208d7cb33354010000000000000000000000000000000000ffff532e8b97208dfd1e8854010000000000000000000000000000000000ffff6ee91680208d480a1354010000000000000000000000000000000000ffff60f220f6208d55838754010000000000000000000000000000000000ffff1f36d48f208d247e8754010000000000000000000000000000000000ffff44b5a49a208d0efa4954010000000000000000000000000000000000ffff63890597208d862e8854010000000000000000000000000000000000ffffc14d8751208d443c8854010000000000000000000000000000000000ffff70d16dba208d72fe8754010000000000000000000000000000000000ffff55983dd3208d236b6854010000000000000000000000000000000000ffff7aea27f9208d67e08754010000000000000000000000000000000000ffff1b20750d208df10a7154010000000000000000000000000000000000ffff7b9fe56e208db32d8854010000000000000000000000000000000000ffff7ba42d54208db3f58754010000000000000000000000000000000000ffff027b4eba208d70d68754010000000000000000000000000000000000ffffbc1b62b9208d97951454010000000000000000000000000000000000ffffb4b6af10208d251288540100000000000000200100009d386ab800773149c4d7e673208d7b168854010000000000000000000000000000000000ffff58969392208d91d18754010000000000000000000000000000000000ffff9ffd6d4e208d19008854010000000000000000000000000000000000ffff4e926cb8208dd1c98754010000000000000000000000000000000000ffff546cdb77208d6a0a8854010000000000000000000000000000000000ffff6caa8c15208d3a3a8854010000000000000000000000000000000000ffff62e28d6a208d3fe48754010000000000000000000000000000000000ffffd9850be7208dcb438854010000000000000000000000000000000000ffff6c2444b3208d78338854010000000000000000000000000000000000ffffb2df574a208d3b2d3054010000000000000000000000000000000000ffff4b529e67208d5b988754010000000000000000000000000000000000ffff36e152e6208d01ab8754010000000000000000000000000000000000ffff58b531bf208d7b038854010000000000000000000000000000000000ffff54717a6f208db0388854010000000000000000000000000000000000ffff4a694e98208d65418854010000000000000000000000000000000000ffffcef84b25208dcf218854010000000000000000000000000000000000ffff5364fed9208db6c68754010000000000000000000000000000000000ffff4d03decd208dc6158854010000000000000000000000000000000000ffffb9044c79208d65418854010000000000000000000000000000000000ffff56b2192f208d23308754010000000000000000000000000000000000ffffd973f7a4208d05158854010000000000000000000000000000000000ffff1b6d9975208d52cd8754010000000000000000000000000000000000ffffdf5b92ad208d3a128854010000000000000000000000000000000000ffff6d93787e208d351e8854010000000000000000000000000000000000ffffb6a4336f208d9bd58754010000000000000000000000000000000000ffff3ba7f521208dc3288854010000000000000000000000000000000000ffff6bbf20cc208d68998754010000000000000000000000000000000000ffff63b30032208d86848754010000000000000000000000000000000000ffffb52ef15c208dff108854010000000000000000000000000000000000ffff4d06718b208d226f0754010000000000000000000000000000000000ffff44248ba1208da3af7354010000000000000000000000000000000000ffff86f9d72c208d9fd68754010000000000000000000000000000000000ffff521f2444208dae428854010000000000000000000000000000000000ffffd445b025208dfafc8754010000000000000000000000000000000000ffffd447fa84208dbb358854010000000000000000000000000000000000ffff3ee2d196208da01a8854010000000000000000000000000000000000ffff45a615b2208d32edb053010000000000000000000000000000000000ffff6d6494da208d77ed8754010000000000000000000000000000000000ffff555dcc44208d09608754010000000000000000000000000000000000ffff45a5d222208d84558754010000000000000000000000000000000000ffffb01c30ab208d71fe8754010000000000000000000000000000000000ffffae5f68c2208de0acdf53010000000000000000000000000000000000ffffae737936208d29e18754010000000000000000000000000000000000ffffb00a63cb208d220b8854010000000000000000000000000000000000ffff9ec4d12f208db4408854010000000000000000000000000000000000ffff4c6d9bcc208d65a58754010000000000000000000000000000000000ffff5962c453208d2d628854010000000000000000000000000000000000ffff257191d50000", + "payload": "fdf5016816fa53010000000000000000000000000000000000ffff51403eea208ddb2a8854010000000000000000000000000000000000ffff707c60d9208d31413a54010000000000000000000000000000000000ffff5ed5fd13208d182c8854010000000000000000000000000000000000ffff505f3f81208d65ecb853010000000000000000000000000000000000ffff54d72104208dcca94054010000000000000000000000000000000000ffff40bbe1f2208d73238854010000000000000000000000000000000000ffff55d683cd208d5d258854010000000000000000000000000000000000ffffd523a66d208dcbea87540100000000000000200100005ef579fd2c9d47be4b56d671208db3e58754010000000000000000000000000000000000ffffae373156208d11e58754810000000000000000000000000000000000ffff51071276208d38f48754010000000000000000000000000000000000ffff6d4372e0208d4f448854010000000000000000000000000000000000ffffc556cbea208deb208854010000000000000000000000000000000000ffff450eb059208d97336054010000000000000000000000000000000000ffff4d60444a208dad608754010000000000000000000000000000000000ffff01abd3c3208d2cbe8754010000000000000000000000000000000000ffff7963d8b8208d9dfa8754010000000000000000000000000000000000ffffcebe862c208da7f66554010000000000000000000000000000000000ffff62f2a82e208d430d8854010000000000000000000000000000000000ffff6deb6956208d08148854010000000000000000000000000000000000ffff4b9804ef208d950e8854010000000000000000000000000000000000ffff6b8dd2e1208d1cfc8754010000000000000000000000000000000000ffffb2fe1dab208de7458754010000000000000000000000000000000000ffff32cccb82208d12625654010000000000000000000000000000000000ffff55e6f35a208dd7908354010000000000000000000000000000000000ffff4c642e6f208d17398854010000000000000000000000000000000000ffff45ea14df208dc72d8854010000000000000000000000000000000000ffffdaa48b96208d57fb8654010000000000000000000000000000000000ffff5f0080a9208d4e9a6d54010000000000000000000000000000000000ffff6d94e741208d7a418854010000000000000000000000000000000000ffff6d43b0c1208d5c098854010000000000000000000000000000000000ffff9c11e7ea208d442d8854010000000000000000000000000000000000ffffd0424482208db142fb53010000000000000000000000000000000000ffff54344a4a208d70158854010000000000000000000000000000000000ffff538f8201208d39288854010000000000000000000000000000000000ffff57daab71208d262f8854010000000000000000000000000000000000ffff57e045b1208d91430654010000000000000000000000000000000000ffff4212def5208d7c468854010000000000000000000000000000000000ffff62a4f53b208d9c3e8854010000000000000000000000000000000000ffff5b736bcc208d4d074f54010000000000000000000000000000000000ffff43af9d9b208deaca8754010000000000000000000000000000000000ffff4a8084a0208d74f68754010000000000000000000000000000000000ffff46304541208d5d098854010000000000000000000000000000000000ffff6eabb626208d08228854010000000000000000000000000000000000ffff1f07b030208d959a8254010000000000000000000000000000000000ffff4fa996b3208d4bac8754010000000000000000000000000000000000ffff43a0605b208de4038854010000000000000000000000000000000000ffff4cb41a5b208ddc308854010000000000000000000000000000000000ffffb72e6072208deac86f54010000000000000000000000000000000000ffffd5b815e2208d1b118854010000000000000000000000000000000000ffff3ff8d0a4208db1258854010000000000000000000000000000000000ffff5edd3a6a208d20268754010000000000000000000000000000000000ffff59ed3cfc208d83ed8754010000000000000000000000000000000000ffff4460baea208d71278854010000000000000000000000000000000000ffff4583b547208da7968754010000000000000000000000000000000000ffffd1c34463208d9d428854010000000000000000000000000000000000ffff43c15bc5208d33448854010000000000000000000000000000000000ffff2e0d8a5c208dfa2e7154010000000000000000000000000000000000ffffceff2bca208d47288854010000000000000000000000000000000000ffff5b8d0190208d0e847954010000000000000000000000000000000000ffff5501b8c2208d552f8854010000000000000000000000000000000000ffff5cf3b604208d8c2e8854010000000000000000000000000000000000ffff58666b0d208dcbdd8754010000000000000000000000000000000000ffff59a85cf9208d04208854010000000000000000000000000000000000ffff5f60abe0208dba238854010000000000000000000000000000000000ffff2e77ce67208d62c68754010000000000000000000000000000000000ffffcbae5658208d41108854010000000000000000000000000000000000ffff49329ec8208ddf1d8854010000000000000000000000000000000000ffff603ada12208df90c8854010000000000000000000000000000000000ffff51e0c21c208d5dff8754010000000000000000000000000000000000ffff904ca539208de0118854010000000000000000000000000000000000ffff5c0cecf7208d1b2d88540100000000000000200100005ef579fb18f317b2aba4edfc208dfaa27f54010000000000000000000000000000000000ffff54c65a7c208dc2db8754010000000000000000000000000000000000ffffad0b7981208d88a58754010000000000000000000000000000000000ffff188a1995208de0d78754010000000000000000000000000000000000ffff62a7a6bd208d18f66b54010000000000000000000000000000000000ffffdb4fbe80208dfcf78754010000000000000000000000000000000000ffff67145102208d3d3a8854010000000000000000000000000000000000ffff64259bd6208dcaa98654010000000000000000000000000000000000ffff32c737e1208d3a6a5c54010000000000000000000000000000000000ffff6d793f75208d22d38754010000000000000000000000000000000000ffff0595cc0d208da9ea8754010000000000000000000000000000000000ffff62ca37d3208d440e8854010000000000000000000000000000000000ffff123e1c11208dab428854010000000000000000000000000000000000ffff461df51f208d0f0b8854010000000000000000000000000000000000ffff59d36752208d631f8854010000000000000000000000000000000000ffff5ef8c640208dbc1d8854010000000000000000000000000000000000ffff5f5dc582208d04bf8754010000000000000000000000000000000000ffff0e6797e3208db8028854010000000000000000000000000000000000ffff3cf0c4a1208d6a0c8854010000000000000000000000000000000000ffff58bb5c55208d4f438854010000000000000000000000000000000000ffff5d326005208df6068854010000000000000000000000000000000000ffff5d5a5850208d40eb8754010000000000000000000000000000000000ffff5751ed86208dbad18754010000000000000000000000000000000000ffff63e51608208d99108854010000000000000000000000000000000000ffff36e299cd208d9a1e88540100000000000000200100005ef579fd38a43785a5d441b7208d13258854010000000000000000000000000000000000ffffb90de279208db3778754010000000000000000000000000000000000ffff67fcc80c208d3d358854010000000000000000000000000000000000ffffd00c40fc208dfd0d8854010000000000000000000000000000000000ffff5e89c2f3208dd6428854010000000000000000000000000000000000ffff1863e14a208d89e78754010000000000000000000000000000000000ffff43a11d76208d89ce8754010000000000000000000000000000000000ffff3edb6281208d03298854010000000000000000000000000000000000ffff6d5b26dd208d16df6e54010000000000000000000000000000000000ffffd9c3d176208d7c028854010000000000000000000000000000000000ffffbcc2db1f208d67fc8754010000000000000000000000000000000000ffff59910667208d86b18754010000000000000000000000000000000000ffff6c1482b9208d790b8854010000000000000000000000000000000000ffff0263150d208d89c44554010000000000000000000000000000000000ffff72fcd6b8208d331c8854010000000000000000000000000000000000ffff4c7dcfa8208d9fe98754010000000000000000000000000000000000ffffad08fd05208d0ecd8754010000000000000000000000000000000000ffff7c0fec89208d4af98754010000000000000000000000000000000000ffff2ea6a167208d270b8854010000000000000000000000000000000000ffff59d40fca208d11cf4054010000000000000000000000000000000000ffffb52ac058208d63358254010000000000000000000000000000000000ffff6bd9a130208dbde48754010000000000000000000000000000000000ffff4f915dc7208dece78754010000000000000000000000000000000000ffff5f8423ef208dba308854010000000000000000000000000000000000ffff904cba8c208d953d8854010000000000000000000000000000000000ffff8d1421e5208d5f188854010000000000000000000000000000000000ffff4e1bbfb6208d36fc8754010000000000000000000000000000000000ffff3205cfb6208d5e458854010000000000000000000000000000000000ffff493092c2208dd0878754010000000000000000000000000000000000ffff97caacde208dd99d8754010000000000000000000000000000000000ffffc05f3c38208da7018854010000000000000000000000000000000000ffffdcf5f82d208df0f42254010000000000000000000000000000000000ffff58bf997f208d94228854010000000000000000000000000000000000ffffbc8ee72d208de3448854010000000000000000000000000000000000ffff6d780f99208d32448854010000000000000000000000000000000000ffffbc64c76e208d0b3d8854010000000000000000000000000000000000ffff6c414312208d49138854010000000000000000000000000000000000ffff407e52c6208d2d128854010000000000000000000000000000000000ffff6dc987d8208d4acf8754010000000000000000000000000000000000ffffae151a25208de9dd8754010000000000000000000000000000000000ffff522d2d10208d17df8754010000000000000000000000000000000000ffff4d294737208ddbd48754010000000000000000000000000000000000ffff443703fd208d79108854010000000000000000000000000000000000ffff4bb5a154208d1df28753010000000000000000000000000000000000ffff4c44fc51208d5d496a53010000000000000000000000000000000000ffff530924b0208d180b8854010000000000000000000000000000000000ffffc15fced3208d53128854010000000000000000000000000000000000ffff5bc5b9e4208de21f8854010000000000000000000000000000000000ffff2e05fe1f208d31ab6554010000000000000000000000000000000000ffffae865915208d09248854010000000000000000000000000000000000ffff7d71a4b0208d120b8854010000000000000000000000000000000000ffff93e40150208d25d98754010000000000000000000000000000000000ffffc19f7a86208d99e78754010000000000000000000000000000000000ffff4d675993208d2b218854010000000000000000000000000000000000ffff5144776c208d5a208854010000000000000000000000000000000000ffff2e1cce5820f69e8f8154010000000000000000000000000000000000ffff58718848208d9edd6554010000000000000000000000000000000000ffff2ea758c5208d28c98754010000000000000000000000000000000000ffffc60bd694208d181d8854010000000000000000000000000000000000ffffba6b689e208d77868754010000000000000000000000000000000000ffff182016aa208de1698054010000000000000000000000000000000000ffff5cf5daa9208d823d8854010000000000000000000000000000000000ffffbe4d61c8208d2c3b8854010000000000000000000000000000000000ffff71647df2208da2248854010000000000000000000000000000000000ffff4ff359ca208da2dd8754010000000000000000000000000000000000ffff59f1e52b208da6e60d54010000000000000000000000000000000000ffff579596231159ad218854010000000000000000000000000000000000ffff5895f42d208d5ba78754010000000000000000000000000000000000ffff458ae8b0208ddc9b8754010000000000000000000000000000000000ffff18b507a5208d20eb5754010000000000000000000000000000000000ffff5181a537208daf1d8854010000000000000000000000000000000000ffffd5f51eba208d07f58754010000000000000000000000000000000000ffff6dec54bc208d4b3b8854010000000000000000000000000000000000ffff5c3e1991208d13b54054010000000000000000000000000000000000ffffb273845a208d4f338854010000000000000000000000000000000000ffffcfac797b208d4d898754010000000000000000000000000000000000ffffadaf880d208de72f8854010000000000000000000000000000000000ffffd956ef58208dc1e98754010000000000000000000000000000000000ffff980711cf208dfd8f7154010000000000000000000000000000000000ffff50aba98f208d59fe8754010000000000000000000000000000000000ffff5f2ae582208d3ffa8754010000000000000000000000000000000000ffffbca836db208d389b0054010000000000000000000000000000000000ffffded10925208deb154354010000000000000000000000000000000000ffff717532da208dadde7654010000000000000000000000000000000000ffff93afd0e5208d40458854010000000000000000000000000000000000ffff54ee8cb0208d08fa8754010000000000000000000000000000000000ffff3658eba9208d0df58754010000000000000000000000000000000000ffff72c6876e208d5d408854010000000000000000000000000000000000ffffa7582d7c208de8338854010000000000000000000000000000000000ffffad40d7ad208d6c2a8854010000000000000000000000000000000000ffffbc1833a3208d0fc28754010000000000000000000000000000000000ffffcbceb676208d98448854010000000000000000000000000000000000ffff0121ce6e208d77b58754010000000000000000000000000000000000ffff4b40d938208d853b8854010000000000000000000000000000000000ffff5648d643208d1c9c7854010000000000000000000000000000000000ffff6c3230db208d4e028754010000000000000000000000000000000000ffffd8e33c5d208daf738754010000000000000000000000000000000000ffffad4e9d56208dca438854010000000000000000000000000000000000ffff4f8a0353208d7b408854010000000000000000000000000000000000ffff0599e92a208dc62f8854010000000000000000000000000000000000ffff46236049208dc4d58754010000000000000000000000000000000000ffff4400384d208d0ddb4c54010000000000000000000000000000000000ffff71653b5a208d53388854010000000000000000000000000000000000ffff43bddf66208d7c388854010000000000000000000000000000000000ffff47edca96208d8c068854010000000000000000000000000000000000ffffdded3c2c208d89f60554010000000000000000000000000000000000ffff4e60d8d6208da5ff1154010000000000000000000000000000000000ffff55983c68208d72f58754010000000000000000000000000000000000ffffce47f56a208d7e2d8854010000000000000000000000000000000000ffff17e25c12208da7ca8754010000000000000000000000000000000000ffff051d0baa208d19af8754010000000000000000000000000000000000ffffad42d883208dffd68754010000000000000000000000000000000000ffff5418ff88208d87bf8754010000000000000000000000000000000000ffff4c1fe0dc208deef18754010000000000000000000000000000000000ffff4c71d406208d25868754010000000000000000000000000000000000ffffda67a475208d4d348854010000000000000000000000000000000000ffff5d561269208da5918754010000000000000000000000000000000000ffff42b163b3208dbfac3754010000000000000000000000000000000000ffffad4e01eb208d5c408854010000000000000000000000000000000000ffffcc0bb91e208d4a928754010000000000000000000000000000000000ffff531dba30208db9fd8754010000000000000000000000000000000000ffffa3f72b33208dade58754010000000000000000000000000000000000ffffd8b93aeb208db75d4d54010000000000000000000000000000000000ffffae1d5595208de8d18754010000000000000000000000000000000000ffff4405665c208d88e78754010000000000000000000000000000000000ffff6b96180a208dae667354010000000000000000000000000000000000ffff531f490a208d92f98754010000000000000000000000000000000000ffffd447e857208db1428854010000000000000000000000000000000000ffff36c6b4bb208dd42c8854010000000000000000000000000000000000ffff4b4aff14208df71acd53010000000000000000000000000000000000ffff62d17930208da0b88754010000000000000000000000000000000000ffff974bf913208d5faa7953010000000000000000000000000000000000ffff7419d269208d6c1e8854010000000000000000000000000000000000ffff48a72343208d9c2b8854010000000000000000000000000000000000ffff64431e87208de8908754010000000000000000000000000000000000ffff44660d45208d410b7d54010000000000000000000000000000000000ffff43a3340d208d5b298854010000000000000000000000000000000000ffff9ffda678208de7238854010000000000000000000000000000000000ffff189a3759208da5745354010000000000000000000000000000000000ffff57026e31208df23c885401000000000000002a010488006710000523fbe100000001208dddf48754010000000000000000000000000000000000ffffc1531c5b208d61888754010000000000000000000000000000000000ffff42cd8bc1208df92e8854010000000000000000000000000000000000ffff36e1e970208d0cf75754010000000000000000000000000000000000ffffad4e2420208d0f028854010000000000000000000000000000000000ffff6ec61b02208de6228854010000000000000000000000000000000000ffffdcaa807c208d1cac8754010000000000000000000000000000000000ffff6179416c208db6068854010000000000000000000000000000000000ffffd46121e4208d7ea58754010000000000000000000000000000000000ffff80a41679208d91216254010000000000000000000000000000000000ffff69ece091208d0bf78754010000000000000000000000000000000000ffff45320c0b208dd03c8854010000000000000000000000000000000000ffff4859a2a5208d8e308854010000000000000000000000000000000000ffff2e1ce15a208d30f36354010000000000000000000000000000000000ffff4b61ee15208d7aa05553010000000000000000000000000000000000ffffb129186b208dbb238854010000000000000000000000000000000000ffff581a7f6f208d41248754010000000000000000000000000000000000ffffb009055b208dee218854010000000000000000000000000000000000ffff2e7771f9208db4e78754010000000000000000000000000000000000ffff411bf6ee208db5178854010000000000000000000000000000000000ffffacff001b208dbcf487540100000000000000200100009d386ab83061cfabfe3513a3208d76048854010000000000000000000000000000000000ffff2e35dbc2208d83d28754010000000000000000000000000000000000ffffd91bb1d5208d5c3d8854010000000000000000000000000000000000ffff32a47969208df4bd875401000000000000002a0104f8012032250000000000000002208d1bfa8754010000000000000000000000000000000000ffff53a2f4b6208d141c4554010000000000000000000000000000000000ffffb27b8281208d580c7e54010000000000000000000000000000000000ffffc9ea1340208df7228854010000000000000000000000000000000000ffffd90a268d208d41e18754010000000000000000000000000000000000ffff555b8809208d66358854010000000000000000000000000000000000ffff5280ff23208deecd8754010000000000000000000000000000000000ffff84c6a102208df9bc8754010000000000000000000000000000000000ffffbbc96dae208d9d0a8854010000000000000000000000000000000000ffffb2a2d18a208da9458854010000000000000000000000000000000000ffffca16c30e208d2ca88754010000000000000000000000000000000000ffff6baab63e208d923b8854010000000000000000000000000000000000ffffd5b87bee208d12258854010000000000000000000000000000000000ffff59ee408b208d980e8854010000000000000000000000000000000000ffff2edfb0c4208d33378854010000000000000000000000000000000000ffff6caa7b42208d41176d53010000000000000000000000000000000000ffff1fa2df68208dd1c57f54010000000000000000000000000000000000ffffbeab679a208d17c78754010000000000000000000000000000000000ffff3f983f51208dbb278854010000000000000000000000000000000000ffff568dbc28208d5d5ad953010000000000000000000000000000000000ffff0ec112b7208da1b78754010000000000000000000000000000000000ffffbc287698208d99348854010000000000000000000000000000000000ffff5ff1a302208d23ef8454010000000000000000000000000000000000ffff7ba55d22208d90e98754010000000000000000000000000000000000ffff5ae14503208d58f48754010000000000000000000000000000000000ffffd45a3cae208d530c8854010000000000000000000000000000000000ffff18fb80c4208dc6138854010000000000000000000000000000000000ffff4e2fd6eb208d97208854010000000000000000000000000000000000ffff55ddd5b8208d74f88654010000000000000000000000000000000000ffff1809ad86208dc0278854010000000000000000000000000000000000ffff6ef2dfb6208d84338854010000000000000000000000000000000000ffff58d8114e208dcae78754010000000000000000000000000000000000ffffc21c474c208d61e78754010000000000000000000000000000000000ffffd5b3fc7a208d7a078854010000000000000000000000000000000000ffff32740193208de7547b54010000000000000000000000000000000000ffff8046aac3208db5328854010000000000000000000000000000000000ffff5b0c5366208d5f208854010000000000000000000000000000000000ffff6caa054a208d1d682854010000000000000000000000000000000000ffff69e3eb9d208d179e6b54010000000000000000000000000000000000ffff54aac628208ded2e8854010000000000000000000000000000000000ffff4166d226208d8e148854010000000000000000000000000000000000ffff54305af6208d1ed88754010000000000000000000000000000000000ffff3ed2b2a4208d78ec8554010000000000000000000000000000000000ffff6dd2e575208ddd3e7854010000000000000000000000000000000000ffff1b83a102208d421e8854010000000000000000000000000000000000ffff2eb51bf1208dc01f8854010000000000000000000000000000000000ffff58c66033208d08ce8754010000000000000000000000000000000000ffff5d57b84c208df22e8854010000000000000000000000000000000000ffffbf213e3d208d89cb8754010000000000000000000000000000000000ffff53d7ece1208da9d68754010000000000000000000000000000000000ffff71bef412208d38268854010000000000000000000000000000000000ffff84fc8a6f208d4f107254010000000000000000000000000000000000ffff57a072d9208d990f88540100000000000000200100009d3890d71029db21863020f9208df3c36854010000000000000000000000000000000000ffffbba6a6c4208d3ea71f54010000000000000000000000000000000000ffff3e61237b208d60a18754010000000000000000000000000000000000ffff53a56b4b208db2328854010000000000000000000000000000000000ffff44eef265208d2c0f8854010000000000000000000000000000000000ffff566553c0208dec268854010000000000000000000000000000000000ffff57a69ecd208d93148854010000000000000000000000000000000000ffff9b8f4433208d741b8854010000000000000000000000000000000000ffffae17c74c208ddab94f54010000000000000000000000000000000000ffffab19c63a208d5e886f54010000000000000000000000000000000000ffffbc67a037208d5c3ccc53010000000000000000000000000000000000ffff0e77c86e208d26fb8754010000000000000000000000000000000000ffffc1eae172208d61278854010000000000000000000000000000000000ffff5ce15439208d60df8754010000000000000000000000000000000000ffff2599fb3b208d52418854010000000000000000000000000000000000ffff028710e6208dae448854010000000000000000000000000000000000ffff18152eee208d6ff18754010000000000000000000000000000000000ffff542e396d208d8c248854010000000000000000000000000000000000ffff5519d6d8208d01e33e54010000000000000000000000000000000000ffff5027d563208d05cd8754010000000000000000000000000000000000ffff45a5a9c5208d7c1e8854010000000000000000000000000000000000ffff68c813c8208d38a48754010000000000000000000000000000000000ffff3a60a934208d73ec8754010000000000000000000000000000000000ffffae3db8f3208d096c8754010000000000000000000000000000000000ffff4cb88802208d55d38754010000000000000000000000000000000000ffffd31f082e208d0d8a8754010000000000000000000000000000000000ffff6c22c9bb208d81ba7754010000000000000000000000000000000000ffffca5f88da208d5df88754010000000000000000000000000000000000ffffc31abc05208db1d58754010000000000000000000000000000000000ffff5cf614c8208d2b257c54010000000000000000000000000000000000ffff45f9baa0208d38d78754010000000000000000000000000000000000ffff490e0608208d27e88754010000000000000000000000000000000000ffff17f1ccd0208d501c8854010000000000000000000000000000000000ffff5be834a7208d860b8854010000000000000000000000000000000000ffff640173bc208deada8754010000000000000000000000000000000000ffffdc98f8db208d983d3c54010000000000000000000000000000000000ffff021ab398208d99f28754010000000000000000000000000000000000ffff59a9e417208d19468854010000000000000000000000000000000000ffff91ff01a1208ddbf68754010000000000000000000000000000000000ffff50048949208d89118854010000000000000000000000000000000000ffffc8366923208dea3b8854010000000000000000000000000000000000ffffbcbbb5a9208d364a8854010000000000000000000000000000000000ffffc654bd58208dd52ef953010000000000000000000000000000000000ffff7cabb7bc208db086cc53010000000000000000000000000000000000ffff6c4004ea208d0ff48754010000000000000000000000000000000000ffff0536a3cb208d1c0b8854010000000000000000000000000000000000ffffd90bfef3208dbf188854010000000000000000000000000000000000ffff0252431f208dd1c78754010000000000000000000000000000000000ffff25732b19208dd6fa3954010000000000000000000000000000000000ffff6c1131dc208da6108854010000000000000000000000000000000000ffff1fb9b748208daf368854010000000000000000000000000000000000ffff1f2a2981208d9e2f8854010000000000000000000000000000000000ffffbc658482208d6a458854010000000000000000000000000000000000ffff568b843a208dcd428854010000000000000000000000000000000000ffff5d820794208d38258854010000000000000000000000000000000000ffff6440c630208d1cda8754010000000000000000000000000000000000ffff6e9f7be5208ddd388854010000000000000000000000000000000000ffff9f080244208d2e2d8854010000000000000000000000000000000000ffffbb70cf3d208d3e0c8854010000000000000000000000000000000000ffff3ec27a16208d89098854010000000000000000000000000000000000ffffb55fb2fb208d13468854010000000000000000000000000000000000ffff180ab1e5208dfc2a8854010000000000000000000000000000000000ffffd58a5c0e208db9188854010000000000000000000000000000000000ffff81ce808d208d03d06b54010000000000000000000000000000000000ffff4b496858208d319e8454010000000000000000000000000000000000ffff531fac9f208dc8d98754010000000000000000000000000000000000ffffc113e4ea208d06395754010000000000000000000000000000000000ffff7cabf0d6208d722b8854010000000000000000000000000000000000ffff545db4cd208dd91f8854010000000000000000000000000000000000ffff5f12a597208dfbd57054010000000000000000000000000000000000ffff3cf654e6208dd7358854010000000000000000000000000000000000ffffb4998e79208dd7018854010000000000000000000000000000000000ffffb45cc220208d86df5554010000000000000000000000000000000000ffff7660d462208decd28754010000000000000000000000000000000000ffff5b7bdfe2208dafe28754010000000000000000000000000000000000ffff2e76899e208d0c318854010000000000000000000000000000000000ffffc7bcb183208d35258854010000000000000000000000000000000000ffff3ab2d076208db5c38754010000000000000000000000000000000000ffff48b24d30208ddfd18754010000000000000000000000000000000000ffff904cb00c208d40478854010000000000000000000000000000000000ffff02a3b787208deb318854010000000000000000000000000000000000ffffd2564026208de63f8854010000000000000000000000000000000000ffff615d16c0208dbf1d8854010000000000000000000000000000000000ffffc6fff6f0208d4aee8754010000000000000000000000000000000000ffff56c74bc8208d68bc8754010000000000000000000000000000000000ffff31b581f9208ddcca8754010000000000000000000000000000000000ffff0e97397a208db2318854010000000000000000000000000000000000ffffc766348d208de1bd8754010000000000000000000000000000000000ffffd5927a3d208d2aff8754010000000000000000000000000000000000ffff5152d0a5208da7feb753010000000000000000000000000000000000ffff53fe1694208de7418854010000000000000000000000000000000000ffff189f3d99208de7b38754010000000000000000000000000000000000ffff64004655208dacabb653010000000000000000000000000000000000ffff55413231208d43b88754010000000000000000000000000000000000ffff796e1029208d03788754010000000000000000000000000000000000ffff49b90ff6208d04be8754010000000000000000000000000000000000ffffc327ce1c208d33648254010000000000000000000000000000000000ffff2e896478208d9e308854010000000000000000000000000000000000ffffd395d9bf208df13e8854010000000000000000000000000000000000ffff51bb88ed208dec8c5653010000000000000000000000000000000000ffff557f146e208df4538854010000000000000000000000000000000000ffff48d05928208d41bd8154010000000000000000000000000000000000ffff47384162208df6428854010000000000000000000000000000000000ffffc1566314208d753e8854010000000000000000000000000000000000ffff440c9b4f208d2f078854010000000000000000000000000000000000ffff56021a19208d7b0f4754010000000000000000000000000000000000ffff71653a84208dcee88754010000000000000000000000000000000000ffff5401a332208df4db8754010000000000000000000000000000000000ffff5a9db0e3208df8e38754010000000000000000000000000000000000ffff5d5208fa208d2e298854010000000000000000000000000000000000ffff4c61c0bc208d42ea8754010000000000000000000000000000000000ffff4c405ae7208deade8754010000000000000000000000000000000000ffffc009c823208d99358854010000000000000000000000000000000000ffff6cc2b493208df3fd8754010000000000000000000000000000000000ffff904c6007208d1fde8754010000000000000000000000000000000000ffff92732ae8208d666e8354010000000000000000000000000000000000ffff6daa9d16208d18f78754010000000000000000000000000000000000ffff4d6d8d8a208ddea9ce53010000000000000000000000000000000000ffff17e26f7d208d1ae48754010000000000000000000000000000000000ffffc32ebb89208dee258854010000000000000000000000000000000000ffff4a0f10e1208d290a8854010000000000000000000000000000000000ffff42731593208d7cb33354010000000000000000000000000000000000ffff532e8b97208dfd1e8854010000000000000000000000000000000000ffff6ee91680208d480a1354010000000000000000000000000000000000ffff60f220f6208d55838754010000000000000000000000000000000000ffff1f36d48f208d247e8754010000000000000000000000000000000000ffff44b5a49a208d0efa4954010000000000000000000000000000000000ffff63890597208d862e8854010000000000000000000000000000000000ffffc14d8751208d443c8854010000000000000000000000000000000000ffff70d16dba208d72fe8754010000000000000000000000000000000000ffff55983dd3208d236b6854010000000000000000000000000000000000ffff7aea27f9208d67e08754010000000000000000000000000000000000ffff1b20750d208df10a7154010000000000000000000000000000000000ffff7b9fe56e208db32d8854010000000000000000000000000000000000ffff7ba42d54208db3f58754010000000000000000000000000000000000ffff027b4eba208d70d68754010000000000000000000000000000000000ffffbc1b62b9208d97951454010000000000000000000000000000000000ffffb4b6af10208d251288540100000000000000200100009d386ab800773149c4d7e673208d7b168854010000000000000000000000000000000000ffff58969392208d91d18754010000000000000000000000000000000000ffff9ffd6d4e208d19008854010000000000000000000000000000000000ffff4e926cb8208dd1c98754010000000000000000000000000000000000ffff546cdb77208d6a0a8854010000000000000000000000000000000000ffff6caa8c15208d3a3a8854010000000000000000000000000000000000ffff62e28d6a208d3fe48754010000000000000000000000000000000000ffffd9850be7208dcb438854010000000000000000000000000000000000ffff6c2444b3208d78338854010000000000000000000000000000000000ffffb2df574a208d3b2d3054010000000000000000000000000000000000ffff4b529e67208d5b988754010000000000000000000000000000000000ffff36e152e6208d01ab8754010000000000000000000000000000000000ffff58b531bf208d7b038854010000000000000000000000000000000000ffff54717a6f208db0388854010000000000000000000000000000000000ffff4a694e98208d65418854010000000000000000000000000000000000ffffcef84b25208dcf218854010000000000000000000000000000000000ffff5364fed9208db6c68754010000000000000000000000000000000000ffff4d03decd208dc6158854010000000000000000000000000000000000ffffb9044c79208d65418854010000000000000000000000000000000000ffff56b2192f208d23308754010000000000000000000000000000000000ffffd973f7a4208d05158854010000000000000000000000000000000000ffff1b6d9975208d52cd8754010000000000000000000000000000000000ffffdf5b92ad208d3a128854010000000000000000000000000000000000ffff6d93787e208d351e8854010000000000000000000000000000000000ffffb6a4336f208d9bd58754010000000000000000000000000000000000ffff3ba7f521208dc3288854010000000000000000000000000000000000ffff6bbf20cc208d68998754010000000000000000000000000000000000ffff63b30032208d86848754010000000000000000000000000000000000ffffb52ef15c208dff108854010000000000000000000000000000000000ffff4d06718b208d226f0754010000000000000000000000000000000000ffff44248ba1208da3af7354010000000000000000000000000000000000ffff86f9d72c208d9fd68754010000000000000000000000000000000000ffff521f2444208dae428854010000000000000000000000000000000000ffffd445b025208dfafc8754010000000000000000000000000000000000ffffd447fa84208dbb358854010000000000000000000000000000000000ffff3ee2d196208da01a8854010000000000000000000000000000000000ffff45a615b2208d32edb053010000000000000000000000000000000000ffff6d6494da208d77ed8754010000000000000000000000000000000000ffff555dcc44208d09608754010000000000000000000000000000000000ffff45a5d222208d84558754010000000000000000000000000000000000ffffb01c30ab208d71fe8754010000000000000000000000000000000000ffffae5f68c2208de0acdf53010000000000000000000000000000000000ffffae737936208d29e18754010000000000000000000000000000000000ffffb00a63cb208d220b8854010000000000000000000000000000000000ffff9ec4d12f208db4408854010000000000000000000000000000000000ffff4c6d9bcc208d65a58754010000000000000000000000000000000000ffff5962c453208d2d628854010000000000000000000000000000000000ffff257191d50000" + }, + "PING": { + "message": "f9beb4d9706f6e67000000000000000008000000c6466f1e6b86480ae969867c", + "payload": "6b86480ae969867c" + } +} diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..54742fd --- /dev/null +++ b/test/index.html @@ -0,0 +1,18 @@ + + + + Mocha + + + + + +
+ + + + + + diff --git a/test/messages.js b/test/messages.js new file mode 100644 index 0000000..763865c --- /dev/null +++ b/test/messages.js @@ -0,0 +1,354 @@ +'use strict'; + +var chai = require('chai'); + +var should = chai.should(); + +var bitcore = require('bitcore-base'); +var Data = require('./data/messages'); +var P2P = require('../'); +var Messages = P2P.Messages; +var Networks = bitcore.Networks; + +describe('Messages', function() { + + describe('Version', function() { + it('should be able to create instance', function() { + var message = new Messages.Version(); + message.command.should.equal('version'); + message.version.should.equal(70000); + message.subversion.should.equal('/BitcoinX:0.1/'); + should.exist(message.nonce); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Version(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Version(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.VERSION.payload, 'hex'); + new Messages.Version().fromBuffer(payload); + }); + }); + + describe('VerAck', function() { + it('should be able to create instance', function() { + var message = new Messages.VerAck(); + message.command.should.equal('verack'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.VerAck(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.VerAck(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.VERACK.payload, 'hex'); + new Messages.VerAck().fromBuffer(payload); + }); + }); + + describe('Inventory', function() { + it('should be able to create instance', function() { + var message = new Messages.Inventory(); + message.command.should.equal('inv'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Inventory(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Inventory(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.INV.payload, 'hex'); + new Messages.Inventory().fromBuffer(payload); + }); + }); + + describe('Addresses', function() { + it('should be able to create instance', function() { + var message = new Messages.Addresses(); + message.command.should.equal('addr'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Addresses(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Addresses(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.ADDR.payload, 'hex'); + new Messages.Addresses().fromBuffer(payload); + }); + }); + + describe('Ping', function() { + it('should be able to create instance', function() { + var message = new Messages.Ping(); + message.command.should.equal('ping'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Ping(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Ping(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.PING.payload, 'hex'); + new Messages.Ping().fromBuffer(payload); + }); + }); + + describe('Pong', function() { + it('should be able to create instance', function() { + var message = new Messages.Pong(); + message.command.should.equal('pong'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Pong(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Pong(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + + it('should be able to parse payload', function() { + var payload = new Buffer(Data.PING.payload, 'hex'); + new Messages.Pong().fromBuffer(payload); + }); + }); + + describe('Alert', function() { + it('should be able to create instance', function() { + var message = new Messages.Alert(); + message.command.should.equal('alert'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Alert(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Alert(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('Reject', function() { + it('should be able to create instance', function() { + var message = new Messages.Reject(); + message.command.should.equal('reject'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Reject(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Reject(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('Block', function() { + var blockHex = 'f9beb4d91d0100000100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; + var block = new bitcore.Block(new Buffer(blockHex, 'hex')); + + it('should be able to create instance', function() { + var message = new Messages.Block(block); + message.command.should.equal('block'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Block(block); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Block(block); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('GetBlocks', function() { + it('should be able to create instance', function() { + var message = new Messages.GetBlocks(); + message.command.should.equal('getblocks'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.GetBlocks(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.GetBlocks(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('GetHeaders', function() { + it('should be able to create instance', function() { + var message = new Messages.GetHeaders(); + message.command.should.equal('getheaders'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.GetHeaders(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.GetHeaders(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('GetData', function() { + it('should be able to create instance', function() { + var message = new Messages.GetData(); + message.command.should.equal('getdata'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.GetData(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.GetData(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('GetData', function() { + it('should be able to create instance', function() { + var message = new Messages.GetData(); + message.command.should.equal('getdata'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.GetData(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.GetData(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('GetAddresses', function() { + it('should be able to create instance', function() { + var message = new Messages.GetAddresses(); + message.command.should.equal('getaddr'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.GetAddresses(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.GetAddresses(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('Headers', function() { + it('should be able to create instance', function() { + var message = new Messages.Headers(); + message.command.should.equal('headers'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Headers(); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Headers(); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); + + describe('Transaction', function() { + it('should be able to create instance', function() { + var message = new Messages.Transaction(new bitcore.Transaction()); + message.command.should.equal('tx'); + }); + + it('should be able to serialize the payload', function() { + var message = new Messages.Transaction(new bitcore.Transaction()); + var payload = message.getPayload(); + should.exist(payload); + }); + + it('should be able to serialize the message', function() { + var message = new Messages.Transaction(new bitcore.Transaction()); + var buffer = message.serialize(Networks.livenet); + should.exist(buffer); + }); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..4a52320 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--recursive diff --git a/test/peer.js b/test/peer.js new file mode 100644 index 0000000..60d0da0 --- /dev/null +++ b/test/peer.js @@ -0,0 +1,76 @@ +'use strict'; + +var chai = require('chai'); +var Net = require('net'); +var Socks5Client = require('socks5-client'); + +/* jshint unused: false */ +var should = chai.should(); +var expect = chai.expect; + +var bitcore = require('bitcore-base'); +var P2P = require('../'); +var Peer = P2P.Peer; +var Networks = bitcore.Networks; + +describe('Peer', function() { + + it('should be able to create instance', function() { + var peer = new Peer('localhost'); + peer.host.should.equal('localhost'); + peer.network.should.equal(Networks.livenet); + peer.port.should.equal(Networks.livenet.port); + }); + + it('should be able to create instance setting a port', function() { + var peer = new Peer('localhost', 8111); + peer.host.should.equal('localhost'); + peer.network.should.equal(Networks.livenet); + peer.port.should.equal(8111); + }); + + it('should be able to create instance setting a network', function() { + var peer = new Peer('localhost', Networks.testnet); + peer.host.should.equal('localhost'); + peer.network.should.equal(Networks.testnet); + peer.port.should.equal(Networks.testnet.port); + }); + + it('should be able to create instance setting port and network', function() { + var peer = new Peer('localhost', 8111, Networks.testnet); + peer.host.should.equal('localhost'); + peer.network.should.equal(Networks.testnet); + peer.port.should.equal(8111); + }); + + it('should support creating instance without new', function() { + var peer = Peer('localhost', 8111, Networks.testnet); + peer.host.should.equal('localhost'); + peer.network.should.equal(Networks.testnet); + peer.port.should.equal(8111); + }); + + if (typeof(window) === 'undefined'){ + + // Node.js Tests + + it('should be able to set a proxy', function() { + var peer, peer2, socket; + + peer = new Peer('localhost'); + expect(peer.proxy).to.be.undefined(); + socket = peer._getSocket(); + socket.should.be.instanceof(Net.Socket); + + peer2 = peer.setProxy('127.0.0.1', 9050); + peer2.proxy.host.should.equal('127.0.0.1'); + peer2.proxy.port.should.equal(9050); + socket = peer2._getSocket(); + socket.should.be.instanceof(Socks5Client); + + peer.should.equal(peer2); + }); + + } + +}); diff --git a/test/pool.js b/test/pool.js new file mode 100644 index 0000000..1122c7b --- /dev/null +++ b/test/pool.js @@ -0,0 +1,91 @@ +'use strict'; + +var chai = require('chai'); + +/* jshint unused: false */ +var should = chai.should(); +var expect = chai.expect; + +var dns = require('dns'); +var sinon = require('sinon'); + +var bitcore = require('bitcore-base'); +var P2P = require('../'); +var Peer = P2P.Peer; +var MessagesData = require('./data/messages'); +var Messages = P2P.Messages; +var Pool = P2P.Pool; +var Networks = bitcore.Networks; + +describe('Pool', function() { + + it('should be able to create instance', function() { + var pool = new Pool(); + pool.network.should.equal(Networks.livenet); + }); + + it('should be able to create instance setting the network', function() { + var pool = new Peer(Networks.testnet); + pool.network.should.equal(Networks.livenet); + }); + + it('should discover peers via dns', function() { + var stub = sinon.stub(dns, 'resolve', function(seed, callback){ + callback(null, ['10.10.10.1', '10.10.10.2', '10.10.10.3']); + }); + var pool = new Pool(Networks.livenet); + pool.connect(); + pool.disconnect(); + pool._addrs.length.should.equal(3); + stub.restore(); + }); + + it('should not discover peers via dns', function() { + var pool = new Pool(); + pool._addAddr({ip: {v4: '10.10.10.1'}}); + pool.connect(); + pool.disconnect(); + pool._addrs.length.should.equal(1); + }); + + it('should add new addrs as they are announced over the network', function(done) { + + // only emit an event, no need to connect + var peerConnectStub = sinon.stub(Peer.prototype, 'connect', function(){ + this._readMessage(); + this.emit('ready'); + }); + + // mock a addr peer event + var peerMessageStub = sinon.stub(Peer.prototype, '_readMessage', function(){ + var payload = new Buffer(MessagesData.ADDR.payload, 'hex'); + var message = new Messages.Addresses().fromBuffer(payload); + this.emit(message.command, message); + }); + + var pool = new Pool(); + + pool._addAddr({ip: {v4: 'localhost'}}); + + // listen for the event + pool.on('peeraddr', function(peer, message) { + pool._addrs.length.should.equal(502); + + // restore stubs + peerConnectStub.restore(); + peerMessageStub.restore(); + + for (var i = 0; i < pool._addrs.length; i++) { + should.exist(pool._addrs[i].hash); + should.exist(pool._addrs[i].ip); + should.exist(pool._addrs[i].ip.v4); + } + + // done + done(); + }); + + pool.connect(); + + }); +});