commit 40225584fd3bc6ba1ab3e472613c66da108952af Author: Esteban Ordano Date: Wed Dec 24 13:30:55 2014 -0300 Initial commit for bitcore-p2p 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(); + + }); +});