diff --git a/.jshintrc b/.jshintrc index 99ddc133a3e..c51273a7cf4 100644 --- a/.jshintrc +++ b/.jshintrc @@ -8,7 +8,6 @@ "quotmark": "single", "strict" : true, "globals": { - "gc": true, - "RequestSanitizer": true + "gc": true } } diff --git a/README.md b/README.md index 925f44f710f..d2a4a68422c 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Install PostgreSQL (version 9.5.2): ``` curl -sL "https://downloads.lisk.io/scripts/setup_postgresql.Linux" | bash - -sudo -u postgres createuser --createdb --password $USER +sudo -u postgres createuser --createdb $USER createdb lisk_test +sudo -u postgres psql -d lisk_test -c "alter user "$USER" with password 'password';" ``` Install Node.js (version 0.12.x) + npm: diff --git a/app.js b/app.js index ff62721a283..31fdb24f2cb 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,5 @@ 'use strict'; -var appConfig = require('./config.json'); var async = require('async'); var checkIpInList = require('./helpers/checkIpInList.js'); var extend = require('extend'); @@ -35,9 +34,7 @@ program .option('-s, --snapshot ', 'verify snapshot') .parse(process.argv); -if (program.config) { - appConfig = require(path.resolve(process.cwd(), program.config)); -} +var appConfig = require('./helpers/config.js')(program.config); if (program.port) { appConfig.port = program.port; @@ -71,6 +68,9 @@ if (program.snapshot) { ); } +// Define top endpoint availability +process.env.TOP = appConfig.topAccounts; + var config = { db: appConfig.db, modules: { @@ -163,7 +163,7 @@ d.run(function () { require('./helpers/request-limiter')(app, appConfig); - app.use(compression({ level: 6 })); + app.use(compression({ level: 9 })); app.use(cors()); app.options('*', cors()); @@ -226,7 +226,6 @@ d.run(function () { var path = require('path'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); - var requestSanitizer = require('./helpers/request-sanitizer'); var queryParser = require('express-query-int'); scope.network.app.engine('html', require('ejs').renderFile); diff --git a/config.json b/config.json index e3d8dfef2aa..44eb8114c1a 100644 --- a/config.json +++ b/config.json @@ -2,17 +2,19 @@ "port": 8000, "address": "0.0.0.0", "version": "0.4.1", + "minVersion": "~0.4.0", "fileLogLevel": "info", "logFileName": "logs/lisk.log", "consoleLogLevel": "info", "trustProxy": false, + "topAccounts": false, "db": { "host": "localhost", "port": 5432, "database": "lisk_main", - "user": null, + "user": "", "password": "password", - "poolSize": 20, + "poolSize": 95, "poolIdleTimeout": 30000, "reapIntervalMillis": 1000, "logEvents": [ @@ -63,10 +65,16 @@ "delayAfter": 0, "windowMs": 60000 }, - "maxUpdatePeers": 20, "timeout": 5000 } }, + "broadcasts": { + "broadcastInterval": 5000, + "broadcastLimit": 20, + "parallelLimit": 20, + "releaseLimit": 25, + "relayLimit": 2 + }, "forging": { "force": false, "secret": [], diff --git a/helpers/config.js b/helpers/config.js new file mode 100644 index 00000000000..4b00ef548ec --- /dev/null +++ b/helpers/config.js @@ -0,0 +1,43 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var z_schema = require('./z_schema.js'); +var configSchema = require('../schema/config.js'); +var constants = require('../helpers/constants.js'); + +function Config (configPath) { + var configData = fs.readFileSync(path.resolve(process.cwd(), (configPath || 'config.json')), 'utf8'); + + if (!configData.length) { + console.log('Failed to read config file'); + process.exit(1); + } else { + configData = JSON.parse(configData); + } + + var validator = new z_schema(); + var valid = validator.validate(configData, configSchema.config); + + if (!valid) { + console.log('Failed to validate config data', validator.getLastErrors()); + process.exit(1); + } else { + validateForce(configData); + return configData; + } +} + +function validateForce (configData) { + if (configData.forging.force) { + var index = constants.nethashes.indexOf(configData.nethash); + + if (index !== -1) { + console.log('Forced forging disabled for nethash', configData.nethash); + configData.forging.force = false; + } + } +} + +// Exports +module.exports = Config; diff --git a/helpers/constants.js b/helpers/constants.js index 751f216538a..a709b348fc3 100644 --- a/helpers/constants.js +++ b/helpers/constants.js @@ -4,9 +4,10 @@ module.exports = { activeDelegates: 101, addressLength: 208, blockHeaderLength: 248, + blockReceiptTimeOut: 120, // 12 blocks confirmationLength: 77, epochTime: new Date(Date.UTC(2016, 4, 24, 17, 0, 0, 0)), - fees:{ + fees: { send: 10000000, vote: 100000000, secondsignature: 500000000, @@ -16,16 +17,24 @@ module.exports = { }, feeStart: 1, feeStartVolume: 10000 * 100000000, - fixedPoint : Math.pow(10, 8), - forgingTimeOut: 500, // 50 blocks + fixedPoint: Math.pow(10, 8), maxAddressesLength: 208 * 128, maxAmount: 100000000, - maxClientConnections: 100, - maxConfirmations : 77 * 100, + maxConfirmations: 77 * 100, maxPayloadLength: 1024 * 1024, + maxPeers: 100, maxRequests: 10000 * 12, + maxSharedTxs: 100, maxSignaturesLength: 196 * 256, maxTxsPerBlock: 25, + maxTxsPerQueue: 5000, + minBroadhashConsensus: 51, + nethashes: [ + // Mainnet + 'ed14889723f24ecc54871d058d98ce91ff2f973192075c0155ba2b7b70ad2511', + // Testnet + 'da3ed6a45429278bac2666961289ca17ad86595d33b31037615d4b8e8f158bba' + ], numberLength: 100000000, requestLength: 104, rewards: { diff --git a/helpers/exceptions.js b/helpers/exceptions.js index 1edec554b22..f598b30414b 100644 --- a/helpers/exceptions.js +++ b/helpers/exceptions.js @@ -24,6 +24,7 @@ module.exports = { '5384302058030309746', // 869890 '9352922026980330230', // 925165 ], + multisignatures: [], votes: [ '5524930565698900323', // 20407 '11613486949732674475', // 123300 diff --git a/helpers/request-sanitizer.js b/helpers/request-sanitizer.js deleted file mode 100644 index dc84ec7a1ba..00000000000 --- a/helpers/request-sanitizer.js +++ /dev/null @@ -1,410 +0,0 @@ -'use strict'; - -var extend = require('extend'); -var inherits = require('util').inherits; -var Validator = require('./validator/validator.js'); - -module.exports = RequestSanitizer; - -function RequestSanitizer(options) { - Validator.call(this, options); -} - -inherits(RequestSanitizer, Validator); - -RequestSanitizer.prototype.rules = {}; -extend(RequestSanitizer, Validator); - -RequestSanitizer.options = extend({ - reporter : SanitizeReporter -}, Validator.options); - -RequestSanitizer.addRule('empty', { - validate : function (accept, value, field) { - if (accept !== false) { return; } - - return !field.isEmpty(); - } -}); - -RequestSanitizer.addRule('string', { - filter : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - return String(value||''); - } -}); - -RequestSanitizer.addRule('regexp', { - message : 'value should match template', - validate : function (accept, value) { - if (typeof value !== 'string') { return false; } - - return accept.test(value); - } -}); - -RequestSanitizer.addRule('boolean', { - filter : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - switch(String(value).toLowerCase()) { - case 'false': - case 'f': - return false; - default: - return !!value; - } - } -}); - -RequestSanitizer.addRule('int', { - filter : function (accept, value , field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - if (isNaN(value) || parseInt(value) !== value || isNaN(parseInt(value, 10))) { - return 0; - } - - return parseInt(value); - } -}); - -RequestSanitizer.addRule('float', { - filter : function (accept, value , field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - value = parseFloat(value); - - return isNaN(value) ? 0 : value; - } -}); - -RequestSanitizer.addRule('object', { - filter : function (accept, value , field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - return Object.prototype.toString.call(value) === '[object Object]' ? value : {}; - } -}); - -RequestSanitizer.addRule('array', { - filter: function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - if (typeof value === 'string' && (typeof accept === 'string' || accept instanceof RegExp )) { - return value.length ? value.split(accept) : []; - } else if (Array.isArray(value)) { - return value; - } else { - return []; - } - } -}); - -RequestSanitizer.addRule('arrayOf', { - validate : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - if (! Array.isArray(value)) { return false; } - - var l = value.length; - var i = -1; - var child; - - while (++i < l) { - field.child(i, value[i], accept, value).validate(); - } - } -}); - -RequestSanitizer.addRule('hex', { - filter : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - return value; - }, - validate : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return; } - - return /^([A-Fa-f0-9]{2})*$/.test(String(value||'')); - } -}); - -RequestSanitizer.addRule('buffer', { - filter : function (accept, value) { - if (typeof accept !== 'string') { - accept = 'utf8'; - } - - try { - return new Buffer(value||'', accept); - } catch (err) { - return new Buffer(); - } - } -}); - -RequestSanitizer.addRule('variant', { - filter : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - return typeof value === 'undefined' ? '' : value; - } -}); - -RequestSanitizer.addRule('required', { - message : 'value is required', - validate : function (accept, value) { - return (typeof value !== 'undefined') === accept; - } -}); - -RequestSanitizer.addRule('default', { - filter : function (accept, value) { - return (typeof value === 'undefined') ? accept : value; - } -}); - -RequestSanitizer.addRule('properties', { - validate : function (accept, value, field) { - if (! field.isObject()) { return false; } - - Object.getOwnPropertyNames(accept).forEach(function (name) { - var childAccept = accept[name]; - if (typeof childAccept === 'string') { - childAccept = convertStringRule(childAccept); - } - var child = field.child(name, value[name], childAccept, value); - child.validate(function (err, report, output) { - if (err) { throw err; } - - value[name] = output; - }); - }); - } -}); - -RequestSanitizer.addRule('minLength', { - message : 'minimum length is ${accept}.', - validate : function (accept, value) { - return value.length >= accept; - } -}); - -RequestSanitizer.addRule('case', { - message : 'case is ${accept}.', - validate : function (accept, value) { - return typeof value === 'string' && ((accept==='lower' && value===value.toLowerCase())||(accept==='upper' && value===value.toUpperCase())); - }, - filter : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return null; } - - if(accept==='lower') { - return String(value||'').toLowerCase(); - } - else if(accept==='upper') { - return String(value||'').toUpperCase(); - } - } -}); - -RequestSanitizer.addRule('maxLength', { - message : 'maximum length is ${accept}.', - validate : function (accept, value) { - return value.length <= accept; - } -}); - -RequestSanitizer.addRule('maxByteLength', { - message : 'maximum size is ${accept.length} bytes', - accept : function (accept) { - if (typeof accept !== 'object') { - accept = { - encoding : 'utf8', - length : accept - }; - } - return accept; - }, - validate : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return; } - - if (typeof value === 'object' && value !== null) { - value = JSON.stringify(value); - } - - - return Buffer.byteLength(value, 'utf8') <= accept.length; - } -}); - -RequestSanitizer.addRule('minByteLength', { - message : 'minimum size is ${accept.length} bytes', - accept : function (accept) { - if (typeof accept !== 'object') { - accept = { - encoding : 'utf8', - length : accept - }; - } - - return accept; - }, - validate : function (accept, value, field) { - if (field.isEmpty() && field.rules.empty) { return; } - - if (typeof value === 'object' && value !== null) { - value = JSON.stringify(value); - } - return Buffer.byteLength(value, 'utf8') >= accept.length; - } -}); - -/** - * Express middleware factory - * @param {Object} options Validator constructor options - * @returns {Function} Express middleware - */ -RequestSanitizer.express = function (options) { - options = extend({}, RequestSanitizer.options, options); - - - return function (req, res, next) { - req.sanitize = sanitize; - - function sanitize(value, properties, callback) { - var values = {}; - if (typeof value === 'string') { - value = req[value] || {}; - } - - Object.getOwnPropertyNames(properties).forEach(function (name) { - values[name] = value.hasOwnProperty(name) ? value[name] : undefined; - if (typeof properties[name] === 'string') { - properties[name] = convertStringRule(properties[name]); - } - }); - - return (new RequestSanitizer(options)).validate(values, {properties:properties}, callback); - } - - next(); - }; -}; - -// Define filter rules as standalone methods -var rules = RequestSanitizer.prototype.rules; -[ - 'string', - 'boolean', - 'int', - 'float', - 'variant', - 'array', - 'object', - 'hex', - 'buffer' -].forEach(function (name) { - var rule = rules[name]; - if (typeof rule.filter !== 'function') { return; } - if (name in RequestSanitizer) { return; } - - RequestSanitizer[name] = function filter(value, extra) { - var rules = {}; - if (typeof extra === 'object') { - extend(rules, extra); - } else if (typeof extra !== 'undefined') { - rules.empty = extra; - } - - rules[name] = true; - - var report = (new RequestSanitizer(RequestSanitizer.options)).validate(value, rules); - if (! report.isValid) { - var error = new Error(report.issues); - error.name = 'ValidationError'; - error.issues = report.issues; - throw error; - } - - return report.value; - }; -}); - -RequestSanitizer.options.reporter = SanitizeReporter; - -function SanitizeReporter(validator) { - this.validator = validator; -} - -SanitizeReporter.prototype.format = function (message, values) { - return String(message).replace(/\$\{([^}]+)}/g, function (match, id) { - return getByPath(values, id.split('.')) || ''; - }); -}; - -SanitizeReporter.prototype.convert = function (issues) { - var self = this; - - var grouped = issues.reduce(function (result, item) { - var path = item.path.join('.'); - if (path in result === false) { result[path] = []; } - - result[path].push(item); - - return result; - }, {}); - - var result = ''; - - Object.getOwnPropertyNames(grouped).forEach(function (path) { - result += 'Property \'' + path + '\':\n'; - - grouped[path].forEach(function (item) { - var rule = self.validator.getRule(item.rule); - - result += '\t- '; - - if (rule.hasOwnProperty('message')) { - result += self.format(rule.message, item) + '\n'; - } else { - result += 'break rule \'' + item.rule + '\'\n'; - } - }); - }); - - return result; -}; - -function getByPath (target, path) { - var segment; - path = path.slice(); - var i = -1; - var l = path.length - 1; - - while (++i < l) { - segment = path[i]; - if (typeof target[segment] !== 'object') { - return null; - } - - target = target[segment]; - } - - return target[path[l]]; -} - -function convertStringRule (rule) { - var result = {}; - - if (rule.charAt(rule.length-1) === '!') { - result.required = true; - rule = rule.slice(0, -1); - } else if (rule.charAt(rule.length-1) === '?') { - result.empty = true; - rule = rule.slice(0, -1); - } - - result[rule] = true; - return result; -} diff --git a/helpers/z_schema.js b/helpers/z_schema.js index 502bc6f8579..2b3a409dc8d 100644 --- a/helpers/z_schema.js +++ b/helpers/z_schema.js @@ -3,6 +3,30 @@ var ip = require('ip'); var z_schema = require('z-schema'); +z_schema.registerFormat('id', function (str) { + if (str.length === 0) { + return true; + } + + return /^[0-9]+$/g.test(str); +}); + +z_schema.registerFormat('address', function (str) { + if (str.length === 0) { + return true; + } + + return /^[0-9]+[L]$/ig.test(str); +}); + +z_schema.registerFormat('username', function (str) { + if (str.length === 0) { + return true; + } + + return /^[a-z0-9!@$&_.]+$/ig.test(str); +}); + z_schema.registerFormat('hex', function (str) { try { new Buffer(str, 'hex'); @@ -77,6 +101,22 @@ z_schema.registerFormat('ip', function (str) { return ip.isV4Format(str); }); +z_schema.registerFormat('os', function (str) { + if (str.length === 0) { + return true; + } + + return /^[a-z0-9-_.+]+$/ig.test(str); +}); + +z_schema.registerFormat('version', function (str) { + if (str.length === 0) { + return true; + } + + return /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})([a-z]{1})?$/g.test(str); +}); + // var registeredFormats = z_schema.getRegisteredFormats(); // console.log(registeredFormats); diff --git a/logic/block.js b/logic/block.js index 36e611e267d..af17d212966 100644 --- a/logic/block.js +++ b/logic/block.js @@ -231,7 +231,10 @@ Block.prototype.schema = { type: 'object', properties: { id: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, height: { type: 'integer' @@ -255,7 +258,10 @@ Block.prototype.schema = { type: 'integer' }, previousBlock: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, timestamp: { type: 'integer' diff --git a/logic/broadcaster.js b/logic/broadcaster.js new file mode 100644 index 00000000000..1fb230f8251 --- /dev/null +++ b/logic/broadcaster.js @@ -0,0 +1,235 @@ +'use strict'; + +var async = require('async'); +var constants = require('../helpers/constants.js'); +var extend = require('extend'); +var _ = require('lodash'); + +// Private fields +var modules, library, self, __private = {}; + +// Constructor +function Broadcaster (scope) { + library = scope; + self = this; + + self.queue = []; + self.config = library.config.broadcasts; + self.config.peerLimit = constants.maxPeers; + + // Optionally ignore broadhash consensus + if (library.config.forging.force) { + self.consensus = undefined; + } else { + self.consensus = 100; + } + + // Broadcast routes + self.routes = [{ + path: '/transactions', + collection: 'transactions', + object: 'transaction', + method: 'POST' + }, { + path: '/signatures', + collection: 'signatures', + object: 'signature', + method: 'POST' + }]; + + // Broadcaster timer + setImmediate(function nextRelease () { + async.series([ + __private.releaseQueue + ], function (err) { + if (err) { + library.logger.log('Broadcaster timer', err); + } + + return setTimeout(nextRelease, self.config.broadcastInterval); + }); + }); +} + +// Public methods +Broadcaster.prototype.bind = function (scope) { + modules = scope; +}; + +Broadcaster.prototype.getPeers = function (params, cb) { + params.limit = params.limit || self.config.peerLimit; + params.broadhash = params.broadhash || null; + + var originalLimit = params.limit; + + modules.peers.list(params, function (err, peers, consensus) { + if (err) { + return setImmediate(cb, err); + } + + if (self.consensus !== undefined && originalLimit === constants.maxPeers) { + library.logger.info(['Broadhash consensus now', consensus, '%'].join(' ')); + self.consensus = consensus; + } + + return setImmediate(cb, null, peers); + }); +}; + +Broadcaster.prototype.enqueue = function (params, options) { + options.immediate = false; + return self.queue.push({params: params, options: options}); +}; + +Broadcaster.prototype.broadcast = function (params, options, cb) { + params.limit = params.limit || self.config.peerLimit; + params.broadhash = params.broadhash || null; + + async.waterfall([ + function getPeers (waterCb) { + if (!params.peers) { + return self.getPeers(params, waterCb); + } else { + return setImmediate(waterCb, null, params.peers); + } + }, + function getFromPeer (peers, waterCb) { + library.logger.debug('Begin broadcast', options); + + if (params.limit === self.config.peerLimit) { peers.splice(0, self.config.broadcastLimit); } + + async.eachLimit(peers, self.config.parallelLimit, function (peer, eachLimitCb) { + peer = modules.peers.accept(peer); + + modules.transport.getFromPeer(peer, options, function (err) { + if (err) { + library.logger.debug('Failed to broadcast to peer: ' + peer.string, err); + } + return setImmediate(eachLimitCb); + }); + }, function (err) { + library.logger.debug('End broadcast'); + return setImmediate(waterCb, err, peers); + }); + } + ], function (err, peers) { + if (cb) { + return setImmediate(cb, err, {body: null, peer: peers}); + } + }); +}; + +Broadcaster.prototype.maxRelays = function (object) { + if (!Number.isInteger(object.relays)) { + object.relays = 0; // First broadcast + } + + if (Math.abs(object.relays) >= self.config.relayLimit) { + library.logger.debug('Broadcast relays exhausted', object); + return true; + } else { + object.relays++; // Next broadcast + return false; + } +}; + +// Private +__private.filterQueue = function (cb) { + library.logger.debug('Broadcasts before filtering: ' + self.queue.length); + + async.filter(self.queue, function (broadcast, filterCb) { + if (broadcast.options.immediate) { + return setImmediate(filterCb, null, false); + } else if (broadcast.options.data) { + var transaction = (broadcast.options.data.transaction || broadcast.options.data.signature); + return __private.filterTransaction(transaction, filterCb); + } else { + return setImmediate(filterCb, null, true); + } + }, function (err, broadcasts) { + self.queue = broadcasts; + + library.logger.debug('Broadcasts after filtering: ' + self.queue.length); + return setImmediate(cb); + }); +}; + +__private.filterTransaction = function (transaction, cb) { + if (transaction !== undefined) { + if (modules.transactions.transactionInPool(transaction.id)) { + return setImmediate(cb, null, true); + } else { + return library.logic.transaction.checkConfirmed(transaction, function (err) { + return setImmediate(cb, null, !err); + }); + } + } else { + return setImmediate(cb, null, false); + } +}; + +__private.squashQueue = function (broadcasts) { + var grouped = _.groupBy(broadcasts, function (broadcast) { + return broadcast.options.api; + }); + + var squashed = []; + + self.routes.forEach(function (route) { + if (Array.isArray(grouped[route.path])) { + var data = {}; + + data[route.collection] = grouped[route.path].map(function (broadcast) { + return broadcast.options.data[route.object]; + }).filter(Boolean); + + squashed.push({ + options: { api: route.path, data: data, method: route.method }, + immediate: false + }); + } + }); + + return squashed; +}; + +__private.releaseQueue = function (cb) { + library.logger.debug('Releasing enqueued broadcasts'); + + if (!self.queue.length) { + library.logger.debug('Queue empty'); + return setImmediate(cb); + } + + async.waterfall([ + function filterQueue (waterCb) { + return __private.filterQueue(waterCb); + }, + function squashQueue (waterCb) { + var broadcasts = self.queue.splice(0, self.config.releaseLimit); + return setImmediate(waterCb, null, __private.squashQueue(broadcasts)); + }, + function getPeers (broadcasts, waterCb) { + self.getPeers({}, function (err, peers) { + return setImmediate(waterCb, err, broadcasts, peers); + }); + }, + function broadcast (broadcasts, peers, waterCb) { + async.eachSeries(broadcasts, function (broadcast, eachSeriesCb) { + self.broadcast(extend({peers: peers}, broadcast.params), broadcast.options, eachSeriesCb); + }, function (err) { + return setImmediate(waterCb, err, broadcasts); + }); + } + ], function (err, broadcasts) { + if (err) { + library.logger.debug('Failed to release broadcast queue', err); + } else { + library.logger.debug('Broadcasts released: ' + broadcasts.length); + } + return setImmediate(cb); + }); +}; + +// Export +module.exports = Broadcaster; diff --git a/logic/dapp.js b/logic/dapp.js index 482cef18e86..46b41238489 100644 --- a/logic/dapp.js +++ b/logic/dapp.js @@ -131,7 +131,28 @@ DApp.prototype.verify = function (trs, sender, cb) { } } - return setImmediate(cb); + library.db.query(sql.getExisting, { + name: trs.asset.dapp.name, + link: trs.asset.dapp.link || null, + transactionId: trs.id + }).then(function (rows) { + var dapp = rows[0]; + + if (dapp) { + if (dapp.name === trs.asset.dapp.name) { + return setImmediate(cb, 'Application name already exists: ' + dapp.name); + } else if (dapp.link === trs.asset.dapp.link) { + return setImmediate(cb, 'Application link already exists: ' + dapp.link); + } else { + return setImmediate(cb, 'Application already exists'); + } + } else { + return setImmediate(cb, null, trs); + } + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'DApp#verify error'); + }); }; DApp.prototype.process = function (trs, sender, cb) { @@ -197,27 +218,7 @@ DApp.prototype.applyUnconfirmed = function (trs, sender, cb) { __private.unconfirmedNames[trs.asset.dapp.name] = true; __private.unconfirmedLinks[trs.asset.dapp.link] = true; - library.db.query(sql.getExisting, { - name: trs.asset.dapp.name, - link: trs.asset.dapp.link || null, - transactionId: trs.id - }).then(function (rows) { - var dapp = rows[0]; - - if (dapp) { - if (dapp.name === trs.asset.dapp.name) { - return setImmediate(cb, 'Application name already exists: ' + dapp.name); - } else if (dapp.link === trs.asset.dapp.link) { - return setImmediate(cb, 'Application link already exists: ' + dapp.link); - } else { - return setImmediate(cb, 'Unknown error'); - } - } else { - return setImmediate(cb, null, trs); - } - }).catch(function (err) { - return setImmediate(cb, 'DApp#applyUnconfirmed error'); - }); + return setImmediate(cb); }; DApp.prototype.undoUnconfirmed = function (trs, sender, cb) { @@ -336,7 +337,9 @@ DApp.prototype.dbSave = function (trs) { }; DApp.prototype.afterSave = function (trs, cb) { - library.network.io.sockets.emit('dapps/change', {}); + if (library) { + library.network.io.sockets.emit('dapps/change', {}); + } return setImmediate(cb); }; diff --git a/logic/delegate.js b/logic/delegate.js index 1bb001037ef..c37674b1ab6 100644 --- a/logic/delegate.js +++ b/logic/delegate.js @@ -147,38 +147,18 @@ Delegate.prototype.undo = function (trs, block, sender, cb) { }; Delegate.prototype.applyUnconfirmed = function (trs, sender, cb) { - if (sender.u_isDelegate) { - return setImmediate(cb, 'Account is already a delegate'); - } - - function done () { - var data = { - address: sender.address, - u_isDelegate: 1, - isDelegate: 0 - }; - - if (trs.asset.delegate.username) { - data.username = null; - data.u_username = trs.asset.delegate.username; - } + var data = { + address: sender.address, + u_isDelegate: 1, + isDelegate: 0 + }; - modules.accounts.setAccountAndGet(data, cb); + if (trs.asset.delegate.username) { + data.username = null; + data.u_username = trs.asset.delegate.username; } - modules.accounts.getAccount({ - u_username: trs.asset.delegate.username - }, function (err, account) { - if (err) { - return setImmediate(cb, err); - } - - if (account) { - return setImmediate(cb, 'Username already exists'); - } - - done(); - }); + modules.accounts.setAccountAndGet(data, cb); }; Delegate.prototype.undoUnconfirmed = function (trs, sender, cb) { diff --git a/logic/inTransfer.js b/logic/inTransfer.js index e7229fa73f1..2977cdcde4f 100644 --- a/logic/inTransfer.js +++ b/logic/inTransfer.js @@ -76,7 +76,7 @@ InTransfer.prototype.getBytes = function (trs) { }; InTransfer.prototype.apply = function (trs, block, sender, cb) { - shared.getGenesis({id: trs.asset.inTransfer.dappId}, function (err, res) { + shared.getGenesis({dappid: trs.asset.inTransfer.dappId}, function (err, res) { if (err) { return setImmediate(cb, err); } @@ -93,7 +93,7 @@ InTransfer.prototype.apply = function (trs, block, sender, cb) { }; InTransfer.prototype.undo = function (trs, block, sender, cb) { - shared.getGenesis({id: trs.asset.inTransfer.dappId}, function (err, res) { + shared.getGenesis({dappid: trs.asset.inTransfer.dappId}, function (err, res) { if (err) { return setImmediate(cb, err); } @@ -123,7 +123,9 @@ InTransfer.prototype.schema = { properties: { dappId: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, }, required: ['dappId'] diff --git a/logic/multisignature.js b/logic/multisignature.js index e1edceb6b50..691d3b47342 100644 --- a/logic/multisignature.js +++ b/logic/multisignature.js @@ -4,6 +4,7 @@ var async = require('async'); var ByteBuffer = require('bytebuffer'); var constants = require('../helpers/constants.js'); var Diff = require('../helpers/diff.js'); +var exceptions = require('../helpers/exceptions.js'); // Private fields var modules, library, __private = {}; @@ -52,14 +53,25 @@ Multisignature.prototype.verify = function (trs, sender, cb) { return setImmediate(cb, 'Invalid multisignature min. Must be between 1 and 16'); } - if (trs.asset.multisignature.min > trs.asset.multisignature.keysgroup.length + 1) { - return setImmediate(cb, 'Invalid multisignature min. Must be less than keysgroup size'); + if (trs.asset.multisignature.min > trs.asset.multisignature.keysgroup.length) { + var err = 'Invalid multisignature min. Must be less than keysgroup size'; + + if (exceptions.multisignatures.indexOf(trs.id) > -1) { + this.scope.logger.debug(err); + this.scope.logger.debug(JSON.stringify(trs)); + } else { + return setImmediate(cb, err); + } } if (trs.asset.multisignature.lifetime < 1 || trs.asset.multisignature.lifetime > 72) { return setImmediate(cb, 'Invalid multisignature lifetime. Must be between 1 and 72'); } + if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { + return setImmediate(cb, 'Account already has multisignatures enabled'); + } + if (this.ready(trs, sender)) { try { for (var s = 0; s < trs.asset.multisignature.keysgroup.length; s++) { @@ -195,10 +207,6 @@ Multisignature.prototype.applyUnconfirmed = function (trs, sender, cb) { return setImmediate(cb, 'Signature on this account is pending confirmation'); } - if (Array.isArray(sender.multisignatures) && sender.multisignatures.length) { - return setImmediate(cb, 'Account already has multisignatures enabled'); - } - __private.unconfirmedSignatures[sender.address] = true; this.scope.account.merge(sender.address, { @@ -301,7 +309,7 @@ Multisignature.prototype.dbSave = function (trs) { }; Multisignature.prototype.afterSave = function (trs, cb) { - library.network.io.sockets.emit('multisignatures/change', {}); + library.network.io.sockets.emit('multisignatures/change', trs); return setImmediate(cb); }; diff --git a/logic/outTransfer.js b/logic/outTransfer.js index 9cbd343695f..a768f9d7acc 100644 --- a/logic/outTransfer.js +++ b/logic/outTransfer.js @@ -155,11 +155,15 @@ OutTransfer.prototype.schema = { properties: { dappId: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, transactionId: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['dappId', 'transactionId'] diff --git a/logic/peer.js b/logic/peer.js new file mode 100644 index 00000000000..15761908919 --- /dev/null +++ b/logic/peer.js @@ -0,0 +1,114 @@ +'use strict'; + +var extend = require('extend'); +var ip = require('ip'); + +// Constructor +function Peer (peer) { + return this.accept(peer || {}); +} + +// Public properties +Peer.prototype.properties = [ + 'ip', + 'port', + 'state', + 'os', + 'version', + 'dappid', + 'broadhash', + 'height' +]; + +Peer.prototype.nullable = [ + 'os', + 'version', + 'dappid', + 'broadhash', + 'height' +]; + +// Public methods +Peer.prototype.accept = function (peer) { + if (/^[0-9]+$/.test(peer.ip)) { + this.ip = ip.fromLong(peer.ip); + } else { + this.ip = peer.ip; + } + + this.port = this.parseInt(peer.port, 0); + + if (this.ip) { + this.string = (this.ip + ':' + this.port || 'unknown'); + } else { + this.string = 'unknown'; + } + + if (peer.state != null) { + this.state = peer.state; + } else { + this.state = 1; + } + + if (peer.dappid != null) { + this.dappid = peer.dappid; + } + + this.headers(peer); + return this; +}; + +Peer.prototype.parseInt = function (integer, fallback) { + integer = parseInt(integer); + integer = isNaN(integer) ? fallback : integer; + + return integer; +}; + +Peer.prototype.headers = function (headers) { + headers = headers || {}; + + headers.os = headers.os || 'unknown'; + headers.version = headers.version || '0.0.0'; + headers.port = this.parseInt(headers.port, 0); + + if (headers.height != null) { + headers.height = this.parseInt(headers.height, 1); + } + + this.nullable.forEach(function (property) { + if (headers[property] != null) { + this[property] = headers[property]; + } else { + delete headers[property]; + } + }.bind(this)); + + return headers; +}; + +Peer.prototype.extend = function (object) { + var base = this.object(); + var extended = extend(this.object(), object); + + return this.headers(extended); +}; + +Peer.prototype.object = function () { + var object = {}; + + this.properties.forEach(function (property) { + object[property] = this[property]; + }.bind(this)); + + this.nullable.forEach(function (property) { + if (object[property] == null) { + object[property] = null; + } + }); + + return object; +}; + +// Export +module.exports = Peer; diff --git a/logic/peerSweeper.js b/logic/peerSweeper.js new file mode 100644 index 00000000000..bd35c18fa78 --- /dev/null +++ b/logic/peerSweeper.js @@ -0,0 +1,102 @@ +'use strict'; + +var pgp = require('pg-promise'); + +// Constructor +function PeerSweeper (scope) { + this.peers = []; + this.limit = 100; + this.scope = scope; + + var self = this; + + setImmediate(function nextSweep () { + if (self.peers.length) { + self.sweep(self.peers.splice(0, self.limit), function () { + return setTimeout(nextSweep, 1000); + }); + } else { + return setTimeout(nextSweep, 1000); + } + }); +} + +// Public methods +PeerSweeper.prototype.push = function (action, peer) { + if (action) { + peer.action = action; + } else { + throw 'Missing push action'; + } + if (peer.broadhash != null) { + peer.broadhash = new Buffer(peer.broadhash, 'hex'); + } + this.peers.push(peer); +}; + +PeerSweeper.prototype.sweep = function (peers, cb) { + var self = this; + + if (!peers.length) { return; } + + self.scope.library.db.tx(function (t) { + var queries = peers.map(function (peer) { + return pgp.as.format(self.scope.sql[peer.action], peer); + }); + + return t.query(queries.join(';')); + }).then(function () { + self.addDapps(peers); + self.scope.library.logger.debug(['Swept', peers.length, 'peer changes'].join(' ')); + + return setImmediate(cb); + }).catch(function (err) { + self.scope.library.logger.error('Failed to sweep peers', err.stack); + return setImmediate(cb, err); + }); +}; + +PeerSweeper.prototype.addDapps = function (peers) { + var self = this; + + peers = peers.filter(function (peer) { + return peer.action === 'upsert' && peer.dappid; + }); + + if (!peers.length) { return; } + + self.scope.library.db.tx(function (t) { + var peerPromises = peers.map(function (peer) { + if (peer.action === 'upsert') { + return t.query(self.scope.sql.getByIdPort, { ip: peer.ip, port: peer.port }); + } + }); + + return t.batch(peerPromises).then(function (res) { + for (var i = 0; i < peers.length; i++) { + var peer = peers[i]; + var row = res[i][0]; + + if (row && row.id) { + peer.id = row.id; + } + } + + var queries = peers.map(function (peer) { + return pgp.as.format(self.scope.sql.addDapp, { + dappId: peer.dappid, + peerId: peer.id + }); + }); + + return t.query(queries.join(';')); + }); + }).then(function () { + self.scope.library.logger.debug(['Added', peers.length, 'dapp peers'].join(' ')); + }).catch(function (err) { + self.scope.library.logger.error('Failed to add dapp peers', err.stack); + }); +}; + +// Export +module.exports = PeerSweeper; diff --git a/logic/round.js b/logic/round.js index 0bf37fcd99a..cb06dc5106e 100644 --- a/logic/round.js +++ b/logic/round.js @@ -27,7 +27,7 @@ Round.prototype.updateMissedBlocks = function () { return this.t; } - return this.t.none(sql.updateMissedBlocks, [this.scope.outsiders]); + return this.t.none(sql.updateMissedBlocks(this.scope.backwards), [this.scope.outsiders]); }; Round.prototype.getVotes = function () { @@ -53,6 +53,14 @@ Round.prototype.updateVotes = function () { }); }; +Round.prototype.markBlockId = function () { + if (this.scope.backwards) { + return this.t.none(sql.updateBlockId, { oldId: this.scope.block.id, newId: '0' }); + } else { + return this.t; + } +}; + Round.prototype.flushRound = function () { return this.t.none(sql.flush, { round: this.scope.round }); }; @@ -110,8 +118,13 @@ Round.prototype.land = function () { // Constructor function RoundChanges (scope) { - this.roundFees = Math.floor(scope.__private.feesByRound[scope.round]) || 0; - this.roundRewards = (scope.__private.rewardsByRound[scope.round] || []); + if (scope.backwards) { + this.roundFees = Math.floor(scope.__private.unFeesByRound[scope.round]) || 0; + this.roundRewards = (scope.__private.unRewardsByRound[scope.round] || []); + } else { + this.roundFees = Math.floor(scope.__private.feesByRound[scope.round]) || 0; + this.roundRewards = (scope.__private.rewardsByRound[scope.round] || []); + } } // Public methods diff --git a/logic/signature.js b/logic/signature.js index a2e8baffb88..5c72b623c0c 100644 --- a/logic/signature.js +++ b/logic/signature.js @@ -91,7 +91,7 @@ Signature.prototype.undo = function (trs, block, sender, cb) { Signature.prototype.applyUnconfirmed = function (trs, sender, cb) { if (sender.u_secondSignature || sender.secondSignature) { - return setImmediate(cb, 'Failed second signature: ' + trs.id); + return setImmediate(cb, 'Second signature already enabled'); } modules.accounts.setAccountAndGet({address: sender.address, u_secondSignature: 1}, cb); diff --git a/logic/transaction.js b/logic/transaction.js index e18cac596e7..6ed371685ef 100644 --- a/logic/transaction.js +++ b/logic/transaction.js @@ -6,11 +6,12 @@ var ByteBuffer = require('bytebuffer'); var constants = require('../helpers/constants.js'); var crypto = require('crypto'); var exceptions = require('../helpers/exceptions.js'); +var extend = require('extend'); var slots = require('../helpers/slots.js'); var sql = require('../sql/transactions.js'); // Private fields -var __private = {}, genesisblock = null; +var self, __private = {}, genesisblock = null; __private.types = {}; @@ -18,6 +19,7 @@ __private.types = {}; function Transaction (scope, cb) { this.scope = scope; genesisblock = this.scope.genesisblock; + self = this; if (cb) { return setImmediate(cb, null, this); } @@ -189,6 +191,40 @@ Transaction.prototype.ready = function (trs, sender) { return __private.types[trs.type].ready.call(this, trs, sender); }; +Transaction.prototype.countById = function (trs, cb) { + this.scope.db.one(sql.countById, { id: trs.id }).then(function (row) { + return setImmediate(cb, null, row.count); + }).catch(function (err) { + this.scope.logger.error(err.stack); + return setImmediate(cb, 'Transaction#countById error'); + }); +}; + +Transaction.prototype.checkConfirmed = function (trs, cb) { + this.countById(trs, function (err, count) { + if (err) { + return setImmediate(cb, err); + } else if (count > 0) { + return setImmediate(cb, 'Transaction is already confirmed: ' + trs.id); + } else { + return setImmediate(cb); + } + }); +}; + +Transaction.prototype.checkBalance = function (amount, balance, trs, sender) { + var exceededBalance = bignum(sender[balance].toString()).lessThan(amount); + var exceeded = (trs.blockId !== genesisblock.block.id && exceededBalance); + + return { + exceeded: exceeded, + error: exceeded ? [ + 'Account does not have enough LSK:', sender.address, + 'balance:', bignum(sender[balance].toString() || '0').div(Math.pow(10,8)) + ].join(' ') : null + }; +}; + Transaction.prototype.process = function (trs, sender, requester, cb) { if (typeof requester === 'function') { cb = requester; @@ -203,6 +239,11 @@ Transaction.prototype.process = function (trs, sender, requester, cb) { // return setImmediate(cb, 'Transaction is not ready: ' + trs.id); // } + // Check sender + if (!sender) { + return setImmediate(cb, 'Missing sender'); + } + // Get transaction id var txId; @@ -220,43 +261,16 @@ Transaction.prototype.process = function (trs, sender, requester, cb) { trs.id = txId; } - // Check sender - if (!sender) { - return setImmediate(cb, 'Missing sender'); - } - // Equalize sender address trs.senderId = sender.address; - // Check requester public key - if (trs.requesterPublicKey) { - if (sender.multisignatures.indexOf(trs.requesterPublicKey) < 0) { - return setImmediate(cb, 'Invalid requester public key'); - } - } - - // Verify signature - if (!this.verifySignature(trs, (trs.requesterPublicKey || trs.senderPublicKey), trs.signature)) { - return setImmediate(cb, 'Failed to verify signature'); - } - // Call process on transaction type __private.types[trs.type].process.call(this, trs, sender, function (err, trs) { if (err) { return setImmediate(cb, err); - } - - // Check for already confirmed transaction - this.scope.db.one(sql.countById, { id: trs.id }).then(function (row) { - if (row.count > 0) { - return setImmediate(cb, 'Transaction is already confirmed: ' + trs.id, trs, true); - } - + } else { return setImmediate(cb, null, trs); - }).catch(function (err) { - this.scope.logger.error(err.stack); - return setImmediate(cb, 'Transaction#process error'); - }); + } }.bind(this)); }; @@ -268,14 +282,34 @@ Transaction.prototype.verify = function (trs, sender, requester, cb) { cb = requester; } + // Check sender + if (!sender) { + return setImmediate(cb, 'Missing sender'); + } + // Check transaction type if (!__private.types[trs.type]) { return setImmediate(cb, 'Unknown transaction type ' + trs.type); } - // Check sender - if (!sender) { - return setImmediate(cb, 'Missing sender'); + // Check for missing sender second signature + if (!trs.requesterPublicKey && sender.secondSignature && !trs.signSignature && trs.blockId !== genesisblock.block.id) { + return setImmediate(cb, 'Missing sender second signature'); + } + + // If second signature provided, check if sender has one enabled + if (!trs.requesterPublicKey && !sender.secondSignature && (trs.signSignature && trs.signSignature.length > 0)) { + return setImmediate(cb, 'Sender does not have a second signature'); + } + + // Check for missing requester second signature + if (trs.requesterPublicKey && requester.secondSignature && !trs.signSignature) { + return setImmediate(cb, 'Missing requester second signature'); + } + + // If second signature provided, check if requester has one enabled + if (trs.requesterPublicKey && !requester.secondSignature && (trs.signSignature && trs.signSignature.length > 0)) { + return setImmediate(cb, 'Requester does not have a second signature'); } // Check sender public key @@ -295,10 +329,23 @@ Transaction.prototype.verify = function (trs, sender, requester, cb) { return setImmediate(cb, 'Invalid sender address'); } + // Determine multisignatures from sender or transaction asset + var multisignatures = sender.multisignatures || sender.u_multisignatures || []; + if (multisignatures.length === 0) { + if (trs.asset && trs.asset.multisignature && trs.asset.multisignature.keysgroup) { + + multisignatures = trs.asset.multisignature.keysgroup.map(function (key) { + return key.slice(1); + }); + } + } + // Check requester public key if (trs.requesterPublicKey) { + multisignatures.push(trs.senderPublicKey); + if (sender.multisignatures.indexOf(trs.requesterPublicKey) < 0) { - return setImmediate(cb, 'Invalid requester public key'); + return setImmediate(cb, 'Account does not belong to multisignature group'); } } @@ -350,22 +397,6 @@ Transaction.prototype.verify = function (trs, sender, requester, cb) { } } - // Determine multisignatures from sender or transaction asset - var multisignatures = sender.multisignatures || sender.u_multisignatures || []; - if (multisignatures.length === 0) { - if (trs.asset && trs.asset.multisignature && trs.asset.multisignature.keysgroup) { - - multisignatures = trs.asset.multisignature.keysgroup.map(function (key) { - return key.slice(1); - }); - } - } - - // Add sender to multisignatures - if (trs.requesterPublicKey) { - multisignatures.push(trs.senderPublicKey); - } - // Verify multisignatures if (trs.signatures) { for (var d = 0; d < trs.signatures.length; d++) { @@ -398,6 +429,14 @@ Transaction.prototype.verify = function (trs, sender, requester, cb) { return setImmediate(cb, 'Invalid transaction amount'); } + // Check confirmed sender balance + var amount = bignum(trs.amount.toString()).plus(trs.fee.toString()); + var senderBalance = this.checkBalance(amount, 'balance', trs, sender); + + if (senderBalance.exceeded) { + return setImmediate(cb, senderBalance.error); + } + // Check timestamp if (slots.getSlotNumber(trs.timestamp) > slots.getSlotNumber()) { return setImmediate(cb, 'Invalid transaction timestamp'); @@ -405,7 +444,12 @@ Transaction.prototype.verify = function (trs, sender, requester, cb) { // Call verify on transaction type __private.types[trs.type].verify.call(this, trs, sender, function (err) { - return setImmediate(cb, err); + if (err) { + return setImmediate(cb, err); + } else { + // Check for already confirmed transaction + return self.checkConfirmed(trs, cb); + } }); }; @@ -470,22 +514,16 @@ Transaction.prototype.verifyBytes = function (bytes, publicKey, signature) { }; Transaction.prototype.apply = function (trs, block, sender, cb) { - if (!__private.types[trs.type]) { - return setImmediate(cb, 'Unknown transaction type ' + trs.type); - } - if (!this.ready(trs, sender)) { return setImmediate(cb, 'Transaction is not ready'); } + // Check confirmed sender balance var amount = bignum(trs.amount.toString()).plus(trs.fee.toString()); - var exceedsBalance = bignum(sender.balance.toString()).lessThan(amount); + var senderBalance = this.checkBalance(amount, 'balance', trs, sender); - if (trs.blockId !== genesisblock.block.id && exceedsBalance) { - return setImmediate(cb, [ - 'Account does not have enough LSK:', sender.address, - 'balance:', bignum(sender.u_balance || 0).div(Math.pow(10,8)) - ].join(' ')); + if (senderBalance.exceeded) { + return setImmediate(cb, senderBalance.error); } amount = amount.toNumber(); @@ -516,11 +554,8 @@ Transaction.prototype.apply = function (trs, block, sender, cb) { }; Transaction.prototype.undo = function (trs, block, sender, cb) { - if (!__private.types[trs.type]) { - return setImmediate(cb, 'Unknown transaction type ' + trs.type); - } - - var amount = trs.amount + trs.fee; + var amount = bignum(trs.amount.toString()); + amount = amount.plus(trs.fee.toString()).toNumber(); this.scope.account.merge(sender.address, { balance: amount, @@ -552,34 +587,12 @@ Transaction.prototype.applyUnconfirmed = function (trs, sender, requester, cb) { cb = requester; } - if (!__private.types[trs.type]) { - return setImmediate(cb, 'Unknown transaction type ' + trs.type); - } - - if (!trs.requesterPublicKey && sender.secondSignature && !trs.signSignature && trs.blockId !== genesisblock.block.id) { - return setImmediate(cb, 'Missing sender second signature'); - } - - if (!trs.requesterPublicKey && !sender.secondSignature && (trs.signSignature && trs.signSignature.length > 0)) { - return setImmediate(cb, 'Sender does not have a second signature'); - } - - if (trs.requesterPublicKey && requester.secondSignature && !trs.signSignature) { - return setImmediate(cb, 'Missing requester second signature'); - } - - if (trs.requesterPublicKey && !requester.secondSignature && (trs.signSignature && trs.signSignature.length > 0)) { - return setImmediate(cb, 'Requester does not have a second signature'); - } - + // Check unconfirmed sender balance var amount = bignum(trs.amount.toString()).plus(trs.fee.toString()); - var exceedsBalance = bignum(sender.u_balance.toString()).lessThan(amount); + var senderBalance = this.checkBalance(amount, 'u_balance', trs, sender); - if (trs.blockId !== genesisblock.block.id && exceedsBalance) { - return setImmediate(cb, [ - 'Account does not have enough LSK:', sender.address, - 'balance:', bignum(sender.balance || 0).div(Math.pow(10,8)) - ].join(' ')); + if (senderBalance.exceeded) { + return setImmediate(cb, senderBalance.error); } amount = amount.toNumber(); @@ -602,11 +615,8 @@ Transaction.prototype.applyUnconfirmed = function (trs, sender, requester, cb) { }; Transaction.prototype.undoUnconfirmed = function (trs, sender, cb) { - if (!__private.types[trs.type]) { - return setImmediate(cb, 'Unknown transaction type ' + trs.type); - } - - var amount = trs.amount + trs.fee; + var amount = bignum(trs.amount.toString()); + amount = amount.plus(trs.fee.toString()).toNumber(); this.scope.account.merge(sender.address, {u_balance: amount}, function (err, sender) { if (err) { @@ -709,13 +719,19 @@ Transaction.prototype.schema = { type: 'object', properties: { id: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, height: { type: 'integer' }, blockId: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, type: { type: 'integer' @@ -732,10 +748,16 @@ Transaction.prototype.schema = { format: 'publicKey' }, senderId: { - type: 'string' + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 }, recipientId: { - type: 'string' + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 }, amount: { type: 'integer', @@ -820,7 +842,7 @@ Transaction.prototype.dbRead = function (raw) { var asset = __private.types[tx.type].dbRead.call(this, raw); if (asset) { - tx.asset = _.extend(tx.asset, asset); + tx.asset = extend(tx.asset, asset); } return tx; diff --git a/logic/transactionPool.js b/logic/transactionPool.js new file mode 100644 index 00000000000..02e90ee9505 --- /dev/null +++ b/logic/transactionPool.js @@ -0,0 +1,541 @@ +'use strict'; + +var async = require('async'); +var constants = require('../helpers/constants.js'); +var transactionTypes = require('../helpers/transactionTypes.js'); + +// Private fields +var modules, library, self, __private = {}; + +// Constructor +function TransactionPool (scope) { + library = scope; + self = this; + + self.unconfirmed = { transactions: [], index: {} }; + self.bundled = { transactions: [], index: {} }; + self.queued = { transactions: [], index: {} }; + self.multisignature = { transactions: [], index: {} }; + self.expiryInterval = 30000; + self.bundledInterval = library.config.broadcasts.broadcastInterval; + self.bundleLimit = library.config.broadcasts.releaseLimit; + self.processed = 0; + + // Bundled transaction timer + setImmediate(function nextBundle () { + async.series([ + self.processBundled + ], function (err) { + if (err) { + library.logger.log('Bundled transaction timer', err); + } + + return setTimeout(nextBundle, self.bundledInterval); + }); + }); + + // Transaction expiry timer + setImmediate(function nextExpiry () { + async.series([ + self.expireTransactions + ], function (err) { + if (err) { + library.logger.log('Transaction expiry timer', err); + } + + return setTimeout(nextExpiry, self.expiryInterval); + }); + }); +} + +// Public methods +TransactionPool.prototype.bind = function (scope) { + modules = scope; +}; + +TransactionPool.prototype.transactionInPool = function (id) { + return [ + self.unconfirmed.index[id], + self.bundled.index[id], + self.queued.index[id], + self.multisignature.index[id] + ].filter(Boolean).length > 0; +}; + +TransactionPool.prototype.getUnconfirmedTransaction = function (id) { + var index = self.unconfirmed.index[id]; + return self.unconfirmed.transactions[index]; +}; + +TransactionPool.prototype.getBundledTransaction = function (id) { + var index = self.bundled.index[id]; + return self.bundled.transactions[index]; +}; + +TransactionPool.prototype.getQueuedTransaction = function (id) { + var index = self.queued.index[id]; + return self.queued.transactions[index]; +}; + +TransactionPool.prototype.getMultisignatureTransaction = function (id) { + var index = self.multisignature.index[id]; + return self.multisignature.transactions[index]; +}; + +TransactionPool.prototype.getUnconfirmedTransactionList = function (reverse, limit) { + return __private.getTransactionList(self.unconfirmed.transactions, reverse, limit); +}; + +TransactionPool.prototype.getBundledTransactionList = function (reverse, limit) { + return __private.getTransactionList(self.bundled.transactions, reverse, limit); +}; + +TransactionPool.prototype.getQueuedTransactionList = function (reverse, limit) { + return __private.getTransactionList(self.queued.transactions, reverse, limit); +}; + +TransactionPool.prototype.getMultisignatureTransactionList = function (reverse, ready, limit) { + if (ready) { + return __private.getTransactionList(self.multisignature.transactions, reverse).filter(function (transaction) { + return transaction.ready; + }); + } else { + return __private.getTransactionList(self.multisignature.transactions, reverse, limit); + } +}; + +TransactionPool.prototype.getMergedTransactionList = function (reverse, limit) { + var minLimit = (constants.maxTxsPerBlock + 2); + + if (limit <= minLimit || limit > constants.maxSharedTxs) { + limit = minLimit; + } + + var unconfirmed = modules.transactions.getUnconfirmedTransactionList(false, constants.maxTxsPerBlock); + limit -= unconfirmed.length; + + var multisignatures = modules.transactions.getMultisignatureTransactionList(false, false, constants.maxTxsPerBlock); + limit -= multisignatures.length; + + var queued = modules.transactions.getQueuedTransactionList(false, limit); + limit -= queued.length; + + return unconfirmed.concat(multisignatures).concat(queued); +}; + +TransactionPool.prototype.addUnconfirmedTransaction = function (transaction) { + if (transaction.type === transactionTypes.MULTI || Array.isArray(transaction.signatures)) { + self.removeMultisignatureTransaction(transaction.id); + } else { + self.removeQueuedTransaction(transaction.id); + } + + if (self.unconfirmed.index[transaction.id] === undefined) { + if (!transaction.receivedAt) { + transaction.receivedAt = new Date(); + } + + self.unconfirmed.transactions.push(transaction); + var index = self.unconfirmed.transactions.indexOf(transaction); + self.unconfirmed.index[transaction.id] = index; + } +}; + +TransactionPool.prototype.removeUnconfirmedTransaction = function (id) { + var index = self.unconfirmed.index[id]; + + if (index !== undefined) { + self.unconfirmed.transactions[index] = false; + delete self.unconfirmed.index[id]; + } + + self.removeQueuedTransaction(id); + self.removeMultisignatureTransaction(id); +}; + +TransactionPool.prototype.countUnconfirmed = function () { + return Object.keys(self.unconfirmed.index).length; +}; + +TransactionPool.prototype.addBundledTransaction = function (transaction) { + self.bundled.transactions.push(transaction); + var index = self.bundled.transactions.indexOf(transaction); + self.bundled.index[transaction.id] = index; +}; + +TransactionPool.prototype.removeBundledTransaction = function (id) { + var index = self.bundled.index[id]; + + if (index !== undefined) { + self.bundled.transactions[index] = false; + delete self.bundled.index[id]; + } +}; + +TransactionPool.prototype.countBundled = function () { + return Object.keys(self.bundled.index).length; +}; + +TransactionPool.prototype.addQueuedTransaction = function (transaction) { + if (self.queued.index[transaction.id] === undefined) { + if (!transaction.receivedAt) { + transaction.receivedAt = new Date(); + } + + self.queued.transactions.push(transaction); + var index = self.queued.transactions.indexOf(transaction); + self.queued.index[transaction.id] = index; + } +}; + +TransactionPool.prototype.removeQueuedTransaction = function (id) { + var index = self.queued.index[id]; + + if (index !== undefined) { + self.queued.transactions[index] = false; + delete self.queued.index[id]; + } +}; + +TransactionPool.prototype.countQueued = function () { + return Object.keys(self.queued.index).length; +}; + +TransactionPool.prototype.addMultisignatureTransaction = function (transaction) { + if (self.multisignature.index[transaction.id] === undefined) { + if (!transaction.receivedAt) { + transaction.receivedAt = new Date(); + } + + self.multisignature.transactions.push(transaction); + var index = self.multisignature.transactions.indexOf(transaction); + self.multisignature.index[transaction.id] = index; + } +}; + +TransactionPool.prototype.removeMultisignatureTransaction = function (id) { + var index = self.multisignature.index[id]; + + if (index !== undefined) { + self.multisignature.transactions[index] = false; + delete self.multisignature.index[id]; + } +}; + +TransactionPool.prototype.countMultisignature = function () { + return Object.keys(self.multisignature.index).length; +}; + +TransactionPool.prototype.receiveTransactions = function (transactions, broadcast, cb) { + async.eachSeries(transactions, function (transaction, cb) { + self.processUnconfirmedTransaction(transaction, broadcast, cb); + }, function (err) { + return setImmediate(cb, err, transactions); + }); +}; + +TransactionPool.prototype.reindexQueues = function () { + ['bundled', 'queued', 'multisignature', 'unconfirmed'].forEach(function (queue) { + self[queue].index = {}; + self[queue].transactions = self[queue].transactions.filter(Boolean); + self[queue].transactions.forEach(function (transaction) { + var index = self[queue].transactions.indexOf(transaction); + self[queue].index[transaction.id] = index; + }); + }); +}; + +TransactionPool.prototype.processBundled = function (cb) { + var bundled = self.getBundledTransactionList(true, self.bundleLimit); + + async.eachSeries(bundled, function (transaction, eachSeriesCb) { + if (!transaction) { + return setImmediate(eachSeriesCb); + } + + self.removeBundledTransaction(transaction.id); + delete transaction.bundled; + + __private.processVerifyTransaction(transaction, true, function (err, sender) { + if (err) { + library.logger.debug('Failed to process / verify bundled transaction: ' + transaction.id, err); + self.removeUnconfirmedTransaction(transaction); + return setImmediate(eachSeriesCb); + } else { + self.queueTransaction(transaction, function (err) { + if (err) { + library.logger.debug('Failed to queue bundled transaction: ' + transaction.id, err); + } + return setImmediate(eachSeriesCb); + }); + } + }); + }, function (err) { + return setImmediate(cb, err); + }); +}; + +TransactionPool.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) { + if (self.transactionInPool(transaction.id)) { + return setImmediate(cb, 'Transaction is already processed: ' + transaction.id); + } else { + self.processed++; + if (self.processed > 1000) { + self.reindexQueues(); + self.processed = 1; + } + } + + if (transaction.bundled) { + return self.queueTransaction(transaction, cb); + } + + __private.processVerifyTransaction(transaction, broadcast, function (err) { + if (!err) { + return self.queueTransaction(transaction, cb); + } else { + return setImmediate(cb, err); + } + }); +}; + +TransactionPool.prototype.queueTransaction = function (transaction, cb) { + delete transaction.receivedAt; + + if (transaction.bundled) { + if (self.countBundled() >= constants.maxTxsPerQueue) { + return setImmediate(cb, 'Transaction pool is full'); + } else { + self.addBundledTransaction(transaction); + } + } else if (transaction.type === transactionTypes.MULTI || Array.isArray(transaction.signatures)) { + if (self.countMultisignature() >= constants.maxTxsPerQueue) { + return setImmediate(cb, 'Transaction pool is full'); + } else { + self.addMultisignatureTransaction(transaction); + } + } else { + if (self.countQueued() >= constants.maxTxsPerQueue) { + return setImmediate(cb, 'Transaction pool is full'); + } else { + self.addQueuedTransaction(transaction); + } + } + + return setImmediate(cb); +}; + +TransactionPool.prototype.applyUnconfirmedList = function (cb) { + return __private.applyUnconfirmedList(self.getUnconfirmedTransactionList(true), cb); +}; + +TransactionPool.prototype.applyUnconfirmedIds = function (ids, cb) { + return __private.applyUnconfirmedList(ids, cb); +}; + +TransactionPool.prototype.undoUnconfirmedList = function (cb) { + var ids = []; + + async.eachSeries(self.getUnconfirmedTransactionList(false), function (transaction, eachSeriesCb) { + if (transaction) { + ids.push(transaction.id); + modules.transactions.undoUnconfirmed(transaction, function (err) { + if (err) { + library.logger.error('Failed to undo unconfirmed transaction: ' + transaction.id, err); + self.removeUnconfirmedTransaction(transaction.id); + } + return setImmediate(eachSeriesCb); + }); + } else { + return setImmediate(eachSeriesCb); + } + }, function (err) { + return setImmediate(cb, err, ids); + }); +}; + +TransactionPool.prototype.expireTransactions = function (cb) { + var ids = []; + + async.waterfall([ + function (seriesCb) { + __private.expireTransactions(self.getUnconfirmedTransactionList(true), ids, seriesCb); + }, + function (res, seriesCb) { + __private.expireTransactions(self.getQueuedTransactionList(true), ids, seriesCb); + }, + function (res, seriesCb) { + __private.expireTransactions(self.getMultisignatureTransactionList(true, false), ids, seriesCb); + } + ], function (err, ids) { + return setImmediate(cb, err, ids); + }); +}; + +TransactionPool.prototype.fillPool = function (cb) { + if (modules.loader.syncing()) { return setImmediate(cb); } + + var unconfirmedCount = self.countUnconfirmed(); + library.logger.debug('Transaction pool size: ' + unconfirmedCount); + + if (unconfirmedCount >= constants.maxTxsPerBlock) { + return setImmediate(cb); + } else { + var spare = 0, spareMulti; + var multisignatures; + var multisignaturesLimit = 5; + var transactions; + + spare = (constants.maxTxsPerBlock - unconfirmedCount); + spareMulti = (spare >= multisignaturesLimit) ? multisignaturesLimit : 0; + multisignatures = self.getMultisignatureTransactionList(true, true, multisignaturesLimit).slice(0, spareMulti); + spare = Math.abs(spare - multisignatures.length); + transactions = self.getQueuedTransactionList(true, constants.maxTxsPerBlock).slice(0, spare); + transactions = multisignatures.concat(transactions); + + transactions.forEach(function (transaction) { + self.addUnconfirmedTransaction(transaction); + }); + + return __private.applyUnconfirmedList(transactions, cb); + } +}; + +// Private +__private.getTransactionList = function (transactions, reverse, limit) { + var a = []; + + for (var i = 0; i < transactions.length; i++) { + var transaction = transactions[i]; + + if (transaction !== false) { + a.push(transaction); + } + } + + a = reverse ? a.reverse() : a; + + if (limit) { + a.splice(limit); + } + + return a; +}; + +__private.processVerifyTransaction = function (transaction, broadcast, cb) { + if (!transaction) { + return setImmediate(cb, 'Missing transaction'); + } + + async.waterfall([ + function setAccountAndGet (waterCb) { + modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, waterCb); + }, + function getRequester (sender, waterCb) { + var multisignatures = Array.isArray(sender.multisignatures) && sender.multisignatures.length; + + if (multisignatures) { + transaction.signatures = transaction.signatures || []; + } + + if (sender && transaction.requesterPublicKey && multisignatures) { + modules.accounts.getAccount({publicKey: transaction.requesterPublicKey}, function (err, requester) { + if (!requester) { + return setImmediate(waterCb, 'Requester not found'); + } else { + return setImmediate(waterCb, null, sender, requester); + } + }); + } else { + return setImmediate(waterCb, null, sender, null); + } + }, + function processTransaction (sender, requester, waterCb) { + library.logic.transaction.process(transaction, sender, requester, function (err) { + if (err) { + return setImmediate(waterCb, err); + } else { + return setImmediate(waterCb, null, sender); + } + }); + }, + function verifyTransaction (sender, waterCb) { + library.logic.transaction.verify(transaction, sender, function (err) { + if (err) { + return setImmediate(waterCb, err); + } else { + return setImmediate(waterCb, null, sender); + } + }); + } + ], function (err, sender) { + if (!err) { + library.bus.message('unconfirmedTransaction', transaction, broadcast); + } + + return setImmediate(cb, err, sender); + }); +}; + +__private.applyUnconfirmedList = function (transactions, cb) { + async.eachSeries(transactions, function (transaction, eachSeriesCb) { + if (typeof transaction === 'string') { + transaction = self.getUnconfirmedTransaction(transaction); + } + if (!transaction) { + return setImmediate(eachSeriesCb); + } + __private.processVerifyTransaction(transaction, false, function (err, sender) { + if (err) { + library.logger.error('Failed to process / verify unconfirmed transaction: ' + transaction.id, err); + self.removeUnconfirmedTransaction(transaction.id); + return setImmediate(eachSeriesCb); + } + modules.transactions.applyUnconfirmed(transaction, sender, function (err) { + if (err) { + library.logger.error('Failed to apply unconfirmed transaction: ' + transaction.id, err); + self.removeUnconfirmedTransaction(transaction.id); + } + return setImmediate(eachSeriesCb); + }); + }); + }, cb); +}; + +__private.transactionTimeOut = function (transaction) { + if (transaction.type === transactionTypes.MULTI) { + return (transaction.asset.multisignature.lifetime * 3600); + } else if (Array.isArray(transaction.signatures)) { + return (constants.unconfirmedTransactionTimeOut * 8); + } else { + return (constants.unconfirmedTransactionTimeOut); + } +}; + +__private.expireTransactions = function (transactions, parentIds, cb) { + var ids = []; + + async.eachSeries(transactions, function (transaction, eachSeriesCb) { + if (!transaction) { + return setImmediate(eachSeriesCb); + } + + var timeNow = new Date(); + var timeOut = __private.transactionTimeOut(transaction); + var seconds = Math.floor((timeNow.getTime() - new Date(transaction.receivedAt).getTime()) / 1000); + + if (seconds > timeOut) { + ids.push(transaction.id); + self.removeUnconfirmedTransaction(transaction.id); + library.logger.info('Expired transaction: ' + transaction.id + ' received at: ' + transaction.receivedAt.toUTCString()); + return setImmediate(eachSeriesCb); + } else { + return setImmediate(eachSeriesCb); + } + }, function (err) { + return setImmediate(cb, err, ids.concat(parentIds)); + }); +}; + +// Export +module.exports = TransactionPool; diff --git a/logic/transfer.js b/logic/transfer.js index 3ecb9a359b6..4b9d3e55560 100644 --- a/logic/transfer.js +++ b/logic/transfer.js @@ -26,9 +26,8 @@ Transfer.prototype.calculateFee = function (trs, sender) { }; Transfer.prototype.verify = function (trs, sender, cb) { - var isAddress = /^[0-9]{1,21}[L|l]$/g; - if (!trs.recipientId || !isAddress.test(trs.recipientId)) { - return setImmediate(cb, 'Invalid recipient'); + if (!trs.recipientId) { + return setImmediate(cb, 'Missing recipient'); } if (trs.amount <= 0) { diff --git a/logic/vote.js b/logic/vote.js index 2f1a5a1996d..f72cf0c44e9 100644 --- a/logic/vote.js +++ b/logic/vote.js @@ -1,14 +1,17 @@ 'use strict'; +var async = require('async'); var constants = require('../helpers/constants.js'); var exceptions = require('../helpers/exceptions.js'); var Diff = require('../helpers/diff.js'); // Private fields -var modules, library; +var modules, library, self; // Constructor -function Vote () {} +function Vote () { + self = this; +} // Public methods Vote.prototype.bind = function (scope) { @@ -48,13 +51,30 @@ Vote.prototype.verify = function (trs, sender, cb) { return setImmediate(cb, 'Voting limit exceeded. Maximum is 33 votes per transaction'); } - modules.delegates.checkDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { + self.checkConfirmedDelegates(trs, cb); +}; + +Vote.prototype.checkConfirmedDelegates = function (trs, cb) { + modules.delegates.checkConfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { + if (err && exceptions.votes.indexOf(trs.id) > -1) { + library.logger.debug(err); + library.logger.debug(JSON.stringify(trs)); + err = null; + } + + return setImmediate(cb, err); + }); +}; + +Vote.prototype.checkUnconfirmedDelegates = function (trs, cb) { + modules.delegates.checkUnconfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { if (err && exceptions.votes.indexOf(trs.id) > -1) { library.logger.debug(err); library.logger.debug(JSON.stringify(trs)); err = null; } - return setImmediate(cb, err, trs); + + return setImmediate(cb, err); }); }; @@ -75,13 +95,22 @@ Vote.prototype.getBytes = function (trs) { }; Vote.prototype.apply = function (trs, block, sender, cb) { - this.scope.account.merge(sender.address, { - delegates: trs.asset.votes, - blockId: block.id, - round: modules.rounds.calc(block.height) - }, function (err) { - return setImmediate(cb, err); - }); + var parent = this; + + async.series([ + function (seriesCb) { + self.checkConfirmedDelegates(trs, seriesCb); + }, + function (seriesCb) { + parent.scope.account.merge(sender.address, { + delegates: trs.asset.votes, + blockId: block.id, + round: modules.rounds.calc(block.height) + }, function (err) { + return setImmediate(cb, err); + }); + } + ], cb); }; Vote.prototype.undo = function (trs, block, sender, cb) { @@ -99,23 +128,20 @@ Vote.prototype.undo = function (trs, block, sender, cb) { }; Vote.prototype.applyUnconfirmed = function (trs, sender, cb) { - modules.delegates.checkUnconfirmedDelegates(trs.senderPublicKey, trs.asset.votes, function (err) { - if (err) { - if (exceptions.votes.indexOf(trs.id) > -1) { - library.logger.debug(err); - library.logger.debug(JSON.stringify(trs)); - err = null; - } else { - return setImmediate(cb, err); - } + var parent = this; + + async.series([ + function (seriesCb) { + self.checkUnconfirmedDelegates(trs, seriesCb); + }, + function (seriesCb) { + parent.scope.account.merge(sender.address, { + u_delegates: trs.asset.votes + }, function (err) { + return setImmediate(seriesCb, err); + }); } - - this.scope.account.merge(sender.address, { - u_delegates: trs.asset.votes - }, function (err) { - return setImmediate(cb, err); - }); - }.bind(this)); + ], cb); }; Vote.prototype.undoUnconfirmed = function (trs, sender, cb) { diff --git a/modules/accounts.js b/modules/accounts.js index cb024e6619c..2d6832a5d0b 100644 --- a/modules/accounts.js +++ b/modules/accounts.js @@ -10,6 +10,7 @@ var schema = require('../schema/accounts.js'); var sandboxHelper = require('../helpers/sandbox.js'); var slots = require('../helpers/slots.js'); var transactionTypes = require('../helpers/transactionTypes.js'); +var Vote = require('../logic/vote.js'); // Private fields var modules, library, self, __private = {}, shared = {}; @@ -24,7 +25,6 @@ function Accounts (cb, scope) { __private.attachApi(); - var Vote = require('../logic/vote.js'); __private.assetTypes[transactionTypes.VOTE] = library.logic.transaction.attachAssetType( transactionTypes.VOTE, new Vote() ); @@ -100,7 +100,7 @@ __private.attachApi = function () { library.network.app.use('/api/accounts', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -116,6 +116,9 @@ __private.openAccount = function (secret, cb) { } if (account) { + if (account.publicKey == null) { + account.publicKey = publicKey; + } return setImmediate(cb, null, account); } else { return setImmediate(cb, null, { @@ -253,11 +256,6 @@ shared.getBalance = function (req, cb) { return setImmediate(cb, err[0].message); } - var isAddress = /^[0-9]{1,21}[L|l]$/g; - if (!isAddress.test(req.body.address)) { - return setImmediate(cb, 'Invalid address'); - } - self.getAccount({ address: req.body.address }, function (err, account) { if (err) { return setImmediate(cb, err); @@ -277,11 +275,6 @@ shared.getPublickey = function (req, cb) { return setImmediate(cb, err[0].message); } - var isAddress = /^[0-9]{1,21}[L|l]$/g; - if (!isAddress.test(req.body.address)) { - return setImmediate(cb, 'Invalid address'); - } - self.getAccount({ address: req.body.address }, function (err, account) { if (err) { return setImmediate(cb, err); @@ -423,7 +416,7 @@ shared.addDelegates = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -461,7 +454,7 @@ shared.addDelegates = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } }, function (err, transaction) { @@ -480,11 +473,6 @@ shared.getAccount = function (req, cb) { return setImmediate(cb, err[0].message); } - var isAddress = /^[0-9]{1,21}[L|l]$/g; - if (!isAddress.test(req.body.address)) { - return setImmediate(cb, 'Invalid address'); - } - self.getAccount({ address: req.body.address }, function (err, account) { if (err) { return setImmediate(cb, err); diff --git a/modules/blocks.js b/modules/blocks.js index 65ac8a00f65..e90ab079b74 100644 --- a/modules/blocks.js +++ b/modules/blocks.js @@ -99,6 +99,7 @@ __private.attachApi = function () { router.map(shared, { 'get /get': 'getBlock', 'get /': 'getBlocks', + 'get /getBroadhash': 'getBroadhash', 'get /getEpoch': 'getEpoch', 'get /getHeight': 'getHeight', 'get /getNethash': 'getNethash', @@ -117,37 +118,11 @@ __private.attachApi = function () { library.network.app.use('/api/blocks', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; -__private.saveGenesisBlock = function (cb) { - library.db.query(sql.getGenesisBlockId, { id: genesisblock.block.id }).then(function (rows) { - var blockId = rows.length && rows[0].id; - - if (!blockId) { - __private.saveBlock(genesisblock.block, function (err) { - return setImmediate(cb, err); - }); - } else { - return setImmediate(cb); - } - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#saveGenesisBlock error'); - }); -}; - -__private.deleteBlock = function (blockId, cb) { - library.db.none(sql.deleteBlock, {id: blockId}).then(function () { - return setImmediate(cb); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#deleteBlock error'); - }); -}; - __private.list = function (filter, cb) { var params = {}, where = []; @@ -245,6 +220,44 @@ __private.list = function (filter, cb) { }); }; +__private.readDbRows = function (rows) { + var blocks = {}; + var order = []; + + for (var i = 0, length = rows.length; i < length; i++) { + var block = library.logic.block.dbRead(rows[i]); + + if (block) { + if (!blocks[block.id]) { + if (block.id === genesisblock.block.id) { + block.generationSignature = (new Array(65)).join('0'); + } + + order.push(block.id); + blocks[block.id] = block; + } + + var transaction = library.logic.transaction.dbRead(rows[i]); + blocks[block.id].transactions = blocks[block.id].transactions || {}; + + if (transaction) { + if (!blocks[block.id].transactions[transaction.id]) { + blocks[block.id].transactions[transaction.id] = transaction; + } + } + } + } + + blocks = order.map(function (v) { + blocks[v].transactions = Object.keys(blocks[v].transactions).map(function (t) { + return blocks[v].transactions[t]; + }); + return blocks[v]; + }); + + return blocks; +}; + __private.getById = function (id, cb) { library.db.query(sql.getById, {id: id}).then(function (rows) { if (!rows.length) { @@ -260,6 +273,94 @@ __private.getById = function (id, cb) { }); }; +__private.getIdSequence = function (height, cb) { + library.db.query(sql.getIdSequence, { height: height, limit: 5, delegates: slots.delegates, activeDelegates: constants.activeDelegates }).then(function (rows) { + if (rows.length === 0) { + return setImmediate(cb, 'Failed to get id sequence for height: ' + height); + } + + var ids = []; + + if (genesisblock && genesisblock.block) { + var __genesisblock = { + id: genesisblock.block.id, + height: genesisblock.block.height + }; + + if (!_.includes(rows, __genesisblock.id)) { + rows.push(__genesisblock); + } + } + + if (__private.lastBlock && !_.includes(rows, __private.lastBlock.id)) { + rows.unshift({ + id: __private.lastBlock.id, + height: __private.lastBlock.height + }); + } + + rows.forEach(function (row) { + if (!_.includes(ids, row.id)) { + ids.push(row.id); + } + }); + + return setImmediate(cb, null, { firstHeight: rows[0].height, ids: ids.join(',') }); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#getIdSequence error'); + }); +}; + +__private.saveGenesisBlock = function (cb) { + library.db.query(sql.getGenesisBlockId, { id: genesisblock.block.id }).then(function (rows) { + var blockId = rows.length && rows[0].id; + + if (!blockId) { + __private.saveBlock(genesisblock.block, function (err) { + return setImmediate(cb, err); + }); + } else { + return setImmediate(cb); + } + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#saveGenesisBlock error'); + }); +}; + +// Apply the genesis block, provided it has been verified. +// Shortcuting the unconfirmed/confirmed states. +__private.applyGenesisBlock = function (block, cb) { + block.transactions = block.transactions.sort(function (a, b) { + if (a.type === transactionTypes.VOTE) { + return 1; + } else { + return 0; + } + }); + async.eachSeries(block.transactions, function (transaction, cb) { + modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { + if (err) { + return setImmediate(cb, { + message: err, + transaction: transaction, + block: block + }); + } + __private.applyTransaction(block, transaction, sender, cb); + }); + }, function (err) { + if (err) { + // If genesis block is invalid, kill the node... + return process.exit(0); + } else { + __private.lastBlock = block; + modules.rounds.tick(__private.lastBlock, cb); + } + }); +}; + __private.saveBlock = function (block, cb) { library.db.tx(function (t) { var promise = library.logic.block.dbSave(block); @@ -318,127 +419,207 @@ __private.promiseTransactions = function (t, block, blockPromises) { return t; }; -__private.afterSave = function (block, cb) { - async.eachSeries(block.transactions, function (transaction, cb) { - return library.logic.transaction.afterSave(transaction, cb); - }, function (err) { - return setImmediate(cb, err); - }); -}; +// Apply the block, provided it has been verified. +__private.applyBlock = function (block, broadcast, cb, saveBlock) { + // Prevent shutdown during database writes. + __private.isActive = true; -__private.popLastBlock = function (oldLastBlock, cb) { - library.balancesSequence.add(function (cb) { - self.loadBlocksPart({ id: oldLastBlock.previousBlock }, function (err, previousBlock) { - if (err || !previousBlock.length) { - return setImmediate(cb, err || 'previousBlock is null'); - } - previousBlock = previousBlock[0]; + // Transactions to rewind in case of error. + var appliedTransactions = {}; - async.eachSeries(oldLastBlock.transactions.reverse(), function (transaction, cb) { - async.series([ - function (cb) { - modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { - if (err) { - return setImmediate(cb, err); - } - modules.transactions.undo(transaction, oldLastBlock, sender, cb); - }); - }, function (cb) { - modules.transactions.undoUnconfirmed(transaction, cb); - }, function (cb) { - return setImmediate(cb); - } - ], cb); - }, function (err) { - modules.rounds.backwardTick(oldLastBlock, previousBlock, function () { - __private.deleteBlock(oldLastBlock.id, function (err) { + // List of unconfirmed transactions ids. + var unconfirmedTransactionIds; + + async.series({ + // Rewind any unconfirmed transactions before applying block. + // TODO: It should be possible to remove this call if we can guarantee that only this function is processing transactions atomically. Then speed should be improved further. + // TODO: Other possibility, when we rebuild from block chain this action should be moved out of the rebuild function. + undoUnconfirmedList: function (seriesCb) { + modules.transactions.undoUnconfirmedList(function (err, ids) { + if (err) { + // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. + return process.exit(0); + } else { + unconfirmedTransactionIds = ids; + return setImmediate(seriesCb); + } + }); + }, + // Apply transactions to unconfirmed mem_accounts fields. + applyUnconfirmed: function (seriesCb) { + async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { + // DATABASE write + modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { + // DATABASE: write + modules.transactions.applyUnconfirmed(transaction, sender, function (err) { if (err) { - return setImmediate(cb, err); + err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); + library.logger.error(err); + library.logger.error('Transaction', transaction); + return setImmediate(eachSeriesCb, err); } - return setImmediate(cb, null, previousBlock); + appliedTransactions[transaction.id] = transaction; + + // Remove the transaction from the node queue, if it was present. + var index = unconfirmedTransactionIds.indexOf(transaction.id); + if (index >= 0) { + unconfirmedTransactionIds.splice(index, 1); + } + + return setImmediate(eachSeriesCb); }); }); + }, function (err) { + if (err) { + // Rewind any already applied unconfirmed transactions. + // Leaves the database state as per the previous block. + async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { + modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { + if (err) { + return setImmediate(eachSeriesCb, err); + } + // The transaction has been applied? + if (appliedTransactions[transaction.id]) { + // DATABASE: write + library.logic.transaction.undoUnconfirmed(transaction, sender, eachSeriesCb); + } else { + return setImmediate(eachSeriesCb); + } + }); + }, function (err) { + return setImmediate(seriesCb, err); + }); + } else { + return setImmediate(seriesCb); + } }); - }); - }, cb); -}; - -__private.getIdSequence = function (height, cb) { - library.db.query(sql.getIdSequence, { height: height, limit: 5, delegates: slots.delegates, activeDelegates: constants.activeDelegates }).then(function (rows) { - if (rows.length === 0) { - return setImmediate(cb, 'Failed to get id sequence for height: ' + height); - } + }, + // Block and transactions are ok. + // Apply transactions to confirmed mem_accounts fields. + applyConfirmed: function (seriesCb) { + async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { + modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { + if (err) { + err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); + library.logger.error(err); + library.logger.error('Transaction', transaction); + // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. + process.exit(0); + } + // DATABASE: write + modules.transactions.apply(transaction, block, sender, function (err) { + if (err) { + err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); + library.logger.error(err); + library.logger.error('Transaction', transaction); + // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. + process.exit(0); + } + // Transaction applied, removed from the unconfirmed list. + modules.transactions.removeUnconfirmedTransaction(transaction.id); + return setImmediate(eachSeriesCb); + }); + }); + }, function (err) { + return setImmediate(seriesCb, err); + }); + }, + // Optionally save the block to the database. + saveBlock: function (seriesCb) { + __private.lastBlock = block; - var ids = []; + if (saveBlock) { + // DATABASE: write + __private.saveBlock(block, function (err) { + if (err) { + library.logger.error('Failed to save block...'); + library.logger.error('Block', block); + // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. + process.exit(0); + } - if (genesisblock && genesisblock.block) { - var __genesisblock = { - id: genesisblock.block.id, - height: genesisblock.block.height - }; + library.logger.debug('Block applied correctly with ' + block.transactions.length + ' transactions'); + library.bus.message('newBlock', block, broadcast); - if (!_.includes(rows, __genesisblock.id)) { - rows.push(__genesisblock); - } - } + // DATABASE write. Update delegates accounts + modules.rounds.tick(block, seriesCb); + }); + } else { + library.bus.message('newBlock', block, broadcast); - if (__private.lastBlock && !_.includes(rows, __private.lastBlock.id)) { - rows.unshift({ - id: __private.lastBlock.id, - height: __private.lastBlock.height + // DATABASE write. Update delegates accounts + modules.rounds.tick(block, seriesCb); + } + }, + // Push back unconfirmed transactions list (minus the one that were on the block if applied correctly). + // TODO: See undoUnconfirmedList discussion above. + applyUnconfirmedIds: function (seriesCb) { + // DATABASE write + modules.transactions.applyUnconfirmedIds(unconfirmedTransactionIds, function (err) { + return setImmediate(seriesCb, err); }); - } + }, + }, function (err) { + // Allow shutdown, database writes are finished. + __private.isActive = false; - rows.forEach(function (row) { - if (!_.includes(ids, row.id)) { - ids.push(row.id); - } - }); + // Nullify large objects. + // Prevents memory leak during synchronisation. + appliedTransactions = unconfirmedTransactionIds = block = null; - return setImmediate(cb, null, { firstHeight: rows[0].height, ids: ids.join(',') }); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#getIdSequence error'); + // Finish here if snapshotting. + if (err === 'Snapshot finished') { + library.logger.info(err); + process.emit('SIGTERM'); + } + + return setImmediate(cb, err); }); }; -__private.readDbRows = function (rows) { - var blocks = {}; - var order = []; - - for (var i = 0, length = rows.length; i < length; i++) { - var block = library.logic.block.dbRead(rows[i]); - - if (block) { - if (!blocks[block.id]) { - if (block.id === genesisblock.block.id) { - block.generationSignature = (new Array(65)).join('0'); - } - - order.push(block.id); - blocks[block.id] = block; +__private.checkTransaction = function (block, transaction, cb) { + async.waterfall([ + function (waterCb) { + try { + transaction.id = library.logic.transaction.getId(transaction); + } catch (e) { + return setImmediate(waterCb, e.toString()); } - - var transaction = library.logic.transaction.dbRead(rows[i]); - blocks[block.id].transactions = blocks[block.id].transactions || {}; - - if (transaction) { - if (!blocks[block.id].transactions[transaction.id]) { - blocks[block.id].transactions[transaction.id] = transaction; + transaction.blockId = block.id; + return setImmediate(waterCb); + }, + function (waterCb) { + // Check if transaction is already in database, otherwise fork 2. + // DATABASE: read only + library.logic.transaction.checkConfirmed(transaction, function (err) { + if (err) { + // Fork: Transaction already confirmed. + modules.delegates.fork(block, 2); + // Undo the offending transaction. + // DATABASE: write + modules.transactions.undoUnconfirmed(transaction, function (err2) { + modules.transactions.removeUnconfirmedTransaction(transaction.id); + return setImmediate(waterCb, err2 || err); + }); + } else { + return setImmediate(waterCb); } - } + }); + }, + function (waterCb) { + // Get account from database if any (otherwise cold wallet). + // DATABASE: read only + modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, waterCb); + }, + function (sender, waterCb) { + // Check if transaction id valid against database state (mem_* tables). + // DATABASE: read only + library.logic.transaction.verify(transaction, sender, waterCb); } - } - - blocks = order.map(function (v) { - blocks[v].transactions = Object.keys(blocks[v].transactions).map(function (t) { - return blocks[v].transactions[t]; - }); - return blocks[v]; + ], function (err) { + return setImmediate(cb, err); }); - - return blocks; }; __private.applyTransaction = function (block, transaction, sender, cb) { @@ -464,7 +645,98 @@ __private.applyTransaction = function (block, transaction, sender, cb) { }); }; +__private.afterSave = function (block, cb) { + async.eachSeries(block.transactions, function (transaction, cb) { + return library.logic.transaction.afterSave(transaction, cb); + }, function (err) { + return setImmediate(cb, err); + }); +}; + +__private.popLastBlock = function (oldLastBlock, cb) { + library.balancesSequence.add(function (cb) { + self.loadBlocksPart({ id: oldLastBlock.previousBlock }, function (err, previousBlock) { + if (err || !previousBlock.length) { + return setImmediate(cb, err || 'previousBlock is null'); + } + previousBlock = previousBlock[0]; + + async.eachSeries(oldLastBlock.transactions.reverse(), function (transaction, cb) { + async.series([ + function (cb) { + modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { + if (err) { + return setImmediate(cb, err); + } + modules.transactions.undo(transaction, oldLastBlock, sender, cb); + }); + }, function (cb) { + modules.transactions.undoUnconfirmed(transaction, cb); + }, function (cb) { + return setImmediate(cb); + } + ], cb); + }, function (err) { + modules.rounds.backwardTick(oldLastBlock, previousBlock, function () { + __private.deleteBlock(oldLastBlock.id, function (err) { + if (err) { + return setImmediate(cb, err); + } + + return setImmediate(cb, null, previousBlock); + }); + }); + }); + }); + }, cb); +}; + +__private.deleteBlock = function (blockId, cb) { + library.db.none(sql.deleteBlock, {id: blockId}).then(function () { + return setImmediate(cb); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#deleteBlock error'); + }); +}; + +__private.recoverChain = function (cb) { + library.logger.warn('Chain comparison failed, starting recovery'); + self.deleteLastBlock(function (err, newLastBlock) { + if (err) { + library.logger.error('Recovery failed'); + } else { + library.logger.info('Recovery complete, new last block', newLastBlock.id); + } + return setImmediate(cb, err); + }); +}; + // Public methods +Blocks.prototype.count = function (cb) { + library.db.query(sql.countByRowId).then(function (rows) { + var res = rows.length ? rows[0].count : 0; + + return setImmediate(cb, null, res); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#count error'); + }); +}; + +Blocks.prototype.getLastBlock = function () { + if (__private.lastBlock) { + var epoch = constants.epochTime / 1000; + var lastBlockTime = epoch + __private.lastBlock.timestamp; + var currentTime = new Date().getTime() / 1000; + + __private.lastBlock.secondsAgo = Math.round((currentTime - lastBlockTime) * 1e2) / 1e2; + __private.lastBlock.fresh = (__private.lastBlock.secondsAgo < constants.blockReceiptTimeOut); + } + + return __private.lastBlock; +}; + Blocks.prototype.lastReceipt = function (lastReceipt) { if (lastReceipt) { __private.lastReceipt = lastReceipt; @@ -473,13 +745,16 @@ Blocks.prototype.lastReceipt = function (lastReceipt) { if (__private.lastReceipt) { var timeNow = new Date(); __private.lastReceipt.secondsAgo = Math.floor((timeNow.getTime() - __private.lastReceipt.getTime()) / 1000); - __private.lastReceipt.stale = (__private.lastReceipt.secondsAgo > 120); + __private.lastReceipt.secondsAgo = Math.round(__private.lastReceipt.secondsAgo * 1e2) / 1e2; + __private.lastReceipt.stale = (__private.lastReceipt.secondsAgo > constants.blockReceiptTimeOut); } return __private.lastReceipt; }; Blocks.prototype.getCommonBlock = function (peer, height, cb) { + var comparisionFailed = false; + async.waterfall([ function (waterCb) { __private.getIdSequence(height, function (err, res) { @@ -496,12 +771,22 @@ Blocks.prototype.getCommonBlock = function (peer, height, cb) { if (err || res.body.error) { return setImmediate(waterCb, err || res.body.error.toString()); } else if (!res.body.common) { + comparisionFailed = true; return setImmediate(waterCb, ['Chain comparison failed with peer:', peer.string, 'using ids:', ids].join(' ')); } else { return setImmediate(waterCb, null, res); } }); }, + function (res, waterCb) { + library.schema.validate(res.body.common, schema.getCommonBlock, function (err) { + if (err) { + return setImmediate(waterCb, err[0].message); + } else { + return setImmediate(waterCb, null, res); + } + }); + }, function (res, waterCb) { library.db.query(sql.getCommonBlock(res.body.common.previousBlock), { id: res.body.common.id, @@ -509,6 +794,7 @@ Blocks.prototype.getCommonBlock = function (peer, height, cb) { height: res.body.common.height }).then(function (rows) { if (!rows.length || !rows[0].count) { + comparisionFailed = true; return setImmediate(waterCb, ['Chain comparison failed with peer:', peer.string, 'using block:', JSON.stringify(res.body.common)].join(' ')); } else { return setImmediate(waterCb, null, res.body.common); @@ -519,25 +805,94 @@ Blocks.prototype.getCommonBlock = function (peer, height, cb) { }); } ], function (err, res) { - return setImmediate(cb, err, res); + if (comparisionFailed && modules.transport.poorConsensus()) { + return __private.recoverChain(cb); + } else { + return setImmediate(cb, err, res); + } }); }; -Blocks.prototype.count = function (cb) { - library.db.query(sql.countByRowId).then(function (rows) { - var res = rows.length ? rows[0].count : 0; +Blocks.prototype.loadBlocksFromPeer = function (peer, cb) { + var lastValidBlock = __private.lastBlock; - return setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#count error'); - }); -}; + peer = modules.peers.accept(peer); + library.logger.info('Loading blocks from: ' + peer.string); -Blocks.prototype.loadBlocksData = function (filter, options, cb) { - if (arguments.length < 3) { - cb = options; - options = {}; + function getFromPeer (seriesCb) { + modules.transport.getFromPeer(peer, { + method: 'GET', + api: '/blocks?lastBlockId=' + lastValidBlock.id + }, function (err, res) { + err = err || res.body.error; + if (err) { + return setImmediate(seriesCb, err); + } else { + return setImmediate(seriesCb, null, res.body.blocks); + } + }); + } + + function validateBlocks (blocks, seriesCb) { + var report = library.schema.validate(blocks, schema.loadBlocksFromPeer); + + if (!report) { + return setImmediate(seriesCb, 'Received invalid blocks data'); + } else { + return setImmediate(seriesCb, null, blocks); + } + } + + function processBlocks (blocks, seriesCb) { + if (blocks.length === 0) { + return setImmediate(seriesCb); + } + async.eachSeries(__private.readDbRows(blocks), function (block, eachSeriesCb) { + if (__private.cleanup) { + return setImmediate(eachSeriesCb); + } else { + return processBlock(block, eachSeriesCb); + } + }, function (err) { + return setImmediate(seriesCb, err); + }); + } + + function processBlock (block, seriesCb) { + self.processBlock(block, false, function (err) { + if (!err) { + lastValidBlock = block; + library.logger.info(['Block', block.id, 'loaded from:', peer.string].join(' '), 'height: ' + block.height); + } else { + var id = (block ? block.id : 'null'); + + library.logger.debug(['Block', id].join(' '), err.toString()); + if (block) { library.logger.debug('Block', block); } + + library.logger.warn(['Block', id, 'is not valid, ban 10 min'].join(' '), peer.string); + modules.peers.state(peer.ip, peer.port, 0, 600); + } + return seriesCb(err); + }, true); + } + + async.waterfall([ + getFromPeer, + validateBlocks, + processBlocks + ], function (err) { + if (err) { + return setImmediate(cb, 'Error loading blocks: ' + (err.message || err), lastValidBlock); + } else { + return setImmediate(cb, null, lastValidBlock); + } + }); +}; + +Blocks.prototype.loadBlocksData = function (filter, options, cb) { + if (arguments.length < 3) { + cb = options; + options = {}; } options = options || {}; @@ -585,563 +940,102 @@ Blocks.prototype.loadBlocksPart = function (filter, cb) { }); }; -Blocks.prototype.loadBlocksOffset = function (limit, offset, verify, cb) { - var newLimit = limit + (offset || 0); - var params = { limit: newLimit, offset: offset || 0 }; - - library.logger.debug('Loading blocks offset', {limit: limit, offset: offset, verify: verify}); - library.dbSequence.add(function (cb) { - library.db.query(sql.loadBlocksOffset, params).then(function (rows) { - var blocks = __private.readDbRows(rows); - - async.eachSeries(blocks, function (block, cb) { - if (__private.cleanup) { - return setImmediate(cb); - } - - library.logger.debug('Processing block', block.id); - if (verify && block.id !== genesisblock.block.id) { - // Sanity check of the block, if values are coherent. - // No access to database. - var check = self.verifyBlock(block); - - if (!check.verified) { - library.logger.error(['Block', block.id, 'verification failed'].join(' '), check.errors.join(', ')); - return setImmediate(cb, check.errors[0]); - } - } - if (block.id === genesisblock.block.id) { - __private.applyGenesisBlock(block, cb); - } else { - __private.applyBlock(block, false, cb, false); - } - __private.lastBlock = block; - }, function (err) { - return setImmediate(cb, err, __private.lastBlock); - }); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#loadBlocksOffset error'); - }); - }, cb); -}; - -Blocks.prototype.loadLastBlock = function (cb) { - library.dbSequence.add(function (cb) { - library.db.query(sql.loadLastBlock).then(function (rows) { - var block = __private.readDbRows(rows)[0]; - - block.transactions = block.transactions.sort(function (a, b) { - if (block.id === genesisblock.block.id) { - if (a.type === transactionTypes.VOTE) { - return 1; - } - } - - if (a.type === transactionTypes.SIGNATURE) { - return 1; - } - - return 0; - }); - - __private.lastBlock = block; - return setImmediate(cb, null, block); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#loadLastBlock error'); - }); - }, cb); -}; - -Blocks.prototype.getLastBlock = function () { - if (__private.lastBlock) { - var epoch = constants.epochTime / 1000; - var lastBlockTime = epoch + __private.lastBlock.timestamp; - var currentTime = new Date().getTime() / 1000; - - __private.lastBlock.secondsAgo = currentTime - lastBlockTime; - __private.lastBlock.fresh = (__private.lastBlock.secondsAgo < 120); - } - - return __private.lastBlock; -}; - -// Will return all possible errors that are intrinsic to the block. -// NO DATABASE access -Blocks.prototype.verifyBlock = function (block) { - var result = { verified: false, errors: [] }; - - try { - block.id = library.logic.block.getId(block); - } catch (e) { - result.errors.push(e.toString()); - } - - block.height = __private.lastBlock.height + 1; - - if (!block.previousBlock && block.height !== 1) { - result.errors.push('Invalid previous block'); - } else if (block.previousBlock !== __private.lastBlock.id) { - // Fork: Same height but different previous block id. - modules.delegates.fork(block, 1); - result.errors.push(['Invalid previous block:', block.previousBlock, 'expected:', __private.lastBlock.id].join(' ')); - } - - var expectedReward = __private.blockReward.calcReward(block.height); - - if (block.height !== 1 && expectedReward !== block.reward) { - result.errors.push(['Invalid block reward:', block.reward, 'expected:', expectedReward].join(' ')); - } - - var valid; - - try { - valid = library.logic.block.verifySignature(block); - } catch (e) { - result.errors.push(e.toString()); - } - - if (!valid) { - result.errors.push('Failed to verify block signature'); - } - - if (block.version > 0) { - result.errors.push('Invalid block version'); - } - - var blockSlotNumber = slots.getSlotNumber(block.timestamp); - var lastBlockSlotNumber = slots.getSlotNumber(__private.lastBlock.timestamp); - - if (blockSlotNumber > slots.getSlotNumber() || blockSlotNumber <= lastBlockSlotNumber) { - result.errors.push('Invalid block timestamp'); - } - - if (block.payloadLength > constants.maxPayloadLength) { - result.errors.push('Payload length is too high'); - } - - if (block.transactions.length !== block.numberOfTransactions) { - result.errors.push('Invalid number of transactions'); - } - - if (block.transactions.length > constants.maxTxsPerBlock) { - result.errors.push('Transactions length is too high'); - } - - // Checking if transactions of the block adds up to block values. - var totalAmount = 0, - totalFee = 0, - payloadHash = crypto.createHash('sha256'), - appliedTransactions = {}; - - for (var i in block.transactions) { - var transaction = block.transactions[i]; - var bytes; - - try { - bytes = library.logic.transaction.getBytes(transaction); - } catch (e) { - result.errors.push(e.toString()); - } - - if (appliedTransactions[transaction.id]) { - result.errors.push('Encountered duplicate transaction: ' + transaction.id); - } - - appliedTransactions[transaction.id] = transaction; - if (bytes) { payloadHash.update(bytes); } - totalAmount += transaction.amount; - totalFee += transaction.fee; - } - - if (payloadHash.digest().toString('hex') !== block.payloadHash) { - result.errors.push('Invalid payload hash'); - } - - if (totalAmount !== block.totalAmount) { - result.errors.push('Invalid total amount'); - } - - if (totalFee !== block.totalFee) { - result.errors.push('Invalid total fee'); - } - - result.verified = result.errors.length === 0; - return result; -}; - -// Apply the block, provided it has been verified. -__private.applyBlock = function (block, broadcast, cb, saveBlock) { - // Prevent shutdown during database writes. - __private.isActive = true; - - // Transactions to rewind in case of error. - var appliedTransactions = {}; - - // List of currrently unconfirmed transactions. - var unconfirmedTransactions; - - async.series({ - // Rewind any unconfirmed transactions before applying block. - // TODO: It should be possible to remove this call if we can guarantee that only this function is processing transactions atomically. Then speed should be improved further. - // TODO: Other possibility, when we rebuild from block chain this action should be moved out of the rebuild function. - undoUnconfirmedList: function (seriesCb) { - modules.transactions.undoUnconfirmedList(function (err, transactions) { - if (err) { - // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. - return process.exit(0); - } else { - unconfirmedTransactions = transactions; - return setImmediate(seriesCb); - } - }); - }, - // Apply transactions to unconfirmed mem_accounts fields. - applyUnconfirmed: function (seriesCb) { - async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { - // DATABASE write - modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { - // DATABASE: write - modules.transactions.applyUnconfirmed(transaction, sender, function (err) { - if (err) { - err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); - library.logger.error(err); - library.logger.error('Transaction', transaction); - return setImmediate(eachSeriesCb, err); - } - - appliedTransactions[transaction.id] = transaction; - - // Remove the transaction from the node queue, if it was present. - var index = unconfirmedTransactions.indexOf(transaction.id); - if (index >= 0) { - unconfirmedTransactions.splice(index, 1); - } - - return setImmediate(eachSeriesCb); - }); - }); - }, function (err) { - if (err) { - // Rewind any already applied unconfirmed transactions. - // Leaves the database state as per the previous block. - async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { - modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { - if (err) { - return setImmediate(eachSeriesCb, err); - } - // The transaction has been applied? - if (appliedTransactions[transaction.id]) { - // DATABASE: write - library.logic.transaction.undoUnconfirmed(transaction, sender, eachSeriesCb); - } else { - return setImmediate(eachSeriesCb); - } - }); - }, function (err) { - return setImmediate(seriesCb, err); - }); - } else { - return setImmediate(seriesCb); - } - }); - }, - // Block and transactions are ok. - // Apply transactions to confirmed mem_accounts fields. - applyConfirmed: function (seriesCb) { - async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { - modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, function (err, sender) { - if (err) { - err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); - library.logger.error(err); - library.logger.error('Transaction', transaction); - // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. - process.exit(0); - } - // DATABASE: write - modules.transactions.apply(transaction, block, sender, function (err) { - if (err) { - err = ['Failed to apply transaction:', transaction.id, '-', err].join(' '); - library.logger.error(err); - library.logger.error('Transaction', transaction); - // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. - process.exit(0); - } - // Transaction applied, removed from the unconfirmed list. - modules.transactions.removeUnconfirmedTransaction(transaction.id); - return setImmediate(eachSeriesCb); - }); - }); - }, function (err) { - return setImmediate(seriesCb, err); - }); - }, - // Optionally save the block to the database. - saveBlock: function (seriesCb) { - __private.lastBlock = block; - - if (saveBlock) { - // DATABASE: write - __private.saveBlock(block, function (err) { - if (err) { - library.logger.error('Failed to save block...'); - library.logger.error('Block', block); - // TODO: Send a numbered signal to be caught by forever to trigger a rebuild. - process.exit(0); - } - - library.logger.debug('Block applied correctly with ' + block.transactions.length + ' transactions'); - library.bus.message('newBlock', block, broadcast); - - // DATABASE write. Update delegates accounts - modules.rounds.tick(block, seriesCb); - }); - } else { - library.bus.message('newBlock', block, broadcast); - - // DATABASE write. Update delegates accounts - modules.rounds.tick(block, seriesCb); - } - }, - // Push back unconfirmed transactions list (minus the one that were on the block if applied correctly). - // TODO: See undoUnconfirmedList discussion above. - applyUnconfirmedList: function (seriesCb) { - // DATABASE write - modules.transactions.applyUnconfirmedList(unconfirmedTransactions, function (err) { - return setImmediate(seriesCb, err); - }); - }, - }, function (err) { - // Allow shutdown, database writes are finished. - __private.isActive = false; - - // Nullify large objects. - // Prevents memory leak during synchronisation. - appliedTransactions = unconfirmedTransactions = block = null; - - // Finish here if snapshotting. - if (err === 'Snapshot finished') { - library.logger.info(err); - process.emit('SIGTERM'); - } - - return setImmediate(cb, err); - }); -}; - -// Apply the genesis block, provided it has been verified. -// Shortcuting the unconfirmed/confirmed states. -__private.applyGenesisBlock = function (block, cb) { - block.transactions = block.transactions.sort(function (a, b) { - if (a.type === transactionTypes.VOTE) { - return 1; - } else { - return 0; - } - }); - async.eachSeries(block.transactions, function (transaction, cb) { - modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { - if (err) { - return setImmediate(cb, { - message: err, - transaction: transaction, - block: block - }); - } - __private.applyTransaction(block, transaction, sender, cb); - }); - }, function (err) { - if (err) { - // If genesis block is invalid, kill the node... - return process.exit(0); - } else { - __private.lastBlock = block; - modules.rounds.tick(__private.lastBlock, cb); - } - }); -}; - -// Main function to process a Block. -// * Verify the block looks ok -// * Verify the block is compatible with database state (DATABASE readonly) -// * Apply the block to database if both verifications are ok -Blocks.prototype.processBlock = function (block, broadcast, cb, saveBlock) { - if (__private.cleanup) { - return setImmediate(cb, 'Cleaning up'); - } else if (!__private.loaded) { - return setImmediate(cb, 'Blockchain is loading'); - } - - try { - block = library.logic.block.objectNormalize(block); - } catch (err) { - return setImmediate(cb, err); - } - - // Sanity check of the block, if values are coherent. - // No access to database - var check = self.verifyBlock(block); - - if (!check.verified) { - library.logger.error(['Block', block.id, 'verification failed'].join(' '), check.errors.join(', ')); - return setImmediate(cb, check.errors[0]); - } - - // Check if block id is already in the database (very low probability of hash collision). - // TODO: In case of hash-collision, to me it would be a special autofork... - // DATABASE: read only - library.db.query(sql.getBlockId, { id: block.id }).then(function (rows) { - if (rows.length > 0) { - return setImmediate(cb, ['Block', block.id, 'already exists'].join(' ')); - } - - // Check if block was generated by the right active delagate. Otherwise, fork 3. - // DATABASE: Read only to mem_accounts to extract active delegate list - modules.delegates.validateBlockSlot(block, function (err) { - if (err) { - modules.delegates.fork(block, 3); - return setImmediate(cb, err); - } - - // Check against the mem_* tables that we can perform the transactions included in the block. - async.eachSeries(block.transactions, function (transaction, cb) { - async.waterfall([ - function (cb) { - try { - transaction.id = library.logic.transaction.getId(transaction); - } catch (e) { - return setImmediate(cb, e.toString()); - } - transaction.blockId = block.id; - // Check if transaction is already in database, otherwise fork 2. - // DATABASE: read only - library.db.query(sql.getTransactionId, { id: transaction.id }).then(function (rows) { - if (rows.length > 0) { - modules.delegates.fork(block, 2); - return setImmediate(cb, ['Transaction', transaction.id, 'already exists'].join(' ')); - } else { - // Get account from database if any (otherwise cold wallet). - // DATABASE: read only - modules.accounts.getAccount({publicKey: transaction.senderPublicKey}, cb); - } - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#processBlock error'); - }); - }, - function (sender, cb) { - // Check if transaction id valid against database state (mem_* tables). - // DATABASE: read only - library.logic.transaction.verify(transaction, sender, cb); - } - ], - function (err) { - return setImmediate(cb, err); - }); - }, - function (err) { - if (err) { - return setImmediate(cb, err); - } else { - // The block and the transactions are OK i.e: - // * Block and transactions have valid values (signatures, block slots, etc...) - // * The check against database state passed (for instance sender has enough LSK, votes are under 101, etc...) - // We thus update the database with the transactions values, save the block and tick it. - __private.applyBlock(block, broadcast, cb, saveBlock); - } - }); - }); - }); -}; - -Blocks.prototype.simpleDeleteAfterBlock = function (blockId, cb) { - library.db.query(sql.simpleDeleteAfterBlock, {id: blockId}).then(function (res) { - return setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Blocks#simpleDeleteAfterBlock error'); - }); -}; - -Blocks.prototype.loadBlocksFromPeer = function (peer, cb) { - var lastValidBlock = __private.lastBlock; - - peer = modules.peers.inspect(peer); - library.logger.info('Loading blocks from: ' + peer.string); - - modules.transport.getFromPeer(peer, { - method: 'GET', - api: '/blocks?lastBlockId=' + lastValidBlock.id - }, function (err, res) { - if (err || res.body.error) { - return setImmediate(cb, err, lastValidBlock); - } - - var report = library.schema.validate(res.body.blocks, schema.loadBlocksFromPeer); - - if (!report) { - return setImmediate(cb, 'Received invalid blocks data', lastValidBlock); - } - - var blocks = __private.readDbRows(res.body.blocks); +Blocks.prototype.loadBlocksOffset = function (limit, offset, verify, cb) { + var newLimit = limit + (offset || 0); + var params = { limit: newLimit, offset: offset || 0 }; + + library.logger.debug('Loading blocks offset', {limit: limit, offset: offset, verify: verify}); + library.dbSequence.add(function (cb) { + library.db.query(sql.loadBlocksOffset, params).then(function (rows) { + var blocks = __private.readDbRows(rows); - if (blocks.length === 0) { - return setImmediate(cb, null, lastValidBlock); - } else { async.eachSeries(blocks, function (block, cb) { if (__private.cleanup) { return setImmediate(cb); } - self.processBlock(block, false, function (err) { - if (!err) { - lastValidBlock = block; - library.logger.info(['Block', block.id, 'loaded from:', peer.string].join(' '), 'height: ' + block.height); - } else { - var id = (block ? block.id : 'null'); - - library.logger.error(['Block', id].join(' '), err.toString()); - if (block) { library.logger.error('Block', block); } + library.logger.debug('Processing block', block.id); + if (verify && block.id !== genesisblock.block.id) { + // Sanity check of the block, if values are coherent. + // No access to database. + var check = self.verifyBlock(block); - library.logger.warn(['Block', id, 'is not valid, ban 60 min'].join(' '), peer.string); - modules.peers.state(peer.ip, peer.port, 0, 3600); + if (!check.verified) { + library.logger.error(['Block', block.id, 'verification failed'].join(' '), check.errors.join(', ')); + return setImmediate(cb, check.errors[0]); } - return cb(err); - }, true); - }, function (err) { - // Nullify large array of blocks. - // Prevents memory leak during synchronisation. - res = blocks = null; - - if (err) { - return setImmediate(cb, 'Error loading blocks: ' + (err.message || err), lastValidBlock); + } + if (block.id === genesisblock.block.id) { + __private.applyGenesisBlock(block, cb); } else { - return setImmediate(cb, null, lastValidBlock); + __private.applyBlock(block, false, cb, false); } + __private.lastBlock = block; + }, function (err) { + return setImmediate(cb, err, __private.lastBlock); }); - } - }); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#loadBlocksOffset error'); + }); + }, cb); }; -Blocks.prototype.deleteBlocksBefore = function (block, cb) { - var blocks = []; +Blocks.prototype.deleteLastBlock = function (cb) { + library.logger.warn('Deleting last block', __private.lastBlock); + + if (__private.lastBlock.height === 1) { + return setImmediate(cb, 'Can not delete genesis block'); + } - async.whilst( - function () { - return (block.height < __private.lastBlock.height); + async.series({ + backwardSwap: function (seriesCb) { + modules.rounds.directionSwap('backward', null, seriesCb); }, - function (next) { - blocks.unshift(__private.lastBlock); + popLastBlock: function (seriesCb) { __private.popLastBlock(__private.lastBlock, function (err, newLastBlock) { + if (err) { + library.logger.error('Error deleting last block', __private.lastBlock); + } + __private.lastBlock = newLastBlock; - next(err); + return setImmediate(seriesCb); }); }, - function (err) { - return setImmediate(cb, err, blocks); + forwardSwap: function (seriesCb) { + modules.rounds.directionSwap('forward', __private.lastBlock, seriesCb); } - ); + }, function (err) { + return setImmediate(cb, err, __private.lastBlock); + }); +}; + +Blocks.prototype.loadLastBlock = function (cb) { + library.dbSequence.add(function (cb) { + library.db.query(sql.loadLastBlock).then(function (rows) { + var block = __private.readDbRows(rows)[0]; + + block.transactions = block.transactions.sort(function (a, b) { + if (block.id === genesisblock.block.id) { + if (a.type === transactionTypes.VOTE) { + return 1; + } + } + + if (a.type === transactionTypes.SIGNATURE) { + return 1; + } + + return 0; + }); + + __private.lastBlock = block; + return setImmediate(cb, null, block); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#loadLastBlock error'); + }); + }, cb); }; Blocks.prototype.generateBlock = function (keypair, timestamp, cb) { @@ -1182,6 +1076,228 @@ Blocks.prototype.generateBlock = function (keypair, timestamp, cb) { }); }; +// Main function to process a Block. +// * Verify the block looks ok +// * Verify the block is compatible with database state (DATABASE readonly) +// * Apply the block to database if both verifications are ok +Blocks.prototype.processBlock = function (block, broadcast, cb, saveBlock) { + if (__private.cleanup) { + return setImmediate(cb, 'Cleaning up'); + } else if (!__private.loaded) { + return setImmediate(cb, 'Blockchain is loading'); + } + + async.series({ + normalizeBlock: function (seriesCb) { + try { + block = library.logic.block.objectNormalize(block); + } catch (err) { + return setImmediate(seriesCb, err); + } + + return setImmediate(seriesCb); + }, + verifyBlock: function (seriesCb) { + // Sanity check of the block, if values are coherent. + // No access to database + var check = self.verifyBlock(block); + + if (!check.verified) { + library.logger.error(['Block', block.id, 'verification failed'].join(' '), check.errors.join(', ')); + return setImmediate(seriesCb, check.errors[0]); + } + + return setImmediate(seriesCb); + }, + checkExists: function (seriesCb) { + // Check if block id is already in the database (very low probability of hash collision). + // TODO: In case of hash-collision, to me it would be a special autofork... + // DATABASE: read only + library.db.query(sql.getBlockId, { id: block.id }).then(function (rows) { + if (rows.length > 0) { + return setImmediate(seriesCb, ['Block', block.id, 'already exists'].join(' ')); + } else { + return setImmediate(seriesCb); + } + }); + }, + validateBlockSlot: function (seriesCb) { + // Check if block was generated by the right active delagate. Otherwise, fork 3. + // DATABASE: Read only to mem_accounts to extract active delegate list + modules.delegates.validateBlockSlot(block, function (err) { + if (err) { + // Fork: Delegate does not match calculated slot. + modules.delegates.fork(block, 3); + return setImmediate(seriesCb, err); + } else { + return setImmediate(seriesCb); + } + }); + }, + checkTransactions: function (seriesCb) { + // Check against the mem_* tables that we can perform the transactions included in the block. + async.eachSeries(block.transactions, function (transaction, eachSeriesCb) { + __private.checkTransaction(block, transaction, eachSeriesCb); + }, function (err) { + return setImmediate(seriesCb, err); + }); + } + }, function (err) { + if (err) { + return setImmediate(cb, err); + } else { + // The block and the transactions are OK i.e: + // * Block and transactions have valid values (signatures, block slots, etc...) + // * The check against database state passed (for instance sender has enough LSK, votes are under 101, etc...) + // We thus update the database with the transactions values, save the block and tick it. + __private.applyBlock(block, broadcast, cb, saveBlock); + } + }); +}; + +// Will return all possible errors that are intrinsic to the block. +// NO DATABASE access +Blocks.prototype.verifyBlock = function (block) { + var result = { verified: false, errors: [] }; + + try { + block.id = library.logic.block.getId(block); + } catch (e) { + result.errors.push(e.toString()); + } + + block.height = __private.lastBlock.height + 1; + + if (!block.previousBlock && block.height !== 1) { + result.errors.push('Invalid previous block'); + } else if (block.previousBlock !== __private.lastBlock.id) { + // Fork: Same height but different previous block id. + modules.delegates.fork(block, 1); + result.errors.push(['Invalid previous block:', block.previousBlock, 'expected:', __private.lastBlock.id].join(' ')); + } + + var expectedReward = __private.blockReward.calcReward(block.height); + + if (block.height !== 1 && expectedReward !== block.reward) { + result.errors.push(['Invalid block reward:', block.reward, 'expected:', expectedReward].join(' ')); + } + + var valid; + + try { + valid = library.logic.block.verifySignature(block); + } catch (e) { + result.errors.push(e.toString()); + } + + if (!valid) { + result.errors.push('Failed to verify block signature'); + } + + if (block.version > 0) { + result.errors.push('Invalid block version'); + } + + var blockSlotNumber = slots.getSlotNumber(block.timestamp); + var lastBlockSlotNumber = slots.getSlotNumber(__private.lastBlock.timestamp); + + if (blockSlotNumber > slots.getSlotNumber() || blockSlotNumber <= lastBlockSlotNumber) { + result.errors.push('Invalid block timestamp'); + } + + if (block.payloadLength > constants.maxPayloadLength) { + result.errors.push('Payload length is too high'); + } + + if (block.transactions.length !== block.numberOfTransactions) { + result.errors.push('Invalid number of transactions'); + } + + if (block.transactions.length > constants.maxTxsPerBlock) { + result.errors.push('Transactions length is too high'); + } + + // Checking if transactions of the block adds up to block values. + var totalAmount = 0, + totalFee = 0, + payloadHash = crypto.createHash('sha256'), + appliedTransactions = {}; + + for (var i in block.transactions) { + var transaction = block.transactions[i]; + var bytes; + + try { + bytes = library.logic.transaction.getBytes(transaction); + } catch (e) { + result.errors.push(e.toString()); + } + + if (appliedTransactions[transaction.id]) { + result.errors.push('Encountered duplicate transaction: ' + transaction.id); + } + + appliedTransactions[transaction.id] = transaction; + if (bytes) { payloadHash.update(bytes); } + totalAmount += transaction.amount; + totalFee += transaction.fee; + } + + if (payloadHash.digest().toString('hex') !== block.payloadHash) { + result.errors.push('Invalid payload hash'); + } + + if (totalAmount !== block.totalAmount) { + result.errors.push('Invalid total amount'); + } + + if (totalFee !== block.totalFee) { + result.errors.push('Invalid total fee'); + } + + result.verified = result.errors.length === 0; + return result; +}; + +Blocks.prototype.deleteBlocksBefore = function (block, cb) { + var blocks = []; + + async.series({ + backwardSwap: function (seriesCb) { + modules.rounds.directionSwap('backward', null, seriesCb); + }, + popBlocks: function (seriesCb) { + async.whilst( + function () { + return (block.height < __private.lastBlock.height); + }, + function (next) { + blocks.unshift(__private.lastBlock); + __private.popLastBlock(__private.lastBlock, function (err, newLastBlock) { + __private.lastBlock = newLastBlock; + next(err); + }); + }, + function (err) { + return setImmediate(seriesCb, err, blocks); + } + ); + }, + forwardSwap: function (seriesCb) { + modules.rounds.directionSwap('forward', __private.lastBlock, seriesCb); + } + }); +}; + +Blocks.prototype.deleteAfterBlock = function (blockId, cb) { + library.db.query(sql.deleteAfterBlock, {id: blockId}).then(function (res) { + return setImmediate(cb, null, res); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Blocks#deleteAfterBlock error'); + }); +}; + Blocks.prototype.sandboxApi = function (call, args, cb) { sandboxHelper.callMethod(shared, call, args, cb); }; @@ -1208,13 +1324,27 @@ Blocks.prototype.onReceiveBlock = function (block) { self.lastReceipt(new Date()); self.processBlock(block, true, cb, true); } else if (block.previousBlock !== __private.lastBlock.id && __private.lastBlock.height + 1 === block.height) { - // Fork: Same height but different previous block id + // Fork: Consecutive height but different previous block id. modules.delegates.fork(block, 1); - return setImmediate(cb, 'Fork'); + + if (block.previousBlock < __private.lastBlock.id) { + library.logger.info('Last block loses'); + return self.deleteLastBlock(cb); + } else { + library.logger.info('Newly received block wins'); + return setImmediate(cb); + } } else if (block.previousBlock === __private.lastBlock.previousBlock && block.height === __private.lastBlock.height && block.id !== __private.lastBlock.id) { - // Fork: Same height and previous block id, but different block id + // Fork: Same height and previous block id, but different block id. modules.delegates.fork(block, 5); - return setImmediate(cb, 'Fork'); + + if (block.id < __private.lastBlock.id) { + library.logger.info('Last block loses'); + return self.deleteLastBlock(cb); + } else { + library.logger.info('Newly received block wins'); + return setImmediate(cb); + } } else { return setImmediate(cb); } @@ -1237,7 +1367,7 @@ Blocks.prototype.cleanup = function (cb) { setImmediate(function nextWatch () { if (__private.isActive) { library.logger.info('Waiting for block processing to finish...'); - setTimeout(nextWatch, 1 * 1000); + setTimeout(nextWatch, 10000); } else { return setImmediate(cb); } @@ -1288,6 +1418,14 @@ shared.getBlocks = function (req, cb) { }); }; +shared.getBroadhash = function (req, cb) { + if (!__private.loaded) { + return setImmediate(cb, 'Blockchain is loading'); + } + + return setImmediate(cb, null, {broadhash: modules.system.getBroadhash()}); +}; + shared.getEpoch = function (req, cb) { if (!__private.loaded) { return setImmediate(cb, 'Blockchain is loading'); @@ -1325,7 +1463,7 @@ shared.getNethash = function (req, cb) { return setImmediate(cb, 'Blockchain is loading'); } - return setImmediate(cb, null, {nethash: library.config.nethash}); + return setImmediate(cb, null, {nethash: modules.system.getNethash()}); }; shared.getMilestone = function (req, cb) { @@ -1358,11 +1496,12 @@ shared.getStatus = function (req, cb) { } return setImmediate(cb, null, { + broadhash: modules.system.getBroadhash(), epoch: constants.epochTime, height: __private.lastBlock.height, fee: library.logic.block.calculateFee(), milestone: __private.blockReward.calcMilestone(__private.lastBlock.height), - nethash: library.config.nethash, + nethash: modules.system.getNethash(), reward: __private.blockReward.calcReward(__private.lastBlock.height), supply: __private.blockReward.calcSupply(__private.lastBlock.height) }); diff --git a/modules/dapps.js b/modules/dapps.js index e5f8fff92b1..a6df7a3171e 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -4,14 +4,17 @@ var _ = require('lodash'); var async = require('async'); var constants = require('../helpers/constants.js'); var crypto = require('crypto'); +var DApp = require('../logic/dapp.js'); var dappCategories = require('../helpers/dappCategories.js'); var dappTypes = require('../helpers/dappTypes.js'); var DecompressZip = require('decompress-zip'); var extend = require('extend'); var fs = require('fs'); var ip = require('ip'); +var InTransfer = require('../logic/inTransfer.js'); var npm = require('npm'); var OrderBy = require('../helpers/orderBy.js'); +var OutTransfer = require('../logic/outTransfer.js'); var path = require('path'); var popsicle = require('popsicle'); var rmdir = require('rimraf'); @@ -43,17 +46,14 @@ function DApps (cb, scope) { __private.attachApi(); - var DApp = require('../logic/dapp.js'); __private.assetTypes[transactionTypes.DAPP] = library.logic.transaction.attachAssetType( transactionTypes.DAPP, new DApp() ); - var InTransfer = require('../logic/inTransfer.js'); __private.assetTypes[transactionTypes.IN_TRANSFER] = library.logic.transaction.attachAssetType( transactionTypes.IN_TRANSFER, new InTransfer() ); - var OutTransfer = require('../logic/outTransfer.js'); __private.assetTypes[transactionTypes.OUT_TRANSFER] = library.logic.transaction.attachAssetType( transactionTypes.OUT_TRANSFER, new OutTransfer() ); @@ -158,7 +158,7 @@ __private.attachApi = function () { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }, function (err, transaction) { if (err) { @@ -319,14 +319,14 @@ __private.attachApi = function () { }); }); } else { - library.network.io.sockets.emit('dapps/change', {}); + library.network.io.sockets.emit('dapps/change', dapp); __private.loading[body.id] = false; return res.json({success: true, path: dappPath}); } }); } else { - library.network.io.sockets.emit('dapps/change', {}); + library.network.io.sockets.emit('dapps/change', dapp); __private.loading[body.id] = false; return res.json({success: true, path: dappPath}); @@ -409,7 +409,7 @@ __private.attachApi = function () { if (err) { return res.json({success: false, error: err}); } else { - library.network.io.sockets.emit('dapps/change', {}); + library.network.io.sockets.emit('dapps/change', dapp); return res.json({success: true}); } @@ -423,7 +423,7 @@ __private.attachApi = function () { if (err) { return res.json({success: false, error: err}); } else { - library.network.io.sockets.emit('dapps/change', {}); + library.network.io.sockets.emit('dapps/change', dapp); return res.json({success: true}); } @@ -508,7 +508,7 @@ __private.attachApi = function () { library.logger.error(err); return res.json({success: false, error: 'Failed to stop application'}); } else { - library.network.io.sockets.emit('dapps/change', {}); + library.network.io.sockets.emit('dapps/change', dapp); __private.launched[body.id] = false; return res.json({success: true}); } @@ -526,7 +526,7 @@ __private.attachApi = function () { library.network.app.use('/api/dapps', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -672,7 +672,7 @@ __private.getInstalledIds = function (cb) { if (err) { return setImmediate(cb, err); } else { - var regExp = new RegExp(/[0-9]{18,20}/); + var regExp = new RegExp(/[0-9]{1,20}/); ids = _.filter(ids, function (f) { return regExp.test(f.toString()); @@ -925,7 +925,7 @@ __private.createRoutes = function (dapp, cb) { library.network.app.use('/api/dapps/' + dapp.transactionId + '/api/', __private.routes[dapp.transactionId]); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); @@ -1045,11 +1045,12 @@ __private.createSandbox = function (dapp, params, cb) { } async.eachSeries(dappConfig.peers, function (peer, eachSeriesCb) { - modules.peers.addDapp({ + modules.peers.update({ ip: peer.ip, port: peer.port, dappid: dapp.transactionId - }, eachSeriesCb); + }); + return eachSeriesCb(); }, function (err) { if (err) { return setImmediate(cb, err); @@ -1205,7 +1206,7 @@ __private.addTransactions = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -1244,7 +1245,7 @@ __private.addTransactions = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } }, function (err, transaction) { @@ -1267,11 +1268,6 @@ __private.sendWithdrawal = function (req, cb) { var keypair = library.ed.makeKeypair(hash); var query = {}; - var isAddress = /^[0-9]{1,21}[L|l]$/g; - if (!isAddress.test(req.body.recipientId)) { - return setImmediate(cb, 'Invalid recipient'); - } - library.balancesSequence.add(function (cb) { if (req.body.multisigAccountPublicKey && req.body.multisigAccountPublicKey !== keypair.publicKey.toString('hex')) { modules.accounts.getAccount({publicKey: req.body.multisigAccountPublicKey}, function (err, account) { @@ -1333,7 +1329,7 @@ __private.sendWithdrawal = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -1374,7 +1370,7 @@ __private.sendWithdrawal = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } }, function (err, transaction) { diff --git a/modules/delegates.js b/modules/delegates.js index f363c97962d..c37681864ea 100644 --- a/modules/delegates.js +++ b/modules/delegates.js @@ -7,6 +7,7 @@ var BlockReward = require('../logic/blockReward.js'); var checkIpInList = require('../helpers/checkIpInList.js'); var constants = require('../helpers/constants.js'); var crypto = require('crypto'); +var Delegate = require('../logic/delegate.js'); var extend = require('extend'); var MilestoneBlocks = require('../helpers/milestoneBlocks.js'); var OrderBy = require('../helpers/orderBy.js'); @@ -22,7 +23,6 @@ var modules, library, self, __private = {}, shared = {}; __private.assetTypes = {}; __private.loaded = false; -__private.forging = false; __private.blockReward = new BlockReward(); __private.keypairs = {}; @@ -33,7 +33,6 @@ function Delegates (cb, scope) { __private.attachApi(); - var Delegate = require('../logic/delegate.js'); __private.assetTypes[transactionTypes.DELEGATE] = library.logic.transaction.attachAssetType( transactionTypes.DELEGATE, new Delegate() ); @@ -182,7 +181,7 @@ __private.attachApi = function () { library.network.app.use('/api/delegates', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -227,12 +226,7 @@ __private.getBlockSlotData = function (slot, height, cb) { __private.forge = function (cb) { if (!Object.keys(__private.keypairs).length) { library.logger.debug('No delegates enabled'); - return setImmediate(cb); - } - - if (!__private.forging) { - library.logger.debug('Forging disabled due to timeout'); - return setImmediate(cb); + return __private.loadDelegates(cb); } // When client is not loaded, is syncing or round is ticking @@ -246,40 +240,57 @@ __private.forge = function (cb) { var lastBlock = modules.blocks.getLastBlock(); if (currentSlot === slots.getSlotNumber(lastBlock.timestamp)) { - library.logger.debug('Last block within same delegate slot'); + library.logger.debug('Waiting for next delegate slot'); return setImmediate(cb); } __private.getBlockSlotData(currentSlot, lastBlock.height + 1, function (err, currentBlockData) { if (err || currentBlockData === null) { - library.logger.debug('Skipping delegate slot'); + library.logger.warn('Skipping delegate slot', err); return setImmediate(cb); } - library.sequence.add(function (cb) { - if (slots.getSlotNumber(currentBlockData.time) === slots.getSlotNumber()) { - modules.blocks.generateBlock(currentBlockData.keypair, currentBlockData.time, function (err) { - modules.blocks.lastReceipt(new Date()); - - library.logger.info([ - 'Forged new block id:', - modules.blocks.getLastBlock().id, - 'height:', modules.blocks.getLastBlock().height, - 'round:', modules.rounds.calc(modules.blocks.getLastBlock().height), - 'slot:', slots.getSlotNumber(currentBlockData.time), - 'reward:' + modules.blocks.getLastBlock().reward - ].join(' ')); + if (slots.getSlotNumber(currentBlockData.time) !== slots.getSlotNumber()) { + library.logger.debug('Delegate slot', slots.getSlotNumber()); + return setImmediate(cb); + } + library.sequence.add(function (cb) { + async.series({ + getPeers: function (seriesCb) { + return modules.transport.getPeers({limit: constants.maxPeers}, seriesCb); + }, + checkBroadhash: function (seriesCb) { + if (modules.transport.poorConsensus()) { + return setImmediate(seriesCb, ['Inadequate broadhash consensus', modules.transport.consensus(), '%'].join(' ')); + } else { + return setImmediate(seriesCb); + } + } + }, function (err) { + if (err) { + library.logger.warn(err); return setImmediate(cb, err); - }); - } else { - library.logger.debug('Delegate slot', slots.getSlotNumber()); - return setImmediate(cb); - } + } else { + return modules.blocks.generateBlock(currentBlockData.keypair, currentBlockData.time, cb); + } + }); }, function (err) { if (err) { - library.logger.error('Failed generate block within delegate slot', err); + library.logger.error('Failed to generate block within delegate slot', err); + } else { + modules.blocks.lastReceipt(new Date()); + + library.logger.info([ + 'Forged new block id:', + modules.blocks.getLastBlock().id, + 'height:', modules.blocks.getLastBlock().height, + 'round:', modules.rounds.calc(modules.blocks.getLastBlock().height), + 'slot:', slots.getSlotNumber(currentBlockData.time), + 'reward:' + modules.blocks.getLastBlock().reward + ].join(' ')); } + return setImmediate(cb); }); }); @@ -306,14 +317,12 @@ __private.checkDelegates = function (publicKey, votes, state, cb) { async.eachSeries(votes, function (action, cb) { var math = action[0]; - if (math !== '+' && math !== '-') { - return setImmediate(cb, 'Invalid math operator'); - } - if (math === '+') { additions += 1; - } else if (math === '+') { + } else if (math === '-') { removals += 1; + } else { + return setImmediate(cb, 'Invalid math operator'); } var publicKey = action.slice(1); @@ -362,10 +371,21 @@ __private.checkDelegates = function (publicKey, votes, state, cb) { }); }; -__private.loadMyDelegates = function (cb) { - var secrets = null; +__private.loadDelegates = function (cb) { + var secrets; + if (library.config.forging.secret) { - secrets = Array.isArray(library.config.forging.secret) ? library.config.forging.secret : [library.config.forging.secret]; + if (Array.isArray(library.config.forging.secret)) { + secrets = library.config.forging.secret; + } else { + secrets = [library.config.forging.secret]; + } + } + + if (!secrets || !secrets.length) { + return setImmediate(cb); + } else { + library.logger.info(['Loading', secrets.length, 'delegates from config'].join(' ')); } async.eachSeries(secrets, function (secret, cb) { @@ -379,15 +399,16 @@ __private.loadMyDelegates = function (cb) { } if (!account) { - return setImmediate(cb, 'Account ' + keypair.publicKey.toString('hex') + ' not found'); + return setImmediate(cb, ['Account with public key:', keypair.publicKey.toString('hex'), 'not found'].join(' ')); } if (account.isDelegate) { __private.keypairs[keypair.publicKey.toString('hex')] = keypair; - library.logger.info('Forging enabled on account: ' + account.address); + library.logger.info(['Forging enabled on account:', account.address].join(' ')); } else { - library.logger.warn('Delegate with this public key not found: ' + keypair.publicKey.toString('hex')); + library.logger.warn(['Account with public key:', keypair.publicKey.toString('hex'), 'is not a delegate'].join(' ')); } + return setImmediate(cb); }); }, cb); @@ -471,7 +492,7 @@ Delegates.prototype.getDelegates = function (query, cb) { }); }; -Delegates.prototype.checkDelegates = function (publicKey, votes, cb) { +Delegates.prototype.checkConfirmedDelegates = function (publicKey, votes, cb) { return __private.checkDelegates(publicKey, votes, 'confirmed', cb); }; @@ -486,15 +507,17 @@ Delegates.prototype.fork = function (block, cause) { cause: cause }); - self.disableForging('fork'); - - library.db.none(sql.insertFork, { + var fork = { delegatePublicKey: block.generatorPublicKey, blockTimestamp: block.timestamp, blockId: block.id, blockHeight: block.height, previousBlock: block.previousBlock, cause: cause + }; + + library.db.none(sql.insertFork, fork).then(function () { + library.network.io.sockets.emit('delegates/fork', fork); }); }; @@ -534,14 +557,16 @@ Delegates.prototype.onBind = function (scope) { Delegates.prototype.onBlockchainReady = function () { __private.loaded = true; - __private.loadMyDelegates(function nextForge (err) { + __private.loadDelegates(function nextForge (err) { if (err) { library.logger.error('Failed to load delegates', err); } - __private.toggleForgingOnReceipt(); - __private.forge(function () { - setTimeout(nextForge, 1000); + async.series([ + __private.forge, + modules.transactions.fillPool + ], function (err) { + return setTimeout(nextForge, 1000); }); }); }; @@ -551,45 +576,7 @@ Delegates.prototype.cleanup = function (cb) { return setImmediate(cb); }; -Delegates.prototype.enableForging = function () { - if (!__private.forging) { - library.logger.debug('Enabling forging'); - __private.forging = true; - } - - return __private.forging; -}; - -Delegates.prototype.disableForging = function (reason) { - if (__private.forging) { - library.logger.debug('Disabling forging due to:', reason); - __private.forging = false; - } - - return __private.forging; -}; - // Private -__private.toggleForgingOnReceipt = function () { - var lastReceipt = modules.blocks.lastReceipt(); - - // Enforce local forging if configured - if (!lastReceipt && library.config.forging.force) { - lastReceipt = modules.blocks.lastReceipt(new Date()); - } - - if (lastReceipt) { - var timeOut = Number(constants.forgingTimeOut); - - library.logger.debug('Last block received: ' + lastReceipt.secondsAgo + ' seconds ago'); - - if (lastReceipt.secondsAgo > timeOut) { - return self.disableForging('timeout'); - } else { - return self.enableForging(); - } - } -}; // Shared shared.getDelegate = function (req, cb) { @@ -828,7 +815,7 @@ shared.addDelegate = function (req, cb) { } catch (e) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -865,7 +852,7 @@ shared.addDelegate = function (req, cb) { } catch (e) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } }, function (err, transaction) { diff --git a/modules/loader.js b/modules/loader.js index 511172740bb..694bb409a97 100644 --- a/modules/loader.js +++ b/modules/loader.js @@ -14,11 +14,6 @@ require('colors'); // Private fields var modules, library, self, __private = {}, shared = {}; -__private.network = { - height: 0, // Network height - peers: [], // "Good" peers and with height close to network height -}; - __private.loaded = false; __private.isActive = false; __private.lastBlock = null; @@ -32,6 +27,7 @@ function Loader (cb, scope) { library = scope; self = this; + __private.initalize(); __private.attachApi(); __private.genesisBlock = __private.lastBlock = library.genesisblock; @@ -39,6 +35,13 @@ function Loader (cb, scope) { } // Private methods +__private.initalize = function () { + __private.network = { + height: 0, // Network height + peers: [], // "Good" peers and with height close to network height + }; +}; + __private.attachApi = function () { var router = new Router(); @@ -56,7 +59,7 @@ __private.attachApi = function () { library.network.app.use('/api/loader', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -78,73 +81,120 @@ __private.syncTrigger = function (turnOn) { }; __private.loadSignatures = function (cb) { - modules.transport.getFromRandomPeer({ - api: '/signatures', - method: 'GET' - }, function (err, res) { - if (err) { - return setImmediate(cb); - } - - library.schema.validate(res.body, schema.loadSignatures, function (err) { - if (err) { - return setImmediate(cb); - } + async.waterfall([ + function (waterCb) { + self.getNetwork(function (err, network) { + if (err) { + return setImmediate(waterCb, err); + } else { + var peer = network.peers[Math.floor(Math.random() * network.peers.length)]; + return setImmediate(waterCb, null, peer); + } + }); + }, + function (peer, waterCb) { + library.logger.log('Loading signatures from: ' + peer.string); + modules.transport.getFromPeer(peer, { + api: '/signatures', + method: 'GET' + }, function (err, res) { + if (err) { + return setImmediate(waterCb, err); + } else { + library.schema.validate(res.body, schema.loadSignatures, function (err) { + return setImmediate(waterCb, err, res.body.signatures); + }); + } + }); + }, + function (signatures, waterCb) { library.sequence.add(function (cb) { - async.eachSeries(res.body.signatures, function (signature, cb) { - async.eachSeries(signature.signatures, function (s, cb) { + async.eachSeries(signatures, function (signature, eachSeriesCb) { + async.eachSeries(signature.signatures, function (s, eachSeriesCb) { modules.multisignatures.processSignature({ signature: s, transaction: signature.transaction }, function (err) { - return setImmediate(cb); + return setImmediate(eachSeriesCb); }); - }, cb); + }, eachSeriesCb); }, cb); - }, cb); - }); + }, waterCb); + } + ], function (err) { + return setImmediate(cb, err); }); }; -__private.loadUnconfirmedTransactions = function (cb) { - modules.transport.getFromRandomPeer({ - api: '/transactions', - method: 'GET' - }, function (err, res) { - if (err) { - return setImmediate(cb); - } - - var report = library.schema.validate(res.body, schema.loadUnconfirmedTransactions); +__private.loadTransactions = function (cb) { + async.waterfall([ + function (waterCb) { + self.getNetwork(function (err, network) { + if (err) { + return setImmediate(waterCb, err); + } else { + var peer = network.peers[Math.floor(Math.random() * network.peers.length)]; + return setImmediate(waterCb, null, peer); + } + }); + }, + function (peer, waterCb) { + library.logger.log('Loading transactions from: ' + peer.string); - if (!report) { - return setImmediate(cb); - } + modules.transport.getFromPeer(peer, { + api: '/transactions', + method: 'GET' + }, function (err, res) { + if (err) { + return setImmediate(waterCb, err); + } - var peer = modules.peers.inspect(res.peer); - var transactions = res.body.transactions; + library.schema.validate(res.body, schema.loadTransactions, function (err) { + if (err) { + return setImmediate(waterCb, err[0].message); + } else { + return setImmediate(waterCb, null, peer, res.body.transactions); + } + }); + }); + }, + function (peer, transactions, waterCb) { + async.eachSeries(transactions, function (transaction, eachSeriesCb) { + var id = (transaction ? transactions.id : 'null'); - for (var i = 0; i < transactions.length; i++) { - var transaction = transactions[i]; - var id = (transaction ? transactions.id : 'null'); + try { + transaction = library.logic.transaction.objectNormalize(transaction); + } catch (e) { + library.logger.debug(['Transaction', id].join(' '), e.toString()); + if (transaction) { library.logger.debug('Transaction', transaction); } - try { - transaction = library.logic.transaction.objectNormalize(transaction); - } catch (e) { - library.logger.error(['Transaction', id].join(' '), e.toString()); - if (transaction) { library.logger.error('Transaction', transaction); } + library.logger.warn(['Transaction', id, 'is not valid, ban 10 min'].join(' '), peer.string); + modules.peers.state(peer.ip, peer.port, 0, 600); - library.logger.warn(['Transaction', id, 'is not valid, ban 60 min'].join(' '), peer.string); - modules.peers.state(peer.ip, peer.port, 0, 3600); + return setImmediate(eachSeriesCb, e); + } - return setImmediate(cb); - } + return setImmediate(eachSeriesCb); + }, function (err) { + return setImmediate(waterCb, err, transactions); + }); + }, + function (transactions, waterCb) { + async.eachSeries(transactions, function (transaction, eachSeriesCb) { + library.balancesSequence.add(function (cb) { + transaction.bundled = true; + modules.transactions.processUnconfirmedTransaction(transaction, false, cb); + }, function (err) { + if (err) { + library.logger.debug(err); + } + return setImmediate(eachSeriesCb); + }); + }, waterCb); } - - library.balancesSequence.add(function (cb) { - modules.transactions.receiveTransactions(transactions, cb); - }, cb); + ], function (err) { + return setImmediate(cb, err); }); }; @@ -156,49 +206,61 @@ __private.loadBlockChain = function () { verify = true; __private.total = count; - library.logic.account.removeTables(function (err) { - if (err) { - throw err; - } else { + async.series({ + removeTables: function (seriesCb) { + library.logic.account.removeTables(function (err) { + if (err) { + throw err; + } else { + return setImmediate(seriesCb); + } + }); + }, + createTables: function (seriesCb) { library.logic.account.createTables(function (err) { if (err) { throw err; } else { - async.until( - function () { - return count < offset; - }, function (cb) { - if (count > 1) { - library.logger.info('Rebuilding blockchain, current block height: ' + (offset + 1)); - } - modules.blocks.loadBlocksOffset(limit, offset, verify, function (err, lastBlock) { - if (err) { - return setImmediate(cb, err); - } - - offset = offset + limit; - __private.lastBlock = lastBlock; - - return setImmediate(cb); - }); - }, function (err) { - if (err) { - library.logger.error(err); - if (err.block) { - library.logger.error('Blockchain failed at: ' + err.block.height); - modules.blocks.simpleDeleteAfterBlock(err.block.id, function (err, res) { - library.logger.error('Blockchain clipped'); - library.bus.message('blockchainReady'); - }); - } - } else { - library.logger.info('Blockchain ready'); - library.bus.message('blockchainReady'); - } - } - ); + return setImmediate(seriesCb); } }); + }, + loadBlocksOffset: function (seriesCb) { + async.until( + function () { + return count < offset; + }, function (cb) { + if (count > 1) { + library.logger.info('Rebuilding blockchain, current block height: ' + (offset + 1)); + } + modules.blocks.loadBlocksOffset(limit, offset, verify, function (err, lastBlock) { + if (err) { + return setImmediate(cb, err); + } + + offset = offset + limit; + __private.lastBlock = lastBlock; + + return setImmediate(cb); + }); + }, function (err) { + return setImmediate(seriesCb, err); + } + ); + } + }, function (err) { + if (err) { + library.logger.error(err); + if (err.block) { + library.logger.error('Blockchain failed at: ' + err.block.height); + modules.blocks.deleteAfterBlock(err.block.id, function (err, res) { + library.logger.error('Blockchain clipped'); + library.bus.message('blockchainReady'); + }); + } + } else { + library.logger.info('Blockchain ready'); + library.bus.message('blockchainReady'); } }); } @@ -208,12 +270,14 @@ __private.loadBlockChain = function () { library.logger.warn(message); library.logger.warn('Recreating memory tables'); } - load(count); + + return load(count); } function checkMemTables (t) { var promises = [ t.one(sql.countBlocks), + t.query(sql.getGenesisBlock), t.one(sql.countMemAccounts), t.query(sql.getMemRounds) ]; @@ -221,17 +285,24 @@ __private.loadBlockChain = function () { return t.batch(promises); } - library.db.task(checkMemTables).then(function (results) { - var count = results[0].count; - var missed = !(results[1].count); - - library.logger.info('Blocks ' + count); - - var round = modules.rounds.calc(count); + function matchGenesisBlock (row) { + if (row) { + var matched = ( + row.id === __private.genesisBlock.block.id && + row.payloadHash.toString('hex') === __private.genesisBlock.block.payloadHash && + row.blockSignature.toString('hex') === __private.genesisBlock.block.blockSignature + ); + if (matched) { + library.logger.info('Genesis block matched with database'); + } else { + throw 'Failed to match genesis block with database'; + } + } + } + function verifySnapshot (count, round) { if (library.config.loading.snapshot !== undefined || library.config.loading.snapshot > 0) { library.logger.info('Snapshot mode enabled'); - verify = true; if (isNaN(library.config.loading.snapshot) || library.config.loading.snapshot >= round) { library.config.loading.snapshot = round; @@ -242,21 +313,38 @@ __private.loadBlockChain = function () { } library.logger.info('Snapshotting to end of round: ' + library.config.loading.snapshot); + return true; + } else { + return false; } + } + + library.db.task(checkMemTables).then(function (results) { + var count = results[0].count; + + library.logger.info('Blocks ' + count); + + var round = modules.rounds.calc(count); if (count === 1) { return reload(count); } + matchGenesisBlock(results[1][0]); + + verify = verifySnapshot(count, round); + if (verify) { return reload(count, 'Blocks verification enabled'); } + var missed = !(results[2].count); + if (missed) { return reload(count, 'Detected missed blocks in mem_accounts'); } - var unapplied = results[2].filter(function (row) { + var unapplied = results[3].filter(function (row) { return (row.round !== String(round)); }); @@ -294,8 +382,8 @@ __private.loadBlockChain = function () { }); }); }).catch(function (err) { - library.logger.error(err.stack); - return process.exit(0); + library.logger.error(err.stack || err); + return process.emit('exit'); }); }; @@ -365,42 +453,59 @@ __private.loadBlocksFromNetwork = function (cb) { }; __private.sync = function (cb) { - var transactions = modules.transactions.getUnconfirmedTransactionList(true); + library.logger.info('Starting sync'); __private.isActive = true; __private.syncTrigger(true); async.series({ - undoUnconfirmedList: function (cb) { + undoUnconfirmedList: function (seriesCb) { library.logger.debug('Undoing unconfirmed transactions before sync'); - return modules.transactions.undoUnconfirmedList(cb); + return modules.transactions.undoUnconfirmedList(seriesCb); + }, + getPeersBefore: function (seriesCb) { + library.logger.debug('Establishling broadhash consensus before sync'); + return modules.transport.getPeers({limit: constants.maxPeers}, seriesCb); + }, + loadBlocksFromNetwork: function (seriesCb) { + return __private.loadBlocksFromNetwork(seriesCb); + }, + updateSystem: function (seriesCb) { + return modules.system.update(seriesCb); }, - loadBlocksFromNetwork: function (cb) { - return __private.loadBlocksFromNetwork(cb); + getPeersAfter: function (seriesCb) { + library.logger.debug('Establishling broadhash consensus after sync'); + return modules.transport.getPeers({limit: constants.maxPeers}, seriesCb); }, - receiveTransactions: function (cb) { - library.logger.debug('Receiving unconfirmed transactions after sync'); - return modules.transactions.receiveTransactions(transactions, cb); + applyUnconfirmedList: function (seriesCb) { + library.logger.debug('Applying unconfirmed transactions after sync'); + return modules.transactions.applyUnconfirmedList(seriesCb); } }, function (err) { __private.isActive = false; __private.syncTrigger(false); __private.blocksToSync = 0; + library.logger.info('Finished sync'); return setImmediate(cb, err); }); }; // Given a list of peers with associated blockchain height (heights = {peer: peer, height: height}), we find a list of good peers (likely to sync with), then perform a histogram cut, removing peers far from the most common observed height. This is not as easy as it sounds, since the histogram has likely been made accross several blocks, therefore need to aggregate). __private.findGoodPeers = function (heights) { - // Removing unreachable peers + var lastBlockHeight = modules.blocks.getLastBlock().height; + heights = heights.filter(function (item) { + // Removing unreachable peers return item != null; + }).filter(function (item) { + // Remove heights below last block height + return item.height >= lastBlockHeight; }); - // Assuming that the node reached at least 10% of the network - if (heights.length < 10) { - return { height: 0, peers: [] }; + // No peers found + if (heights.length === 0) { + return {height: 0, peers: []}; } else { // Ordering the peers with descending height heights = heights.sort(function (a,b) { @@ -433,10 +538,57 @@ __private.findGoodPeers = function (heights) { item.peer.height = item.height; return item.peer; }); + + library.logger.debug('Good peers', peers); + return {height: height, peers: peers}; } }; +__private.getPeer = function (peer, cb) { + async.series({ + validatePeer: function (seriesCb) { + peer = modules.peers.accept(peer); + + library.schema.validate(peer, schema.getNetwork.peer, function (err) { + if (err) { + return setImmediate(seriesCb, ['Failed to validate peer:', err[0].path, err[0].message].join(' ')); + } else { + return setImmediate(seriesCb, null); + } + }); + }, + getHeight: function (seriesCb) { + modules.transport.getFromPeer(peer, { + api: '/height', + method: 'GET' + }, function (err, res) { + if (err) { + return setImmediate(seriesCb, 'Failed to get height from peer: ' + peer.string); + } else { + return setImmediate(seriesCb); + } + }); + }, + validateHeight: function (seriesCb) { + var heightIsValid = library.schema.validate(peer, schema.getNetwork.height); + + if (heightIsValid) { + library.logger.log(['Received height:', peer.height, 'from peer:', peer.string].join(' ')); + return setImmediate(seriesCb); + } else { + return setImmediate(seriesCb, 'Received invalid height from peer: ' + peer.string); + } + } + }, function (err) { + if (err) { + peer.height = null; + library.logger.error(err); + } + return setImmediate(cb, null, {peer: peer, height: peer.height}); + }); +}; + // Public methods // Rationale: @@ -444,71 +596,51 @@ __private.findGoodPeers = function (heights) { // - Then for each of them we grab the height of their blockchain. // - With this list we try to get a peer with sensibly good blockchain height (see __private.findGoodPeers for actual strategy). Loader.prototype.getNetwork = function (cb) { - // If __private.network.height is not so far (i.e. 1 round) from current node height, just return cached __private.network. - if (__private.network.height > 0 && Math.abs(__private.network.height - modules.blocks.getLastBlock().height) < 101) { + if (__private.network.height > 0 && Math.abs(__private.network.height - modules.blocks.getLastBlock().height) === 1) { return setImmediate(cb, null, __private.network); } - - // Fetch a list of 100 random peers - modules.transport.getFromRandomPeer({ - api: '/list', - method: 'GET' - }, function (err, res) { - if (err) { - library.logger.info('Failed to connect properly with network', err); - return setImmediate(cb, err); - } - - library.schema.validate(res.body, schema.getNetwork.peers, function (err) { - if (err) { - return setImmediate(cb, err); - } - - var peers = res.body.peers; - - library.logger.debug(['Received', res.body.peers.length, 'peers from'].join(' '), res.peer.string); - - // Validate each peer and then attempt to get its height - async.map(peers, function (peer, cb) { - var peerIsValid = library.schema.validate(modules.peers.inspect(peer), schema.getNetwork.peer); - - if (peerIsValid) { - modules.transport.getFromPeer(peer, { - api: '/height', - method: 'GET' - }, function (err, res) { - if (err) { - library.logger.error(err); - library.logger.warn('Failed to get height from peer', peer.string); - return setImmediate(cb); - } - - var heightIsValid = library.schema.validate(res.body, schema.getNetwork.height); - - if (heightIsValid) { - library.logger.info(['Received height:', res.body.height, 'from peer'].join(' '), peer.string); - return setImmediate(cb, null, {peer: peer, height: res.body.height}); - } else { - library.logger.warn('Received invalid height from peer', peer.string); - return setImmediate(cb); - } - }); + async.waterfall([ + function (waterCb) { + modules.transport.getFromRandomPeer({ + api: '/list', + method: 'GET' + }, function (err, res) { + if (err) { + return setImmediate(waterCb, err); } else { - library.logger.warn('Failed to validate peer', peer); - return setImmediate(cb); + return setImmediate(waterCb, null, res); } - }, function (err, heights) { - __private.network = __private.findGoodPeers(heights); + }); + }, + function (res, waterCb) { + library.schema.validate(res.body, schema.getNetwork.peers, function (err) { + var peers = modules.peers.acceptable(res.body.peers); if (err) { - return setImmediate(cb, err); - } else if (!__private.network.peers.length) { - return setImmediate(cb, 'Failed to find enough good peers to sync with'); + return setImmediate(waterCb, err); } else { - return setImmediate(cb, null, __private.network); + library.logger.log(['Received', peers.length, 'peers from'].join(' '), res.peer.string); + return setImmediate(waterCb, null, peers); } }); - }); + }, + function (peers, waterCb) { + async.map(peers, __private.getPeer, function (err, peers) { + return setImmediate(waterCb, err, peers); + }); + } + ], function (err, heights) { + if (err) { + return setImmediate(cb, err); + } + + __private.network = __private.findGoodPeers(heights); + + if (!__private.network.peers.length) { + return setImmediate(cb, 'Failed to find enough good peers'); + } else { + return setImmediate(cb, null, __private.network); + } }); }; @@ -522,53 +654,59 @@ Loader.prototype.sandboxApi = function (call, args, cb) { // Events Loader.prototype.onPeersReady = function () { - setImmediate(function nextLoadBlock () { - var lastReceipt = modules.blocks.lastReceipt(); + var retries = 5; - if (__private.loaded && !self.syncing() && (!lastReceipt || lastReceipt.stale)) { - library.logger.debug('Loading blocks from network'); - library.sequence.add(function (cb) { - __private.sync(cb); - }, function (err) { - if (err) { - library.logger.warn('Blocks timer', err); - } + setImmediate(function nextSeries () { + async.series({ + sync: function (seriesCb) { + var lastReceipt = modules.blocks.lastReceipt(); - setTimeout(nextLoadBlock, 10000); - }); - } else { - setTimeout(nextLoadBlock, 10000); - } - }); + if (__private.loaded && !self.syncing() && (!lastReceipt || lastReceipt.stale)) { + library.sequence.add(function (cb) { + async.retry(retries, __private.sync, cb); + }, function (err) { + if (err) { + library.logger.log('Sync timer', err); + } - setImmediate(function nextLoadUnconfirmedTransactions () { - if (__private.loaded && !self.syncing()) { - library.logger.debug('Loading unconfirmed transactions'); - __private.loadUnconfirmedTransactions(function (err) { - if (err) { - library.logger.warn('Unconfirmed transactions timer', err); + return setImmediate(seriesCb); + }); + } else { + return setImmediate(seriesCb); } + }, + loadTransactions: function (seriesCb) { + if (__private.loaded) { + async.retry(retries, __private.loadTransactions, function (err) { + if (err) { + library.logger.log('Unconfirmed transactions timer', err); + } - setTimeout(nextLoadUnconfirmedTransactions, 14000); - }); - } else { - setTimeout(nextLoadUnconfirmedTransactions, 14000); - } - }); - - setImmediate(function nextLoadSignatures () { - if (__private.loaded && !self.syncing()) { - library.logger.debug('Loading signatures'); - __private.loadSignatures(function (err) { - if (err) { - library.logger.warn('Signatures timer', err); + return setImmediate(seriesCb); + }); + } else { + return setImmediate(seriesCb); } + }, + loadSignatures: function (seriesCb) { + if (__private.loaded) { + async.retry(retries, __private.loadSignatures, function (err) { + if (err) { + library.logger.log('Signatures timer', err); + } - setTimeout(nextLoadSignatures, 14000); - }); - } else { - setTimeout(nextLoadSignatures, 14000); - } + return setImmediate(seriesCb); + }); + } else { + return setImmediate(seriesCb); + } + } + }, function (err) { + if (err) { + __private.initalize(); + } + return setTimeout(nextSeries, 10000); + }); }); }; diff --git a/modules/multisignatures.js b/modules/multisignatures.js index 91207feeea0..0a658323fc6 100644 --- a/modules/multisignatures.js +++ b/modules/multisignatures.js @@ -4,6 +4,7 @@ var async = require('async'); var crypto = require('crypto'); var extend = require('extend'); var genesisblock = null; +var Multisignature = require('../logic/multisignature.js'); var Router = require('../helpers/router.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/multisignatures.js'); @@ -24,7 +25,6 @@ function Multisignatures (cb, scope) { __private.attachApi(); - var Multisignature = require('../logic/multisignature.js'); __private.assetTypes[transactionTypes.MULTI] = library.logic.transaction.attachAssetType( transactionTypes.MULTI, new Multisignature() ); @@ -55,182 +55,44 @@ __private.attachApi = function () { library.network.app.use('/api/multisignatures', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; // Public methods -Multisignatures.prototype.sandboxApi = function (call, args, cb) { - sandboxHelper.callMethod(shared, call, args, cb); -}; - -// Events -Multisignatures.prototype.onBind = function (scope) { - modules = scope; - - __private.assetTypes[transactionTypes.MULTI].bind({ - modules: modules, library: library - }); -}; - -shared.getAccounts = function (req, cb) { - library.schema.validate(req.body, schema.getAccounts, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } - - library.db.one(sql.getAccounts, { publicKey: req.body.publicKey }).then(function (row) { - var addresses = Array.isArray(row.accountId) ? row.accountId : []; - - modules.accounts.getAccounts({ - address: { $in: addresses }, - sort: 'balance' - }, ['address', 'balance', 'multisignatures', 'multilifetime', 'multimin'], function (err, rows) { - if (err) { - return setImmediate(cb, err); - } - - async.eachSeries(rows, function (account, cb) { - var addresses = []; - for (var i = 0; i < account.multisignatures.length; i++) { - addresses.push(modules.accounts.generateAddressByPublicKey(account.multisignatures[i])); - } - - modules.accounts.getAccounts({ - address: { $in: addresses } - }, ['address', 'publicKey', 'balance'], function (err, multisigaccounts) { - if (err) { - return setImmediate(cb, err); - } - - account.multisigaccounts = multisigaccounts; - return setImmediate(cb); - }); - }, function (err) { - if (err) { - return setImmediate(cb, err); - } - - return setImmediate(cb, null, {accounts: rows}); - }); - }); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Multisignature#getAccounts error'); - }); - }); -}; - -// Shared -shared.pending = function (req, cb) { - library.schema.validate(req.body, schema.pending, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } - - var transactions = modules.transactions.getUnconfirmedTransactionList(); - transactions = transactions.filter(function (transaction) { - return transaction.senderPublicKey === req.body.publicKey; - }); - - var pendings = []; - async.eachSeries(transactions, function (transaction, cb) { - var signed = false; - - if (transaction.signatures && transaction.signatures.length > 0) { - var verify = false; - - for (var i in transaction.signatures) { - var signature = transaction.signatures[i]; - - try { - verify = library.logic.transaction.verifySignature(transaction, req.body.publicKey, transaction.signatures[i]); - } catch (e) { - library.logger.error(e.stack); - verify = false; - } - - if (verify) { - break; - } - } +Multisignatures.prototype.processSignature = function (tx, cb) { + var transaction = modules.transactions.getMultisignatureTransaction(tx.transaction); - if (verify) { - signed = true; - } - } + function done (cb) { + library.balancesSequence.add(function (cb) { + var transaction = modules.transactions.getMultisignatureTransaction(tx.transaction); - if (!signed && transaction.senderPublicKey === req.body.publicKey) { - signed = true; + if (!transaction) { + return setImmediate(cb, 'Transaction not found'); } modules.accounts.getAccount({ - publicKey: transaction.senderPublicKey + address: transaction.senderId }, function (err, sender) { if (err) { return setImmediate(cb, err); - } - - if (!sender) { + } else if (!sender) { return setImmediate(cb, 'Sender not found'); - } - - var hasUnconfirmed = ( - sender.publicKey === req.body.publicKey && Array.isArray(sender.u_multisignatures) && sender.u_multisignatures.length > 0 - ); - - var belongsToUnconfirmed = ( - Array.isArray(sender.u_multisignatures) && sender.u_multisignatures.indexOf(req.body.publicKey) >= 0 - ); - - var belongsToConfirmed = ( - Array.isArray(sender.multisignatures) && sender.multisignatures.indexOf(req.body.publicKey) >= 0 - ); - - if (hasUnconfirmed || belongsToUnconfirmed || belongsToConfirmed) { - var min = sender.u_multimin || sender.multimin; - var lifetime = sender.u_multilifetime || sender.multilifetime; - var signatures = sender.u_multisignatures || []; + } else { + transaction.signatures = transaction.signatures || []; + transaction.signatures.push(tx.signature); + transaction.ready = Multisignature.prototype.ready(transaction, sender); - pendings.push({ - max: signatures.length, - min: min, - lifetime: lifetime, - signed: signed, - transaction: transaction - }); + library.bus.message('signature', {transaction: tx.transaction, signature: tx.signature}, true); + return setImmediate(cb); } - - return setImmediate(cb); }); - }, function () { - return setImmediate(cb, null, {transactions: pendings}); - }); - }); -}; - -Multisignatures.prototype.processSignature = function (tx, cb) { - var transaction = modules.transactions.getUnconfirmedTransaction(tx.transaction); - - function done (cb) { - library.balancesSequence.add(function (cb) { - var transaction = modules.transactions.getUnconfirmedTransaction(tx.transaction); - - if (!transaction) { - return setImmediate(cb, 'Transaction not found'); - } - - transaction.signatures = transaction.signatures || []; - transaction.signatures.push(tx.signature); - library.bus.message('signature', transaction, true); - - return setImmediate(cb); }, cb); } if (!transaction) { - return setImmediate(cb, 'Missing transaction'); + return setImmediate(cb, 'Transaction not found'); } if (transaction.type === transactionTypes.MULTI) { @@ -296,164 +158,370 @@ Multisignatures.prototype.processSignature = function (tx, cb) { return setImmediate(cb, 'Failed to verify signature'); } - library.network.io.sockets.emit('multisignatures/signature/change', {}); + library.network.io.sockets.emit('multisignatures/signature/change', transaction); return done(cb); }); } }; -shared.sign = function (req, cb) { - library.schema.validate(req.body, schema.sign, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } - - var transaction = modules.transactions.getUnconfirmedTransaction(req.body.transactionId); - - if (!transaction) { - return setImmediate(cb, 'Transaction not found'); - } +Multisignatures.prototype.sandboxApi = function (call, args, cb) { + sandboxHelper.callMethod(shared, call, args, cb); +}; - var hash = crypto.createHash('sha256').update(req.body.secret, 'utf8').digest(); - var keypair = library.ed.makeKeypair(hash); +// Events +Multisignatures.prototype.onBind = function (scope) { + modules = scope; - if (req.body.publicKey) { - if (keypair.publicKey.toString('hex') !== req.body.publicKey) { - return setImmediate(cb, 'Invalid passphrase'); - } - } + __private.assetTypes[transactionTypes.MULTI].bind({ + modules: modules, library: library + }); +}; - var sign = library.logic.transaction.multisign(keypair, transaction); +// Shared +shared.getAccounts = function (req, cb) { + var scope = {}; - function done (cb) { - library.balancesSequence.add(function (cb) { - var transaction = modules.transactions.getUnconfirmedTransaction(req.body.transactionId); + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.getAccounts, function (err) { + if (err) { + return setImmediate(seriesCb, err[0].message); + } else { + return setImmediate(seriesCb); + } + }); + }, + getAccountIds: function (seriesCb) { + library.db.one(sql.getAccountIds, { publicKey: req.body.publicKey }).then(function (row) { + scope.accountIds = Array.isArray(row.accountIds) ? row.accountIds : []; + return setImmediate(seriesCb); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(seriesCb, 'Multisignature#getAccountIds error'); + }); + }, + getAccounts: function (seriesCb) { + modules.accounts.getAccounts({ + address: { $in: scope.accountIds }, + sort: 'balance' + }, ['address', 'balance', 'multisignatures', 'multilifetime', 'multimin'], function (err, accounts) { + if (err) { + return setImmediate(seriesCb, err); + } else { + scope.accounts = accounts; + return setImmediate(seriesCb); + } + }); + }, + buildAccounts: function (seriesCb) { + async.eachSeries(scope.accounts, function (account, eachSeriesCb) { + var addresses = []; - if (!transaction) { - return setImmediate(cb, 'Transaction not found'); + for (var i = 0; i < account.multisignatures.length; i++) { + addresses.push(modules.accounts.generateAddressByPublicKey(account.multisignatures[i])); } - transaction.signatures = transaction.signatures || []; - transaction.signatures.push(sign); + modules.accounts.getAccounts({ + address: { $in: addresses } + }, ['address', 'publicKey', 'balance'], function (err, multisigaccounts) { + if (err) { + return setImmediate(eachSeriesCb, err); + } + + account.multisigaccounts = multisigaccounts; + return setImmediate(eachSeriesCb); + }); + }, seriesCb); + } + }, function (err) { + if (err) { + return setImmediate(cb, err); + } else { + return setImmediate(cb, null, {accounts: scope.accounts}); + } + }); +}; - library.bus.message('signature', { - signature: sign, - transaction: transaction.id - }, true); +shared.pending = function (req, cb) { + var scope = { pending: [] }; - return setImmediate(cb); - }, function (err) { + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.pending, function (err) { if (err) { - return setImmediate(cb, err); + return setImmediate(seriesCb, err[0].message); + } else { + return setImmediate(seriesCb); } - - return setImmediate(cb, null, {transactionId: transaction.id}); }); - } + }, + getTransactionList: function (seriesCb) { + scope.transactions = modules.transactions.getMultisignatureTransactionList(false, false); + scope.transactions = scope.transactions.filter(function (transaction) { + return transaction.senderPublicKey === req.body.publicKey; + }); - if (transaction.type === transactionTypes.MULTI) { - if (transaction.asset.multisignature.keysgroup.indexOf('+' + keypair.publicKey.toString('hex')) === -1 || (transaction.signatures && transaction.signatures.indexOf(sign.toString('hex')) !== -1)) { - return setImmediate(cb, 'Permission to sign transaction denied'); - } + return setImmediate(seriesCb); + }, + buildTransactions: function (seriesCb) { + async.eachSeries(scope.transactions, function (transaction, eachSeriesCb) { + var signed = false; - library.network.io.sockets.emit('multisignatures/signature/change', {}); - return done(cb); - } else { - modules.accounts.getAccount({ - address: transaction.senderId - }, function (err, account) { - if (err) { - return setImmediate(cb, err); - } + if (transaction.signatures && transaction.signatures.length > 0) { + var verify = false; - if (!account) { - return setImmediate(cb, 'Sender not found'); - } + for (var i in transaction.signatures) { + var signature = transaction.signatures[i]; - if (!transaction.requesterPublicKey) { - if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) { - return setImmediate(cb, 'Permission to sign transaction denied'); + try { + verify = library.logic.transaction.verifySignature(transaction, req.body.publicKey, transaction.signatures[i]); + } catch (e) { + library.logger.error(e.stack); + verify = false; + } + + if (verify) { + break; + } } - } else { - if (account.publicKey !== keypair.publicKey.toString('hex') || transaction.senderPublicKey !== keypair.publicKey.toString('hex')) { - return setImmediate(cb, 'Permission to sign transaction denied'); + + if (verify) { + signed = true; } } - if (transaction.signatures && transaction.signatures.indexOf(sign) !== -1) { - return setImmediate(cb, 'Permission to sign transaction denied'); + if (!signed && transaction.senderPublicKey === req.body.publicKey) { + signed = true; } - library.network.io.sockets.emit('multisignatures/signature/change', {}); - return done(cb); + modules.accounts.getAccount({ + publicKey: transaction.senderPublicKey + }, function (err, sender) { + if (err) { + return setImmediate(cb, err); + } + + if (!sender) { + return setImmediate(cb, 'Sender not found'); + } + + var min = sender.u_multimin || sender.multimin; + var lifetime = sender.u_multilifetime || sender.multilifetime; + var signatures = sender.u_multisignatures || []; + + scope.pending.push({ + max: signatures.length, + min: min, + lifetime: lifetime, + signed: signed, + transaction: transaction + }); + + return setImmediate(eachSeriesCb); + }); + }, function (err) { + return setImmediate(seriesCb, err); }); } + }, function (err) { + return setImmediate(cb, err, {transactions: scope.pending}); }); }; -shared.addMultisignature = function (req, cb) { - library.schema.validate(req.body, schema.addMultisignature, function (err) { - if (err) { - return setImmediate(cb, err[0].message); +shared.sign = function (req, cb) { + var scope = {}; + + function checkGroupPermisions (cb) { + var permissionDenied = ( + scope.transaction.asset.multisignature.keysgroup.indexOf('+' + scope.keypair.publicKey.toString('hex')) === -1 + ); + + if (permissionDenied) { + return setImmediate(cb, 'Permission to sign transaction denied'); } - var hash = crypto.createHash('sha256').update(req.body.secret, 'utf8').digest(); - var keypair = library.ed.makeKeypair(hash); + var alreadySigned = ( + Array.isArray(scope.transaction.signatures) && + scope.transaction.signatures.indexOf(scope.signature.toString('hex')) !== -1 + ); - if (req.body.publicKey) { - if (keypair.publicKey.toString('hex') !== req.body.publicKey) { - return setImmediate(cb, 'Invalid passphrase'); - } + if (alreadySigned) { + return setImmediate(cb, 'Transaction already signed'); } - library.balancesSequence.add(function (cb) { - modules.accounts.setAccountAndGet({publicKey: keypair.publicKey.toString('hex')}, function (err, account) { - if (err) { - return setImmediate(cb, err); + return setImmediate(cb); + } + + function checkTransactionPermissions (cb) { + var permissionDenied = true; + + if (!scope.transaction.requesterPublicKey) { + permissionDenied = ( + (scope.sender.multisignatures.indexOf(scope.keypair.publicKey.toString('hex')) === -1) + ); + } else { + permissionDenied = ( + (scope.sender.publicKey !== scope.keypair.publicKey.toString('hex') || (scope.transaction.senderPublicKey !== scope.keypair.publicKey.toString('hex'))) + ); + } + + if (permissionDenied) { + return setImmediate(cb, 'Permission to sign transaction denied'); + } + + var alreadySigned = (scope.transaction.signatures && scope.transaction.signatures.indexOf(scope.signature) !== -1); + + if (alreadySigned) { + return setImmediate(cb, 'Transaction already signed'); + } + + return setImmediate(cb); + } + + library.balancesSequence.add(function (cb) { + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.sign, function (err) { + if (err) { + return setImmediate(seriesCb, err[0].message); + } else { + return setImmediate(seriesCb); + } + }); + }, + signTransaction: function (seriesCb) { + scope.transaction = modules.transactions.getMultisignatureTransaction(req.body.transactionId); + + if (!scope.transaction) { + return setImmediate(seriesCb, 'Transaction not found'); } - if (!account || !account.publicKey) { - return setImmediate(cb, 'Account not found'); + scope.hash = crypto.createHash('sha256').update(req.body.secret, 'utf8').digest(); + scope.keypair = library.ed.makeKeypair(scope.hash); + + if (req.body.publicKey) { + if (scope.keypair.publicKey.toString('hex') !== req.body.publicKey) { + return setImmediate(seriesCb, 'Invalid passphrase'); + } } - if (account.secondSignature && !req.body.secondSecret) { - return setImmediate(cb, 'Invalid second passphrase'); + scope.signature = library.logic.transaction.multisign(scope.keypair, scope.transaction); + return setImmediate(seriesCb); + }, + getAccount: function (seriesCb) { + modules.accounts.getAccount({ + address: scope.transaction.senderId + }, function (err, sender) { + if (err) { + return setImmediate(seriesCb, err); + } else if (!sender) { + return setImmediate(seriesCb, 'Sender not found'); + } else { + scope.sender = sender; + return setImmediate(seriesCb); + } + }); + }, + checkPermissions: function (seriesCb) { + if (scope.transaction.type === transactionTypes.MULTI) { + return checkGroupPermisions(seriesCb); + } else { + return checkTransactionPermissions(seriesCb); } + } + }, function (err) { + if (err) { + return setImmediate(cb, err); + } - var secondKeypair = null; + var transaction = modules.transactions.getMultisignatureTransaction(req.body.transactionId); - if (account.secondSignature) { - var secondHash = crypto.createHash('sha256').update(req.body.secondSecret, 'utf8').digest(); - secondKeypair = library.ed.makeKeypair(secondHash); - } + if (!transaction) { + return setImmediate(cb, 'Transaction not found'); + } - var transaction; - - try { - transaction = library.logic.transaction.create({ - type: transactionTypes.MULTI, - sender: account, - keypair: keypair, - secondKeypair: secondKeypair, - min: req.body.min, - keysgroup: req.body.keysgroup, - lifetime: req.body.lifetime - }); - } catch (e) { - return setImmediate(cb, e.toString()); + transaction.signatures = transaction.signatures || []; + transaction.signatures.push(scope.signature); + transaction.ready = Multisignature.prototype.ready(transaction, scope.sender); + + library.bus.message('signature', {transaction: transaction.id, signature: scope.signature}, true); + library.network.io.sockets.emit('multisignatures/signature/change', transaction); + + return setImmediate(cb, null, {transactionId: transaction.id}); + }); + }, cb); +}; + +shared.addMultisignature = function (req, cb) { + var scope = {}; + + library.balancesSequence.add(function (cb) { + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.addMultisignature, function (err) { + if (err) { + return setImmediate(seriesCb, err[0].message); + } else { + return setImmediate(seriesCb); + } + }); + }, + addMultisignature: function (seriesCb) { + scope.hash = crypto.createHash('sha256').update(req.body.secret, 'utf8').digest(); + scope.keypair = library.ed.makeKeypair(scope.hash); + + if (req.body.publicKey) { + if (scope.keypair.publicKey.toString('hex') !== req.body.publicKey) { + return setImmediate(seriesCb, 'Invalid passphrase'); + } } - modules.transactions.receiveTransactions([transaction], cb); - }); - }, function (err, transaction) { + modules.accounts.setAccountAndGet({publicKey: scope.keypair.publicKey.toString('hex')}, function (err, account) { + if (err) { + return setImmediate(seriesCb, err); + } + + if (!account || !account.publicKey) { + return setImmediate(seriesCb, 'Account not found'); + } + + if (account.secondSignature && !req.body.secondSecret) { + return setImmediate(seriesCb, 'Invalid second passphrase'); + } + + scope.secondKeypair = null; + + if (account.secondSignature) { + scope.secondHash = crypto.createHash('sha256').update(req.body.secondSecret, 'utf8').digest(); + scope.secondKeypair = library.ed.makeKeypair(scope.secondHash); + } + + try { + scope.transaction = library.logic.transaction.create({ + type: transactionTypes.MULTI, + sender: account, + keypair: scope.keypair, + secondKeypair: scope.secondKeypair, + min: req.body.min, + keysgroup: req.body.keysgroup, + lifetime: req.body.lifetime + }); + } catch (e) { + return setImmediate(seriesCb, e.toString()); + } + + modules.transactions.receiveTransactions([scope.transaction], true, seriesCb); + }); + } + }, function (err) { if (err) { return setImmediate(cb, err); + } else { + library.network.io.sockets.emit('multisignatures/change', scope.transaction); + return setImmediate(cb, null, {transactionId: scope.transaction.id}); } - - library.network.io.sockets.emit('multisignatures/change', {}); - return setImmediate(cb, null, {transactionId: transaction[0].id}); }); - }); + }, cb); }; // Export diff --git a/modules/peers.js b/modules/peers.js index 2cc0f376d3f..7027200e81c 100644 --- a/modules/peers.js +++ b/modules/peers.js @@ -2,11 +2,14 @@ var _ = require('lodash'); var async = require('async'); +var constants = require('../helpers/constants.js'); var extend = require('extend'); var fs = require('fs'); var ip = require('ip'); var OrderBy = require('../helpers/orderBy.js'); var path = require('path'); +var Peer = require('../logic/peer.js'); +var PeerSweeper = require('../logic/peerSweeper.js'); var Router = require('../helpers/router.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/peers.js'); @@ -26,6 +29,7 @@ function Peers (cb, scope) { self = this; __private.attachApi(); + __private.sweeper = new PeerSweeper({ library: library, sql: sql }); setImmediate(cb, null, self); } @@ -52,72 +56,88 @@ __private.attachApi = function () { library.network.app.use('/api/peers', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; __private.updatePeersList = function (cb) { - modules.transport.getFromRandomPeer({ - api: '/list', - method: 'GET' - }, function (err, res) { - if (err) { - return setImmediate(cb); - } + function getFromRandomPeer (waterCb) { + modules.transport.getFromRandomPeer({ + api: '/list', + method: 'GET' + }, function (err, res) { + return setImmediate(waterCb, err, res); + }); + } + function validatePeersList (res, waterCb) { library.schema.validate(res.body, schema.updatePeersList.peers, function (err) { - if (err) { - return setImmediate(cb); - } - - // Removing nodes not behaving well - library.logger.debug('Removed peers: ' + removed.length); - var peers = res.body.peers.filter(function (peer) { - return removed.indexOf(peer.ip); - }); + return setImmediate(waterCb, err, res.body.peers); + }); + } - // Update only a subset of the peers to decrease the noise on the network. - // Default is 20 peers. To be fined tuned. Node gets checked by a peer every 3s on average. - // Maybe increasing schedule (every 60s right now). - var maxUpdatePeers = Math.floor(library.config.peers.options.maxUpdatePeers) || 20; - if (peers.length > maxUpdatePeers) { - peers = peers.slice(0, maxUpdatePeers); - } + function pickPeers (peers, waterCb) { + // Protect removed nodes from overflow + if (removed.length > 100) { + removed = []; + } - // Drop one random peer from removed array to give them a chance. - // This mitigates the issue that a node could be removed forever if it was offline for long. - // This is not harmful for the node, but prevents network from shrinking, increasing noise. - // To fine tune: decreasing random value threshold -> reduce noise. - if (Math.random() < 0.5) { // Every 60/0.5 = 120s - // Remove the first element, - // i.e. the one that have been placed first. - removed.shift(); - removed.pop(); - } + library.logger.debug('Removed peers: ' + removed.length); - library.logger.debug(['Picked', peers.length, 'of', res.body.peers.length, 'peers'].join(' ')); + // Pick peers + // + // * Removing unacceptable peers + // * Removing nodes not behaving well + var picked = self.acceptable(peers).filter(function (peer) { + return removed.indexOf(peer.ip); + }); - async.eachLimit(peers, 2, function (peer, cb) { - peer = self.inspect(peer); + // Drop one random peer from removed array to give them a chance. + // This mitigates the issue that a node could be removed forever if it was offline for long. + // This is not harmful for the node, but prevents network from shrinking, increasing noise. + // To fine tune: decreasing random value threshold -> reduce noise. + if (Math.random() < 0.5) { // Every 60/0.5 = 120s + // Remove the first element, + // i.e. the one that have been placed first. + removed.shift(); + removed.pop(); + } - library.schema.validate(peer, schema.updatePeersList.peer, function (err) { - if (err) { - err.forEach(function (e) { - library.logger.error(['Rejecting invalid peer:', peer.ip, e.path, e.message].join(' ')); - }); + library.logger.debug(['Picked', picked.length, 'of', peers.length, 'peers'].join(' ')); + return setImmediate(waterCb, null, picked); + } - return setImmediate(cb); - } else { - library.dbSequence.add(function (cb) { - self.update(peer, cb); - }); + function updatePeers (peers, waterCb) { + async.eachLimit(peers, 2, function (peer, eachCb) { + peer = self.accept(peer); + + library.schema.validate(peer, schema.updatePeersList.peer, function (err) { + if (err) { + err.forEach(function (e) { + library.logger.error(['Rejecting invalid peer', peer.string, e.path, e.message].join(' ')); + }); + } else if (!modules.system.versionCompatible(peer.version)) { + library.logger.error(['Rejecting peer', peer.string, 'with incompatible version', peer.version].join(' ')); + self.remove(peer.ip, peer.port); + } else { + delete peer.broadhash; + delete peer.height; + self.update(peer); + } + + return setImmediate(eachCb); + }); + }, waterCb); + } - return setImmediate(cb); - } - }); - }, cb); - }); + async.waterfall([ + getFromRandomPeer, + validatePeersList, + pickPeers, + updatePeers + ], function (err) { + return setImmediate(cb, err); }); }; @@ -144,7 +164,17 @@ __private.getByFilter = function (filter, cb) { var where = []; var params = {}; - if (filter.state) { + if (filter.ip) { + where.push('"ip" = ${ip}'); + params.ip = filter.ip; + } + + if (filter.port) { + where.push('"port" = ${port}'); + params.port = filter.port; + } + + if (filter.state >= 0) { where.push('"state" = ${state}'); params.state = filter.state; } @@ -159,14 +189,24 @@ __private.getByFilter = function (filter, cb) { params.version = filter.version; } - if (filter.ip) { - where.push('"ip" = ${ip}'); - params.ip = filter.ip; + if (filter.broadhash) { + where.push('"broadhash" = ${broadhash}'); + params.broadhash = filter.broadhash; } - if (filter.port) { - where.push('"port" = ${port}'); - params.port = filter.port; + if (filter.height) { + where.push('"height" = ${height}'); + params.height = filter.height; + } + + var orderBy = OrderBy( + filter.orderBy, { + sortFields: sql.sortFields + } + ); + + if (orderBy.error) { + return setImmediate(cb, orderBy.error); } if (!filter.limit) { @@ -185,16 +225,6 @@ __private.getByFilter = function (filter, cb) { return setImmediate(cb, 'Invalid limit. Maximum is 100'); } - var orderBy = OrderBy( - filter.orderBy, { - sortFields: sql.sortFields - } - ); - - if (orderBy.error) { - return setImmediate(cb, orderBy.error); - } - library.db.query(sql.getByFilter({ where: where, sortField: orderBy.sortField, @@ -208,141 +238,126 @@ __private.getByFilter = function (filter, cb) { }; // Public methods -Peers.prototype.inspect = function (peer) { - peer = peer || {}; +Peers.prototype.accept = function (peer) { + return new Peer(peer); +}; - if (/^[0-9]+$/.test(peer.ip)) { - peer.ip = ip.fromLong(peer.ip); - } +Peers.prototype.acceptable = function (peers) { + return _.chain(peers).filter(function (peer) { + // Removing peers with private ip address + return !ip.isPrivate(peer.ip); + }).uniqWith(function (a, b) { + // Removing non-unique peers + return (a.ip + a.port) === (b.ip + b.port); + // Slicing peers up to maxPeers + }).slice(0, constants.maxPeers).value(); +}; - peer.port = parseInt(peer.port); +Peers.prototype.list = function (options, cb) { + options.limit = options.limit || constants.maxPeers; + options.broadhash = options.broadhash || modules.system.getBroadhash(); + options.attempts = ['matched broadhash', 'unmatched broadhash', 'fallback']; + options.attempt = 0; + options.matched = 0; + + if (!options.broadhash) { + delete options.broadhash; + } - if (peer.ip) { - peer.string = (peer.ip + ':' + peer.port || 'unknown'); - } else { - peer.string = 'unknown'; + function randomList (options, peers, cb) { + library.db.query(sql.randomList(options), options).then(function (rows) { + options.limit -= rows.length; + if (options.attempt === 0 && rows.length > 0) { options.matched = rows.length; } + library.logger.debug(['Listing', rows.length, options.attempts[options.attempt], 'peers'].join(' ')); + return setImmediate(cb, null, self.acceptable(peers.concat(rows))); + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, 'Peers#list error'); + }); } - peer.os = peer.os || 'unknown'; - peer.version = peer.version || '0.0.0'; + async.waterfall([ + // Matched broadhash + function (waterCb) { + return randomList(options, [], waterCb); + }, + // Unmatched broadhash + function (peers, waterCb) { + if (options.limit > 0) { + options.attempt += 1; + + return randomList(options, peers, waterCb); + } else { + return setImmediate(waterCb, null, peers); + } + }, + // Fallback + function (peers, waterCb) { + delete options.broadhash; - return peer; -}; + if (options.limit > 0) { + options.attempt += 1; -Peers.prototype.list = function (options, cb) { - options.limit = options.limit || 100; + return randomList(options, peers, waterCb); + } else { + return setImmediate(waterCb, null, peers); + } + } + ], function (err, peers) { + var consensus = Math.round(options.matched / peers.length * 100 * 1e2) / 1e2; + consensus = isNaN(consensus) ? 0 : consensus; - library.db.query(sql.randomList(options), options).then(function (rows) { - return setImmediate(cb, null, rows); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Peers#list error'); + library.logger.debug(['Listing', peers.length, 'total peers'].join(' ')); + return setImmediate(cb, err, peers, consensus); }); }; -Peers.prototype.state = function (pip, port, state, timeoutSeconds, cb) { - var isFrozenList = _.find(library.config.peers, function (peer) { +Peers.prototype.state = function (pip, port, state, timeoutSeconds) { + var frozenPeer = _.find(library.config.peers, function (peer) { return peer.ip === pip && peer.port === port; }); - if (isFrozenList !== undefined && cb) { - return setImmediate(cb, 'Peer in white list'); - } - var clock; - if (state === 0) { - clock = (timeoutSeconds || 1) * 1000; - clock = Date.now() + clock; + if (frozenPeer) { + library.logger.debug('Not changing state of frozen peer', [pip, port].join(':')); } else { - clock = null; - } - var params = { - state: state, - clock: clock, - ip: pip, - port: port - }; - library.db.query(sql.state, params).then(function (res) { - library.logger.debug('Updated peer state', params); - return cb && setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return cb && setImmediate(cb); - }); -}; + var clock; -Peers.prototype.remove = function (pip, port, cb) { - var isFrozenList = _.find(library.config.peers.list, function (peer) { - return peer.ip === pip && peer.port === port; - }); - if (isFrozenList !== undefined && cb) { - return setImmediate(cb, 'Peer in white list'); + if (state === 0) { + clock = (timeoutSeconds || 1) * 1000; + clock = Date.now() + clock; + } else { + clock = null; + } + return __private.sweeper.push('state', { + state: state, + clock: clock, + ip: pip, + port: port + }); } - removed.push(pip); - var params = { - ip: pip, - port: port - }; - library.db.query(sql.remove, params).then(function (res) { - library.logger.debug('Removed peer', params); - return cb && setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return cb && setImmediate(cb); - }); }; -Peers.prototype.addDapp = function (config, cb) { - library.db.task(function (t) { - return t.query(sql.getByIdPort, { ip: config.ip, port: config.port }).then(function (rows) { - if (rows.length) { - var params = { - dappId: config.dappid, - peerId: rows[0].id - }; - - return t.query(sql.addDapp, params).then(function (res) { - library.logger.debug('Added dapp peer', params); - }); - } else { - return t; - } - }); - }).then(function (res) { - return setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Peers#addDapp error'); - }); +Peers.prototype.isRemoved = function (pip) { + return (removed.indexOf(pip) !== -1); }; -Peers.prototype.update = function (peer, cb) { - var params = { - ip: peer.ip, - port: peer.port, - os: peer.os || null, - version: peer.version || null, - state: 1 - }; - - var query; - if (peer.state !== undefined) { - params.state = peer.state; - query = sql.upsertWithState; +Peers.prototype.remove = function (pip, port) { + var frozenPeer = _.find(library.config.peers.list, function (peer) { + return peer.ip === pip && peer.port === port; + }); + if (frozenPeer) { + library.logger.debug('Not removing frozen peer', [pip, port].join(':')); + } else if (self.isRemoved(pip)) { + library.logger.debug('Peer already removed', [pip, port].join(':')); } else { - query = sql.upsertWithoutState; + removed.push(pip); + return __private.sweeper.push('remove', { ip: pip, port: port }); } +}; - library.db.query(query, params).then(function () { - library.logger.debug('Upserted peer', params); - - if (peer.dappid) { - return self.addDapp({dappid: peer.dappid, ip: peer.ip, port: peer.port}, cb); - } else { - return setImmediate(cb); - } - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Peers#update error'); - }); +Peers.prototype.update = function (peer) { + peer.state = 2; + removed.splice(removed.indexOf(peer.ip)); + return __private.sweeper.push('upsert', self.accept(peer).object()); }; Peers.prototype.sandboxApi = function (call, args, cb) { @@ -355,24 +370,27 @@ Peers.prototype.onBind = function (scope) { }; Peers.prototype.onBlockchainReady = function () { - async.eachSeries(library.config.peers.list, function (peer, cb) { - var params = { - ip: peer.ip, - port: peer.port, - state: 2 - }; - library.db.query(sql.insertSeed, params).then(function (res) { - library.logger.debug('Inserted seed peer', params); - return setImmediate(cb, null, res); - }).catch(function (err) { - library.logger.error(err.stack); - return setImmediate(cb, 'Peers#onBlockchainReady error'); - }); - }, function (err) { - if (err) { - library.logger.error(err); - } + async.series({ + insertSeeds: function (seriesCb) { + async.eachSeries(library.config.peers.list, function (peer, eachCb) { + self.update({ + ip: peer.ip, + port: peer.port, + version: modules.system.getVersion(), + state: 2, + broadhash: modules.system.getBroadhash(), + height: 1 + }); + return setImmediate(eachCb); + }, function (err) { + return setImmediate(seriesCb, err); + }); + }, + waitForSweep: function (seriesCb) { + return setTimeout(seriesCb, 1000); + } + }, function (err) { __private.count(function (err, count) { if (count) { __private.updatePeersList(function (err) { @@ -391,27 +409,31 @@ Peers.prototype.onBlockchainReady = function () { }; Peers.prototype.onPeersReady = function () { - setImmediate(function nextUpdatePeersList () { - __private.updatePeersList(function (err) { - if (err) { - library.logger.error('Peers timer:', err); - } - setTimeout(nextUpdatePeersList, 60 * 1000); - }); - }); - - setImmediate(function nextBanManager () { - __private.banManager(function (err) { - if (err) { - library.logger.error('Ban manager timer:', err); + setImmediate(function nextSeries () { + async.series({ + updatePeersList: function (seriesCb) { + __private.updatePeersList(function (err) { + if (err) { + library.logger.error('Peers timer', err); + } + return setImmediate(seriesCb); + }); + }, + nextBanManager: function (seriesCb) { + __private.banManager(function (err) { + if (err) { + library.logger.error('Ban manager timer', err); + } + return setImmediate(seriesCb); + }); } - setTimeout(nextBanManager, 65 * 1000); + }, function (err) { + return setTimeout(nextSeries, 60000); }); }); }; // Shared - shared.getPeers = function (req, cb) { library.schema.validate(req.body, schema.getPeers, function (err) { if (err) { diff --git a/modules/rounds.js b/modules/rounds.js index 19095d5c962..98380bf7bcc 100644 --- a/modules/rounds.js +++ b/modules/rounds.js @@ -55,12 +55,18 @@ Rounds.prototype.directionSwap = function (direction, lastBlock, cb) { __private.feesByRound = {}; __private.rewardsByRound = {}; __private.delegatesByRound = {}; - self.flush(self.calc(lastBlock.height), cb); + + return setImmediate(cb); } else { __private.unFeesByRound = {}; __private.unRewardsByRound = {}; __private.unDelegatesByRound = {}; - self.flush(self.calc(lastBlock.height), cb); + + if (lastBlock) { + return __private.sumRound(self.calc(lastBlock.height), cb); + } else { + return setImmediate(cb); + } } }; @@ -71,7 +77,7 @@ Rounds.prototype.backwardTick = function (block, previousBlock, done) { __private.unFeesByRound[round] = Math.floor(__private.unFeesByRound[round]) || 0; __private.unFeesByRound[round] += Math.floor(block.totalFee); - __private.unRewardsByRound[round] = (__private.rewardsByRound[round] || []); + __private.unRewardsByRound[round] = (__private.unRewardsByRound[round] || []); __private.unRewardsByRound[round].push(block.reward); __private.unDelegatesByRound[round] = __private.unDelegatesByRound[round] || []; @@ -100,7 +106,9 @@ Rounds.prototype.backwardTick = function (block, previousBlock, done) { delete __private.unFeesByRound[round]; delete __private.unRewardsByRound[round]; delete __private.unDelegatesByRound[round]; - }); + }).then(promised.markBlockId); + } else { + return promised.markBlockId(); } }); } @@ -154,7 +162,7 @@ Rounds.prototype.tick = function (block, done) { scope.finishRound = ( (round !== nextRound && __private.delegatesByRound[round].length === slots.delegates) || - (block.height === 1 || block.heighti === 101) + (block.height === 1 || block.height === 101) ); function Tick (t) { @@ -214,21 +222,10 @@ Rounds.prototype.onBind = function (scope) { Rounds.prototype.onBlockchainReady = function () { var round = self.calc(modules.blocks.getLastBlock().height); - library.db.query(sql.summedRound, { round: round, activeDelegates:constants.activeDelegates }).then(function (rows) { - - var rewards = []; - - rows[0].rewards.forEach(function (reward) { - rewards.push(Math.floor(reward)); - }); - - __private.feesByRound[round] = Math.floor(rows[0].fees); - __private.rewardsByRound[round] = rewards; - __private.delegatesByRound[round] = rows[0].delegates; - __private.loaded = true; - - }).catch(function (err) { - library.logger.error('Round#onBlockchainReady error', err); + __private.sumRound(round, function (err) { + if (!err) { + __private.loaded = true; + } }); }; @@ -242,7 +239,6 @@ Rounds.prototype.cleanup = function (cb) { }; // Private - __private.getOutsiders = function (scope, cb) { scope.outsiders = []; @@ -264,6 +260,26 @@ __private.getOutsiders = function (scope, cb) { }); }; +__private.sumRound = function (round, cb) { + library.db.query(sql.summedRound, { round: round, activeDelegates: constants.activeDelegates }).then(function (rows) { + var rewards = []; + + rows[0].rewards.forEach(function (reward) { + rewards.push(Math.floor(reward)); + }); + + __private.feesByRound[round] = Math.floor(rows[0].fees); + __private.rewardsByRound[round] = rewards; + __private.delegatesByRound[round] = rows[0].delegates; + + return setImmediate(cb); + }).catch(function (err) { + library.logger.error('Failed to sum round', round); + library.logger.error(err.stack); + return setImmediate(cb, err); + }); +}; + // Shared // Export diff --git a/modules/signatures.js b/modules/signatures.js index f2f90bc8aae..ba192745f8d 100644 --- a/modules/signatures.js +++ b/modules/signatures.js @@ -7,6 +7,7 @@ var MilestoneBlocks = require('../helpers/milestoneBlocks.js'); var Router = require('../helpers/router.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/signatures.js'); +var Signature = require('../logic/signature.js'); var slots = require('../helpers/slots.js'); var transactionTypes = require('../helpers/transactionTypes.js'); @@ -22,7 +23,6 @@ function Signatures (cb, scope) { __private.attachApi(); - var Signature = require('../logic/signature.js'); __private.assetTypes[transactionTypes.SIGNATURE] = library.logic.transaction.attachAssetType( transactionTypes.SIGNATURE, new Signature() ); @@ -51,7 +51,7 @@ __private.attachApi = function () { library.network.app.use('/api/signatures', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -151,7 +151,7 @@ shared.addSignature = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -182,7 +182,7 @@ shared.addSignature = function (req, cb) { } catch (e) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } diff --git a/modules/system.js b/modules/system.js index 0c22adca2c9..b94247ac51b 100644 --- a/modules/system.js +++ b/modules/system.js @@ -1,20 +1,36 @@ 'use strict'; +var async = require('async'); +var crypto = require('crypto'); var os = require('os'); var sandboxHelper = require('../helpers/sandbox.js'); +var semver = require('semver'); +var sql = require('../sql/system.js'); // Private fields var modules, library, self, __private = {}, shared = {}; +var rcRegExp = /[a-z]+$/; + // Constructor function System (cb, scope) { library = scope; self = this; + __private.os = os.platform() + os.release(); __private.version = library.config.version; __private.port = library.config.port; + __private.height = 1; __private.nethash = library.config.nethash; - __private.osName = os.platform() + os.release(); + __private.broadhash = library.config.nethash; + __private.minVersion = library.config.minVersion; + + if (rcRegExp.test(__private.minVersion)) { + this.minVersion = __private.minVersion.replace(rcRegExp, ''); + this.minVersionChar = __private.minVersion.charAt(__private.minVersion.length - 1); + } else { + this.minVersion = __private.minVersion; + } setImmediate(cb, null, self); } @@ -22,8 +38,12 @@ function System (cb, scope) { // Private methods // Public methods +System.prototype.headers = function () { + return __private; +}; + System.prototype.getOS = function () { - return __private.osName; + return __private.os; }; System.prototype.getVersion = function () { @@ -34,10 +54,79 @@ System.prototype.getPort = function () { return __private.port; }; +System.prototype.getHeight = function () { + return __private.height; +}; + System.prototype.getNethash = function () { return __private.nethash; }; +System.prototype.networkCompatible = function (nethash) { + return __private.nethash === nethash; +}; + +System.prototype.getMinVersion = function () { + return __private.minVersion; +}; + +System.prototype.versionCompatible = function (version) { + var versionChar; + + if (rcRegExp.test(version)) { + versionChar = version.charAt(version.length - 1); + version = version.replace(rcRegExp, ''); + } + + if (this.minVersionChar && versionChar) { + return (version + versionChar) === (this.minVersion + this.minVersionChar); + } else { + return semver.satisfies(version, this.minVersion); + } +}; + +System.prototype.getBroadhash = function (cb) { + if (typeof cb !== 'function') { + return __private.broadhash; + } + + library.db.query(sql.getBroadhash, { limit: 5 }).then(function (rows) { + if (rows.length <= 1) { + return setImmediate(cb, null, __private.nethash); + } else { + var seed = rows.map(function (row) { return row.id; }).join(''); + var hash = crypto.createHash('sha256').update(seed, 'utf8').digest(); + + return setImmediate(cb, null, hash.toString('hex')); + } + }).catch(function (err) { + library.logger.error(err.stack); + return setImmediate(cb, err); + }); +}; + +System.prototype.update = function (cb) { + async.series({ + getBroadhash: function (seriesCb) { + self.getBroadhash(function (err, hash) { + if (!err) { + __private.broadhash = hash; + } + + return setImmediate(seriesCb); + }); + }, + getHeight: function (seriesCb) { + __private.height = modules.blocks.getLastBlock().height; + return setImmediate(seriesCb); + } + }, function (err) { + library.logger.debug('System headers', __private); + modules.transport.headers(__private); + return setImmediate(cb, err); + }); +}; + System.prototype.sandboxApi = function (call, args, cb) { sandboxHelper.callMethod(shared, call, args, cb); }; diff --git a/modules/transactions.js b/modules/transactions.js index 7ff6f5bee87..c1c628e97b8 100644 --- a/modules/transactions.js +++ b/modules/transactions.js @@ -12,14 +12,14 @@ var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/transactions.js'); var slots = require('../helpers/slots.js'); var sql = require('../sql/transactions.js'); +var TransactionPool = require('../logic/transactionPool.js'); var transactionTypes = require('../helpers/transactionTypes.js'); +var Transfer = require('../logic/transfer.js'); // Private fields var modules, library, self, __private = {}, shared = {}; __private.assetTypes = {}; -__private.unconfirmedTransactions = []; -__private.unconfirmedTransactionsIdIndex = {}; // Constructor function Transactions (cb, scope) { @@ -28,8 +28,8 @@ function Transactions (cb, scope) { self = this; __private.attachApi(); + __private.transactionPool = new TransactionPool(library); - var Transfer = require('../logic/transfer.js'); __private.assetTypes[transactionTypes.SEND] = library.logic.transaction.attachAssetType( transactionTypes.SEND, new Transfer() ); @@ -49,6 +49,10 @@ __private.attachApi = function () { router.map(shared, { 'get /': 'getTransactions', 'get /get': 'getTransaction', + 'get /queued/get': 'getQueuedTransaction', + 'get /queued': 'getQueuedTransactions', + 'get /multisignatures/get': 'getMultisignatureTransaction', + 'get /multisignatures': 'getMultisignatureTransactions', 'get /unconfirmed/get': 'getUnconfirmedTransaction', 'get /unconfirmed': 'getUnconfirmedTransactions', 'put /': 'addTransactions' @@ -61,7 +65,7 @@ __private.attachApi = function () { library.network.app.use('/api/transactions', router); library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; @@ -183,185 +187,98 @@ __private.getById = function (id, cb) { }); }; -__private.addUnconfirmedTransaction = function (transaction, sender, cb) { - self.applyUnconfirmed(transaction, sender, function (err) { +__private.getPooledTransaction = function (method, req, cb) { + library.schema.validate(req.body, schema.getPooledTransaction, function (err) { if (err) { - self.removeUnconfirmedTransaction(transaction.id); - return setImmediate(cb, err); - } else if (modules.loader.syncing()) { - self.undoUnconfirmed(transaction, cb); - } else { - transaction.receivedAt = new Date(); - __private.unconfirmedTransactions.push(transaction); - var index = __private.unconfirmedTransactions.length - 1; - __private.unconfirmedTransactionsIdIndex[transaction.id] = index; - - return setImmediate(cb); + return setImmediate(cb, err[0].message); } - }); -}; - -// Public methods -Transactions.prototype.getUnconfirmedTransaction = function (id) { - var index = __private.unconfirmedTransactionsIdIndex[id]; - return __private.unconfirmedTransactions[index]; -}; -Transactions.prototype.getUnconfirmedTransactionList = function (reverse, limit) { - var a = []; + var transaction = self[method](req.body.id); - for (var i = 0; i < __private.unconfirmedTransactions.length; i++) { - if (__private.unconfirmedTransactions[i] !== false) { - a.push(__private.unconfirmedTransactions[i]); + if (!transaction) { + return setImmediate(cb, 'Transaction not found'); } - } - - a = reverse ? a.reverse() : a; - - if (limit) { - a.splice(limit); - } - - return a; -}; -Transactions.prototype.removeUnconfirmedTransaction = function (id) { - var index = __private.unconfirmedTransactionsIdIndex[id]; - delete __private.unconfirmedTransactionsIdIndex[id]; - __private.unconfirmedTransactions[index] = false; + return setImmediate(cb, null, {transaction: transaction}); + }); }; -Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) { - // Check transaction - if (!transaction) { - return setImmediate(cb, 'Missing transaction'); - } +__private.getPooledTransactions = function (method, req, cb) { + library.schema.validate(req.body, schema.getPooledTransactions, function (err) { + if (err) { + return setImmediate(cb, err[0].message); + } - // Check transaction indexes - if (__private.unconfirmedTransactionsIdIndex[transaction.id] !== undefined) { - library.logger.debug('Transaction is already processed: ' + transaction.id); - return setImmediate(cb); - } + var transactions = self[method](true); + var i, toSend = []; - modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { - function done (err, ignore) { - if (err) { - if (ignore) { - library.logger.debug(err); - return setImmediate(cb); - } else { - return setImmediate(cb, err); + if (req.body.senderPublicKey || req.body.address) { + for (i = 0; i < transactions.length; i++) { + if (transactions[i].senderPublicKey === req.body.senderPublicKey || transactions[i].recipientId === req.body.address) { + toSend.push(transactions[i]); } } - - __private.addUnconfirmedTransaction(transaction, sender, function (err) { - if (err) { - return setImmediate(cb, err); - } - - library.bus.message('unconfirmedTransaction', transaction, broadcast); - - return setImmediate(cb); - }); + } else { + for (i = 0; i < transactions.length; i++) { + toSend.push(transactions[i]); + } } - if (err) { - return done(err); - } + return setImmediate(cb, null, {transactions: toSend, count: transactions.length}); + }); +}; - if (transaction.requesterPublicKey && sender && Array.isArray(sender.multisignatures) && sender.multisignatures.length) { - modules.accounts.getAccount({publicKey: transaction.requesterPublicKey}, function (err, requester) { - if (err) { - return done(err); - } +// Public methods +Transactions.prototype.transactionInPool = function (id) { + return __private.transactionPool.transactionInPool(id); +}; - if (!requester) { - return done('Requester not found'); - } +Transactions.prototype.getUnconfirmedTransaction = function (id) { + return __private.transactionPool.getUnconfirmedTransaction(id); +}; - library.logic.transaction.process(transaction, sender, requester, function (err, transaction, ignore) { - if (err) { - return done(err, ignore); - } +Transactions.prototype.getQueuedTransaction = function (id) { + return __private.transactionPool.getQueuedTransaction(id); +}; - library.logic.transaction.verify(transaction, sender, done); - }); - }); - } else { - library.logic.transaction.process(transaction, sender, function (err, transaction, ignore) { - if (err) { - return done(err, ignore); - } +Transactions.prototype.getMultisignatureTransaction = function (id) { + return __private.transactionPool.getMultisignatureTransaction(id); +}; - library.logic.transaction.verify(transaction, sender, done); - }); - } - }); +Transactions.prototype.getUnconfirmedTransactionList = function (reverse, limit) { + return __private.transactionPool.getUnconfirmedTransactionList(reverse, limit); }; -Transactions.prototype.applyUnconfirmedList = function (ids, cb) { - async.eachSeries(ids, function (id, cb) { - var transaction = self.getUnconfirmedTransaction(id); - modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) { - if (err) { - self.removeUnconfirmedTransaction(id); - return setImmediate(cb, err); - } - self.applyUnconfirmed(transaction, sender, function (err) { - if (err) { - self.removeUnconfirmedTransaction(id); - } - return setImmediate(cb, err); - }); - }); - }, cb); +Transactions.prototype.getQueuedTransactionList = function (reverse, limit) { + return __private.transactionPool.getQueuedTransactionList(reverse, limit); }; -Transactions.prototype.undoUnconfirmedList = function (cb) { - var ids = []; +Transactions.prototype.getMultisignatureTransactionList = function (reverse, limit) { + return __private.transactionPool.getMultisignatureTransactionList(reverse, limit); +}; - async.eachSeries(__private.unconfirmedTransactions, function (transaction, cb) { - if (transaction !== false) { - ids.push(transaction.id); - self.undoUnconfirmed(transaction, cb); - } else { - return setImmediate(cb); - } - }, function (err) { - return setImmediate(cb, err, ids); - }); +Transactions.prototype.getMergedTransactionList = function (reverse, limit) { + return __private.transactionPool.getMergedTransactionList(reverse, limit); }; -Transactions.prototype.expireUnconfirmedList = function (cb) { - var standardTimeOut = Number(constants.unconfirmedTransactionTimeOut); - var ids = []; +Transactions.prototype.removeUnconfirmedTransaction = function (id) { + return __private.transactionPool.removeUnconfirmedTransaction(id); +}; - async.eachSeries(__private.unconfirmedTransactions, function (transaction, cb) { - if (transaction === false) { - return setImmediate(cb); - } +Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) { + return __private.transactionPool.processUnconfirmedTransaction(transaction, broadcast, cb); +}; - var timeNow = new Date(); - var timeOut = (transaction.type === 4) ? (transaction.asset.multisignature.lifetime * 3600) : standardTimeOut; - var seconds = Math.floor((timeNow.getTime() - transaction.receivedAt.getTime()) / 1000); +Transactions.prototype.applyUnconfirmedList = function (cb) { + return __private.transactionPool.applyUnconfirmedList(cb); +}; - if (seconds > timeOut) { - self.undoUnconfirmed(transaction, function (err) { - if (err) { - return setImmediate(cb, err); - } else { - ids.push(transaction.id); - self.removeUnconfirmedTransaction(transaction.id); - library.logger.info('Expired unconfirmed transaction: ' + transaction.id + ' received at: ' + transaction.receivedAt.toUTCString()); - return setImmediate(cb); - } - }); - } else { - return setImmediate(cb); - } - }, function (err) { - return setImmediate(cb, err, ids); - }); +Transactions.prototype.applyUnconfirmedIds = function (ids, cb) { + return __private.transactionPool.applyUnconfirmedIds(ids, cb); +}; + +Transactions.prototype.undoUnconfirmedList = function (cb) { + return __private.transactionPool.undoUnconfirmedList(cb); }; Transactions.prototype.apply = function (transaction, block, sender, cb) { @@ -409,12 +326,12 @@ Transactions.prototype.undoUnconfirmed = function (transaction, cb) { }); }; -Transactions.prototype.receiveTransactions = function (transactions, cb) { - async.eachSeries(transactions, function (transaction, cb) { - self.processUnconfirmedTransaction(transaction, true, cb); - }, function (err) { - return setImmediate(cb, err, transactions); - }); +Transactions.prototype.receiveTransactions = function (transactions, broadcast, cb) { + return __private.transactionPool.receiveTransactions(transactions, broadcast, cb); +}; + +Transactions.prototype.fillPool = function (cb) { + return __private.transactionPool.fillPool(cb); }; Transactions.prototype.sandboxApi = function (call, args, cb) { @@ -425,21 +342,13 @@ Transactions.prototype.sandboxApi = function (call, args, cb) { Transactions.prototype.onBind = function (scope) { modules = scope; + __private.transactionPool.bind(modules); __private.assetTypes[transactionTypes.SEND].bind({ modules: modules, library: library }); }; Transactions.prototype.onPeersReady = function () { - setImmediate(function nextUnconfirmedExpiry () { - self.expireUnconfirmedList(function (err, ids) { - if (err) { - library.logger.error('Unconfirmed transactions timer:', err); - } - - setTimeout(nextUnconfirmedExpiry, 14 * 1000); - }); - }); }; // Shared @@ -474,45 +383,28 @@ shared.getTransaction = function (req, cb) { }); }; -shared.getUnconfirmedTransaction = function (req, cb) { - library.schema.validate(req.body, schema.getUnconfirmedTransaction, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } - - var unconfirmedTransaction = self.getUnconfirmedTransaction(req.body.id); - - if (!unconfirmedTransaction) { - return setImmediate(cb, 'Transaction not found'); - } +shared.getQueuedTransaction = function (req, cb) { + return __private.getPooledTransaction('getQueuedTransaction', req, cb); +}; - return setImmediate(cb, null, {transaction: unconfirmedTransaction}); - }); +shared.getQueuedTransactions = function (req, cb) { + return __private.getPooledTransactions('getQueuedTransactionList', req, cb); }; -shared.getUnconfirmedTransactions = function (req, cb) { - library.schema.validate(req.body, schema.getUnconfirmedTransactions, function (err) { - if (err) { - return setImmediate(cb, err[0].message); - } +shared.getMultisignatureTransaction = function (req, cb) { + return __private.getPooledTransaction('getMultisignatureTransaction', req, cb); +}; - var transactions = self.getUnconfirmedTransactionList(true); - var i, toSend = []; +shared.getMultisignatureTransactions = function (req, cb) { + return __private.getPooledTransactions('getMultisignatureTransactionList', req, cb); +}; - if (req.body.senderPublicKey || req.body.address) { - for (i = 0; i < transactions.length; i++) { - if (transactions[i].senderPublicKey === req.body.senderPublicKey || transactions[i].recipientId === req.body.address) { - toSend.push(transactions[i]); - } - } - } else { - for (i = 0; i < transactions.length; i++) { - toSend.push(transactions[i]); - } - } +shared.getUnconfirmedTransaction = function (req, cb) { + return __private.getPooledTransaction('getUnconfirmedTransaction', req, cb); +}; - return setImmediate(cb, null, {transactions: toSend}); - }); +shared.getUnconfirmedTransactions = function (req, cb) { + return __private.getPooledTransactions('getUnconfirmedTransactionList', req, cb); }; shared.addTransactions = function (req, cb) { @@ -554,7 +446,7 @@ shared.addTransactions = function (req, cb) { return setImmediate(cb, 'Multisignature account not found'); } - if (!account.multisignatures || !account.multisignatures) { + if (!Array.isArray(account.multisignatures)) { return setImmediate(cb, 'Account does not have multisignatures enabled'); } @@ -602,7 +494,7 @@ shared.addTransactions = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); }); } else { @@ -641,7 +533,7 @@ shared.addTransactions = function (req, cb) { return setImmediate(cb, e.toString()); } - modules.transactions.receiveTransactions([transaction], cb); + modules.transactions.receiveTransactions([transaction], true, cb); }); } }); diff --git a/modules/transport.js b/modules/transport.js index adaeaa91235..ccc645d7a09 100644 --- a/modules/transport.js +++ b/modules/transport.js @@ -2,7 +2,9 @@ var _ = require('lodash'); var async = require('async'); +var Broadcaster = require('../logic/broadcaster.js'); var bignum = require('../helpers/bignum.js'); +var constants = require('../helpers/constants.js'); var crypto = require('crypto'); var extend = require('extend'); var ip = require('ip'); @@ -26,6 +28,7 @@ function Transport (cb, scope) { self = this; __private.attachApi(); + __private.broadcaster = new Broadcaster(library); setImmediate(cb, null, self); } @@ -35,29 +38,21 @@ __private.attachApi = function () { var router = new Router(); router.use(function (req, res, next) { + res.set(__private.headers); + if (modules && __private.loaded) { return next(); } res.status(500).send({success: false, error: 'Blockchain is loading'}); }); router.use(function (req, res, next) { - try { - req.peer = modules.peers.inspect( - { - ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress, - port: req.headers.port - } - ); - } catch (e) { - // Remove peer - __private.removePeer({peer: req.peer, code: 'EHEADERS', req: req}); - - library.logger.debug(e.toString()); - return res.status(406).send({success: false, error: 'Invalid request headers'}); - } + req.peer = modules.peers.accept( + { + ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress, + port: req.headers.port + } + ); - var headers = req.headers; - headers.ip = req.peer.ip; - headers.port = req.peer.port; + var headers = req.peer.extend(req.headers); req.sanitize(headers, schema.headers, function (err, report) { if (err) { return next(err); } @@ -65,49 +60,41 @@ __private.attachApi = function () { // Remove peer __private.removePeer({peer: req.peer, code: 'EHEADERS', req: req}); - return res.status(500).send({status: false, error: report.issues}); + return res.status(500).send({success: false, error: report.issues}); } - if (headers.nethash !== library.config.nethash) { + if (!modules.system.networkCompatible(headers.nethash)) { // Remove peer __private.removePeer({peer: req.peer, code: 'ENETHASH', req: req}); - return res.status(200).send({success: false, message: 'Request is made on the wrong network', expected: library.config.nethash, received: headers.nethash}); + return res.status(500).send({success: false, message: 'Request is made on the wrong network', expected: modules.system.getNethash(), received: headers.nethash}); } - req.peer.state = 2; - req.peer.os = headers.os; - req.peer.version = headers.version; + if (!modules.system.versionCompatible(headers.version)) { + // Remove peer + __private.removePeer({peer: req.peer, code: 'EVERSION:' + headers.version, req: req}); + + return res.status(500).send({success: false, message: 'Request is made from incompatible version', expected: modules.system.getMinVersion(), received: headers.version}); + } if (req.body && req.body.dappid) { req.peer.dappid = req.body.dappid; } - if ((req.peer.version === library.config.version) && (headers.nethash === library.config.nethash)) { - if (!modules.blocks.lastReceipt()) { - modules.delegates.enableForging(); - } - - library.dbSequence.add(function (cb) { - modules.peers.update(req.peer, cb); - }); - } + modules.peers.update(req.peer); return next(); }); - }); router.get('/list', function (req, res) { - res.set(__private.headers); - modules.peers.list({limit: 100}, function (err, peers) { - return res.status(200).json({peers: !err ? peers : []}); + modules.peers.list({limit: constants.maxPeers}, function (err, peers) { + peers = (!err ? peers : []); + return res.status(200).json({success: !err, peers: peers}); }); }); router.get('/blocks/common', function (req, res, next) { - res.set(__private.headers); - req.sanitize(req.query, schema.commonBlock, function (err, report, query) { if (err) { return next(err); } if (!report.isValid) { return res.json({success: false, error: report.issues}); } @@ -123,7 +110,10 @@ __private.attachApi = function () { }); if (!escapedIds.length) { - library.logger.warn('Invalid common block request, ban 60 min', req.peer.string); + library.logger.warn('Invalid common block request, ban 10 min', req.peer.string); + + // Ban peer for 10 minutes + __private.banPeer({peer: req.peer, code: 'ECOMMON', req: req, clock: 600}); return res.json({success: false, error: 'Invalid block id sequence'}); } @@ -138,8 +128,6 @@ __private.attachApi = function () { }); router.get('/blocks', function (req, res, next) { - res.set(__private.headers); - req.sanitize(req.query, schema.blocks, function (err, report, query) { if (err) { return next(err); } if (!report.isValid) { return res.json({success: false, error: report.issues}); } @@ -161,20 +149,18 @@ __private.attachApi = function () { }); router.post('/blocks', function (req, res) { - res.set(__private.headers); - var block = req.body.block; var id = (block ? block.id : 'null'); try { block = library.logic.block.objectNormalize(block); } catch (e) { - library.logger.error(['Block', id].join(' '), e.toString()); - if (block) { library.logger.error('Block', block); } + library.logger.debug(['Block', id].join(' '), e.toString()); + if (block) { library.logger.debug('Block', block); } if (req.peer) { - // Ban peer for 60 minutes - __private.banPeer({peer: req.peer, code: 'EBLOCK', req: req, clock: 3600}); + // Ban peer for 10 minutes + __private.banPeer({peer: req.peer, code: 'EBLOCK', req: req, clock: 600}); } return res.status(200).json({success: false, error: e.toString()}); @@ -186,30 +172,30 @@ __private.attachApi = function () { }); router.post('/signatures', function (req, res) { - res.set(__private.headers); - - library.schema.validate(req.body, schema.signatures, function (err) { - if (err) { - return res.status(200).json({success: false, error: 'Signature validation failed'}); - } - - modules.multisignatures.processSignature(req.body.signature, function (err) { + if (req.body.signatures) { + __private.receiveSignatures(req, function (err) { if (err) { - return res.status(200).json({success: false, error: 'Error processing signature'}); + return res.status(200).json({success: false, message: err}); } else { return res.status(200).json({success: true}); } }); - }); + } else { + __private.receiveSignature(req.body.signature, req, function (err, id) { + if (err) { + return res.status(200).json({success: false, message: err}); + } else { + return res.status(200).json({success: true}); + } + }); + } }); router.get('/signatures', function (req, res) { - res.set(__private.headers); - - var unconfirmedList = modules.transactions.getUnconfirmedTransactionList(); + var transactions = modules.transactions.getMultisignatureTransactionList(true, constants.maxSharedTxs); var signatures = []; - async.eachSeries(unconfirmedList, function (trs, cb) { + async.eachSeries(transactions, function (trs, cb) { if (trs.signatures && trs.signatures.length) { signatures.push({ transaction: trs.id, @@ -224,47 +210,32 @@ __private.attachApi = function () { }); router.get('/transactions', function (req, res) { - res.set(__private.headers); - res.status(200).json({success: true, transactions: modules.transactions.getUnconfirmedTransactionList()}); + var transactions = modules.transactions.getMergedTransactionList(true, constants.maxSharedTxs); + + res.status(200).json({success: true, transactions: transactions}); }); router.post('/transactions', function (req, res) { - res.set(__private.headers); - - var transaction = req.body.transaction; - var id = (transaction? transaction.id : 'null'); - - try { - transaction = library.logic.transaction.objectNormalize(transaction); - } catch (e) { - library.logger.error(['Transaction', id].join(' '), e.toString()); - if (transaction) { library.logger.error('Transaction', transaction); } - - if (req.peer) { - // Ban peer for 60 minutes - __private.banPeer({peer: req.peer, code: 'ETRANSACTION', req: req, clock: 3600}); - } - - return res.status(200).json({success: false, message: 'Invalid transaction body'}); + if (req.body.transactions) { + __private.receiveTransactions(req, function (err) { + if (err) { + return res.status(200).json({success: false, message: err}); + } else { + return res.status(200).json({success: true}); + } + }); + } else { + __private.receiveTransaction(req.body.transaction, req, function (err, id) { + if (err) { + return res.status(200).json({success: false, message: err}); + } else { + return res.status(200).json({success: true, transactionId: id}); + } + }); } - - library.balancesSequence.add(function (cb) { - library.logger.debug('Received transaction ' + transaction.id + ' from peer ' + req.peer.string); - modules.transactions.receiveTransactions([transaction], cb); - }, function (err) { - if (err) { - library.logger.error(['Transaction', id].join(' '), err.toString()); - if (transaction) { library.logger.error('Transaction', transaction); } - - res.status(200).json({success: false, message: err.toString()}); - } else { - res.status(200).json({success: true, transactionId: transaction.id}); - } - }); }); router.get('/height', function (req, res) { - res.set(__private.headers); res.status(200).json({ success: true, height: modules.blocks.getLastBlock().height @@ -272,8 +243,6 @@ __private.attachApi = function () { }); router.post('/dapp/message', function (req, res) { - res.set(__private.headers); - try { if (!req.body.dappid) { return res.status(200).json({success: false, message: 'Missing dappid'}); @@ -314,8 +283,6 @@ __private.attachApi = function () { }); router.post('/dapp/request', function (req, res) { - res.set(__private.headers); - try { if (!req.body.dappid) { return res.status(200).json({success: false, message: 'Missing dappid'}); @@ -356,11 +323,12 @@ __private.attachApi = function () { library.network.app.use(function (err, req, res, next) { if (!err) { return next(); } - library.logger.error('API error ' + req.url, err); + library.logger.error('API error ' + req.url, err.message); res.status(500).send({success: false, error: 'API error: ' + err.message}); }); }; +// Private methods __private.hashsum = function (obj) { var buf = new Buffer(JSON.stringify(obj), 'utf8'); var hashdig = crypto.createHash('sha256').update(buf).digest(); @@ -373,37 +341,152 @@ __private.hashsum = function (obj) { }; __private.banPeer = function (options) { - modules.peers.state(options.peer.ip, options.peer.port, 0, options.clock, function (err) { - library.logger.warn([options.code, ['Ban', options.peer.string, (options.clock / 60), 'minutes'].join(' '), options.req.method, options.req.url].join(' ')); - }); + library.logger.warn([options.code, ['Ban', options.peer.string, (options.clock / 60), 'minutes'].join(' '), options.req.method, options.req.url].join(' ')); + modules.peers.state(options.peer.ip, options.peer.port, 0, options.clock); }; __private.removePeer = function (options) { - modules.peers.remove(options.peer.ip, options.peer.port, function (err) { - library.logger.warn([options.code, 'Removing peer', options.peer.string, options.req.method, options.req.url].join(' ')); + library.logger.warn([options.code, 'Removing peer', options.peer.string, options.req.method, options.req.url].join(' ')); + modules.peers.remove(options.peer.ip, options.peer.port); +}; + +__private.receiveSignatures = function (req, cb) { + var signatures; + + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.signatures, function (err) { + if (err) { + return setImmediate(seriesCb, 'Invalid signatures body'); + } else { + return setImmediate(seriesCb); + } + }); + }, + receiveSignatures: function (seriesCb) { + signatures = req.body.signatures; + + async.eachSeries(signatures, function (signature, eachSeriesCb) { + __private.receiveSignature(signature, req, function (err) { + if (err) { + library.logger.debug(err, signature); + } + + return setImmediate(eachSeriesCb); + }); + }, seriesCb); + } + }, function (err) { + return setImmediate(cb, err); }); }; -// Public methods -Transport.prototype.broadcast = function (config, options, cb) { - library.logger.debug('Broadcast', options); +__private.receiveSignature = function (signature, req, cb) { + library.schema.validate({signature: signature}, schema.signature, function (err) { + if (err) { + return setImmediate(cb, 'Invalid signature body'); + } - config.limit = config.limit || 1; - modules.peers.list(config, function (err, peers) { - if (!err) { - async.eachLimit(peers, 3, function (peer, cb) { - return self.getFromPeer(peer, options, cb); - }, function (err) { - if (cb) { - return setImmediate(cb, null, {body: null, peer: peers}); + modules.multisignatures.processSignature(signature, function (err) { + if (err) { + return setImmediate(cb, 'Error processing signature: ' + err); + } else { + return setImmediate(cb); + } + }); + }); +}; + +__private.receiveTransactions = function (req, cb) { + var transactions; + + async.series({ + validateSchema: function (seriesCb) { + library.schema.validate(req.body, schema.transactions, function (err) { + if (err) { + return setImmediate(seriesCb, 'Invalid transactions body'); + } else { + return setImmediate(seriesCb); } }); - } else if (cb) { - return setImmediate(cb, err); + }, + receiveTransactions: function (seriesCb) { + transactions = req.body.transactions; + + async.eachSeries(transactions, function (transaction, eachSeriesCb) { + transaction.bundled = true; + + __private.receiveTransaction(transaction, req, function (err) { + if (err) { + library.logger.debug(err, transaction); + } + + return setImmediate(eachSeriesCb); + }); + }, seriesCb); } + }, function (err) { + return setImmediate(cb, err); }); }; +__private.receiveTransaction = function (transaction, req, cb) { + var id = (transaction ? transaction.id : 'null'); + + try { + transaction = library.logic.transaction.objectNormalize(transaction); + } catch (e) { + library.logger.debug(['Transaction', id].join(' '), e.toString()); + if (transaction) { library.logger.debug('Transaction', transaction); } + + if (req.peer) { + // Ban peer for 10 minutes + __private.banPeer({peer: req.peer, code: 'ETRANSACTION', req: req, clock: 600}); + } + + return setImmediate(cb, 'Invalid transaction body'); + } + + library.balancesSequence.add(function (cb) { + library.logger.debug('Received transaction ' + transaction.id + ' from peer ' + req.peer.string); + modules.transactions.processUnconfirmedTransaction(transaction, true, function (err) { + if (err) { + library.logger.debug(['Transaction', id].join(' '), err.toString()); + if (transaction) { library.logger.debug('Transaction', transaction); } + + return setImmediate(cb, err.toString()); + } else { + return setImmediate(cb, null, transaction.id); + } + }); + }, cb); +}; + +// Public methods +Transport.prototype.headers = function (headers) { + if (headers) { + __private.headers = headers; + } + + return __private.headers; +}; + +Transport.prototype.consensus = function () { + return __private.broadcaster.consensus; +}; + +Transport.prototype.poorConsensus = function () { + if (__private.broadcaster.consensus === undefined) { + return false; + } else { + return (__private.broadcaster.consensus < constants.minBroadhashConsensus); + } +}; + +Transport.prototype.getPeers = function (params, cb) { + return __private.broadcaster.getPeers(params, cb); +}; + Transport.prototype.getFromRandomPeer = function (config, options, cb) { if (typeof options === 'function') { cb = options; @@ -411,16 +494,12 @@ Transport.prototype.getFromRandomPeer = function (config, options, cb) { config = {}; } config.limit = 1; - async.retry(20, function (cb) { - modules.peers.list(config, function (err, peers) { - if (!err && peers.length) { - return self.getFromPeer(peers[0], options, cb); - } else { - return setImmediate(cb, err || 'No reachable peers in db'); - } - }); - }, function (err, results) { - return setImmediate(cb, err, results); + modules.peers.list(config, function (err, peers) { + if (!err && peers.length) { + return self.getFromPeer(peers[0], options, cb); + } else { + return setImmediate(cb, err || 'No acceptable peers found'); + } }); }; @@ -433,12 +512,12 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { url = options.url; } - peer = modules.peers.inspect(peer); + peer = modules.peers.accept(peer); var req = { url: 'http://' + peer.ip + ':' + peer.port + url, method: options.method, - headers: _.extend({}, __private.headers, options.headers), + headers: extend({}, __private.headers, options.headers), timeout: library.config.peers.options.timeout }; @@ -457,9 +536,7 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { return setImmediate(cb, ['Received bad response code', res.status, req.method, req.url].join(' ')); } else { - var headers = res.headers; - headers.ip = peer.ip; - headers.port = peer.port; + var headers = peer.extend(res.headers); var report = library.schema.validate(headers, schema.headers); if (!report) { @@ -469,32 +546,29 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { return setImmediate(cb, ['Invalid response headers', JSON.stringify(headers), req.method, req.url].join(' ')); } - if (headers.nethash !== library.config.nethash) { + if (!modules.system.networkCompatible(headers.nethash)) { // Remove peer __private.removePeer({peer: peer, code: 'ENETHASH', req: req}); return setImmediate(cb, ['Peer is not on the same network', headers.nethash, req.method, req.url].join(' ')); } - if (headers.version === library.config.version) { - library.dbSequence.add(function (cb) { - modules.peers.update({ - ip: peer.ip, - port: headers.port, - state: 2, - os: headers.os, - version: headers.version - }, cb); - }); + if (!modules.system.versionCompatible(headers.version)) { + // Remove peer + __private.removePeer({peer: peer, code: 'EVERSION:' + headers.version, req: req}); + + return setImmediate(cb, ['Peer is using incompatible version', headers.version, req.method, req.url].join(' ')); } + modules.peers.update(peer); + return setImmediate(cb, null, {body: res.body, peer: peer}); } }); request.catch(function (err) { if (peer) { - if (err.code === 'EUNAVAILABLE' || err.code === 'ETIMEOUT') { + if (err.code === 'EUNAVAILABLE') { // Remove peer __private.removePeer({peer: peer, code: err.code, req: req}); } else { @@ -515,12 +589,8 @@ Transport.prototype.sandboxApi = function (call, args, cb) { Transport.prototype.onBind = function (scope) { modules = scope; - __private.headers = { - os: modules.system.getOS(), - version: modules.system.getVersion(), - port: modules.system.getPort(), - nethash: modules.system.getNethash() - }; + __private.headers = modules.system.headers(); + __private.broadcaster.bind(modules); }; Transport.prototype.onBlockchainReady = function () { @@ -528,29 +598,35 @@ Transport.prototype.onBlockchainReady = function () { }; Transport.prototype.onSignature = function (signature, broadcast) { - if (broadcast) { - self.broadcast({limit: 100}, {api: '/signatures', data: {signature: signature}, method: 'POST'}); - library.network.io.sockets.emit('signature/change', {}); + if (broadcast && !__private.broadcaster.maxRelays(signature)) { + __private.broadcaster.enqueue({}, {api: '/signatures', data: {signature: signature}, method: 'POST'}); + library.network.io.sockets.emit('signature/change', signature); } }; Transport.prototype.onUnconfirmedTransaction = function (transaction, broadcast) { - if (broadcast) { - self.broadcast({limit: 100}, {api: '/transactions', data: {transaction: transaction}, method: 'POST'}); - library.network.io.sockets.emit('transactions/change', {}); + if (broadcast && !__private.broadcaster.maxRelays(transaction)) { + __private.broadcaster.enqueue({}, {api: '/transactions', data: {transaction: transaction}, method: 'POST'}); + library.network.io.sockets.emit('transactions/change', transaction); } }; Transport.prototype.onNewBlock = function (block, broadcast) { if (broadcast) { - self.broadcast({limit: 100}, {api: '/blocks', data: {block: block}, method: 'POST'}); - library.network.io.sockets.emit('blocks/change', {}); + var broadhash = modules.system.getBroadhash(); + + modules.system.update(function () { + if (!__private.broadcaster.maxRelays(block)) { + __private.broadcaster.broadcast({limit: constants.maxPeers, broadhash: broadhash}, {api: '/blocks', data: {block: block}, method: 'POST', immediate: true}); + } + library.network.io.sockets.emit('blocks/change', block); + }); } }; Transport.prototype.onMessage = function (msg, broadcast) { - if (broadcast) { - self.broadcast({limit: 100, dappid: msg.dappid}, {api: '/dapp/message', data: msg, method: 'POST'}); + if (broadcast && !__private.broadcaster.maxRelays(msg)) { + __private.broadcaster.broadcast({limit: constants.maxPeers, dappid: msg.dappid}, {api: '/dapp/message', data: msg, method: 'POST', immediate: true}); } }; @@ -564,7 +640,7 @@ shared.message = function (msg, cb) { msg.timestamp = (new Date()).getTime(); msg.hash = __private.hashsum(msg.body, msg.timestamp); - self.broadcast({limit: 100, dappid: msg.dappid}, {api: '/dapp/message', data: msg, method: 'POST'}); + __private.broadcaster.enqueue({dappid: msg.dappid}, {api: '/dapp/message', data: msg, method: 'POST'}); return setImmediate(cb, null, {}); }; diff --git a/package.json b/package.json index cdc1cd32e03..0dd07337cb3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "popsicle": "=8.2.0", "randomstring": "=1.1.5", "rimraf": "=2.5.4", + "semver": "=5.3.0", "socket.io": "=1.4.8", "strftime": "=0.9.2", "valid-url": "=1.0.9", diff --git a/public b/public index f151ca6c61f..3c340a808f4 160000 --- a/public +++ b/public @@ -1 +1 @@ -Subproject commit f151ca6c61f3a8bb821504259ea37b4047fc068f +Subproject commit 3c340a808f4900c70a8d3368f3d2520dddefb7de diff --git a/schema/accounts.js b/schema/accounts.js index 0bd3f091233..44f6db70c71 100644 --- a/schema/accounts.js +++ b/schema/accounts.js @@ -19,7 +19,9 @@ module.exports = { properties: { address: { type: 'string', - minLength: 1 + format: 'address', + minLength: 1, + maxLength: 22 } }, required: ['address'] @@ -30,7 +32,9 @@ module.exports = { properties: { address: { type: 'string', - minLength: 1 + format: 'address', + minLength: 1, + maxLength: 22 } }, required: ['address'] @@ -41,7 +45,8 @@ module.exports = { properties: { secret: { type: 'string', - minLength: 1 + minLength: 1, + maxLength: 100 } }, required: ['secret'] @@ -52,7 +57,9 @@ module.exports = { properties: { address: { type: 'string', - minLength: 1 + format: 'address', + minLength: 1, + maxLength: 22 } }, required: ['address'] @@ -63,7 +70,8 @@ module.exports = { properties: { secret: { type: 'string', - minLength: 1 + minLength: 1, + maxLength: 100 }, publicKey: { type: 'string', @@ -71,7 +79,8 @@ module.exports = { }, secondSecret: { type: 'string', - minLength: 1 + minLength: 1, + maxLength: 100 } } }, @@ -81,7 +90,9 @@ module.exports = { properties: { address: { type: 'string', - minLength: 1 + format: 'address', + minLength: 1, + maxLength: 22 } }, required: ['address'] diff --git a/schema/blocks.js b/schema/blocks.js index 87ace052ea0..99c3e8f0b71 100644 --- a/schema/blocks.js +++ b/schema/blocks.js @@ -13,7 +13,9 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['id'] @@ -24,7 +26,7 @@ module.exports = { properties: { limit: { type: 'integer', - minimum: 0, + minimum: 1, maximum: 100 }, orderBy: { @@ -53,11 +55,38 @@ module.exports = { minimum: 0 }, previousBlock: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, height: { - type: 'integer' + type: 'integer', + minimum: 1 } } + }, + getCommonBlock: { + id: 'blocks.getCommonBlock', + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 + }, + previousBlock: { + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 + }, + height: { + type: 'integer', + minimum: 1 + } + }, + required: ['id', 'previousBlock', 'height'] } }; diff --git a/schema/config.js b/schema/config.js new file mode 100644 index 00000000000..db8517f9aa2 --- /dev/null +++ b/schema/config.js @@ -0,0 +1,268 @@ +'use strict'; + +module.exports = { + config: { + id: 'appCon', + type: 'object', + properties: { + port: { + type: 'integer', + minimum: 1, + maximum: 65535 + }, + address: { + type: 'string', + format: 'ip' + }, + version: { + type: 'string', + format: 'version', + minLength: 5, + maxLength: 12 + }, + minVersion: { + type: 'string' + }, + fileLogLevel: { + type: 'string' + }, + logFileName: { + type: 'string' + }, + consoleLogLevel: { + type: 'string' + }, + trustProxy: { + type: 'boolean' + }, + topAccounts: { + type: 'boolean' + }, + db: { + type: 'object', + properties: { + host: { + type: 'string', + }, + port: { + type: 'integer', + minimum: 1, + maximum: 65535 + }, + database: { + type: 'string' + }, + user: { + type: 'string' + }, + password: { + type: 'string' + }, + poolSize: { + type: 'integer' + }, + poolIdleTimeout: { + type: 'integer' + }, + reapIntervalMillis: { + type: 'integer' + }, + logEvents: { + type: 'array' + } + }, + required: ['host', 'port', 'database', 'user', 'password', 'poolSize', 'poolIdleTimeout', 'reapIntervalMillis', 'logEvents'] + }, + api: { + type: 'object', + properties: { + access: { + type: 'object', + properties: { + whiteList: { + type: 'array' + } + }, + required: ['whiteList'] + }, + options: { + type: 'object', + properties: { + limits: { + type: 'object', + properties: { + max: { + type: 'integer' + }, + delayMs: { + type: 'integer' + }, + delayAfter: { + type: 'integer' + }, + windowMs: { + type: 'integer' + } + }, + required: ['max', 'delayMs', 'delayAfter', 'windowMs'] + } + }, + required: ['limits'] + } + }, + required: ['access', 'options'] + }, + peers: { + type: 'object', + properties: { + list: { + type: 'array' + }, + blackList: { + type: 'array' + }, + options: { + properties: { + limits: { + type: 'object', + properties: { + max: { + type: 'integer' + }, + delayMs: { + type: 'integer' + }, + delayAfter: { + type: 'integer' + }, + windowMs: { + type: 'integer' + } + }, + required: ['max', 'delayMs', 'delayAfter', 'windowMs'] + }, + timeout: { + type: 'integer' + } + }, + required: ['limits', 'timeout'] + } + }, + required: ['list', 'blackList', 'options'] + }, + broadcasts: { + type: 'object', + properties: { + broadcastInterval: { + type: 'integer', + minimum: 1000, + maximum: 60000 + }, + broadcastLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + }, + parallelLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + }, + releaseLimit: { + type: 'integer', + minimum: 1, + maximum: 25 + }, + relayLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + } + } + }, + forging: { + type: 'object', + properties: { + force: { + type: 'boolean' + }, + secret: { + type: 'array' + }, + access: { + type: 'object', + properties: { + whiteList: { + type: 'array' + } + }, + required: ['whiteList'] + } + }, + required: ['force', 'secret', 'access'] + }, + loading: { + type: 'object', + properties: { + verifyOnLoading: { + type: 'boolean' + }, + loadPerIteration: { + type: 'integer', + minimum: 1, + maximum: 5000 + } + }, + required: ['verifyOnLoading', 'loadPerIteration'] + }, + ssl: { + type: 'object', + properties: { + enabled: { + type: 'boolean' + }, + options: { + type: 'object', + properties: { + port: { + type: 'integer' + }, + address: { + type: 'string', + format: 'ip', + }, + key: { + type: 'string' + }, + cert: { + type: 'string' + } + }, + required: ['port', 'address', 'key', 'cert'] + } + }, + required: ['enabled', 'options'] + }, + dapp: { + type: 'object', + properties: { + masterrequired: { + type: 'boolean' + }, + masterpassword: { + type: 'string' + }, + autoexec: { + type: 'array' + } + }, + required: ['masterrequired', 'masterpassword', 'autoexec'] + }, + nethash: { + type: 'string', + format: 'hex' + } + }, + required: ['port', 'address', 'version', 'minVersion', 'fileLogLevel', 'logFileName', 'consoleLogLevel', 'trustProxy', 'topAccounts', 'db', 'api', 'peers', 'forging', 'loading', 'ssl', 'dapp', 'nethash'] + } +}; diff --git a/schema/dapps.js b/schema/dapps.js index 9841058999e..4b7d9fd0f02 100644 --- a/schema/dapps.js +++ b/schema/dapps.js @@ -64,7 +64,9 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['id'] @@ -75,6 +77,7 @@ module.exports = { properties: { id: { type: 'string', + format: 'id', minLength: 1, maxLength: 20 }, @@ -101,18 +104,18 @@ module.exports = { minLength: 1, maxLength: 2000 }, + orderBy: { + type: 'string', + minLength: 1 + }, limit: { type: 'integer', - minimum: 0, + minimum: 1, maximum: 100 }, offset: { type: 'integer', minimum: 0 - }, - orderBy: { - type: 'string', - minLength: 1 } } }, @@ -126,7 +129,9 @@ module.exports = { }, id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, master: { type: 'string', @@ -160,6 +165,7 @@ module.exports = { }, dappId: { type: 'string', + format: 'id', minLength: 1, maxLength: 20 }, @@ -186,7 +192,8 @@ module.exports = { }, recipientId: { type: 'string', - minLength: 2, + format: 'address', + minLength: 1, maxLength: 22 }, secondSecret: { @@ -196,11 +203,13 @@ module.exports = { }, dappId: { type: 'string', + format: 'id', minLength: 1, maxLength: 20 }, transactionId: { type: 'string', + format: 'id', minLength: 1, maxLength: 20 }, @@ -238,7 +247,9 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, master: { type: 'string', @@ -253,7 +264,9 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, master: { type: 'string', @@ -268,7 +281,9 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 }, master: { type: 'string', diff --git a/schema/delegates.js b/schema/delegates.js index 36dd2aaaf97..1b6b681f0c9 100644 --- a/schema/delegates.js +++ b/schema/delegates.js @@ -54,7 +54,10 @@ module.exports = { type: 'string' }, username: { - type: 'string' + type: 'string', + format: 'username', + minLength: 1, + maxLength: 20 } } }, @@ -134,7 +137,10 @@ module.exports = { maxLength: 100 }, username: { - type: 'string' + type: 'string', + format: 'username', + minLength: 1, + maxLength: 20 } }, required: ['secret'] diff --git a/schema/loader.js b/schema/loader.js index 2e476b3cd70..beb05e67d4d 100644 --- a/schema/loader.js +++ b/schema/loader.js @@ -7,18 +7,20 @@ module.exports = { properties: { signatures: { type: 'array', - uniqueItems: true + uniqueItems: true, + maxItems: 100 } }, required: ['signatures'] }, - loadUnconfirmedTransactions: { - id: 'loader.loadUnconfirmedTransactions', + loadTransactions: { + id: 'loader.loadTransactions', type: 'object', properties: { transactions: { type: 'array', - uniqueItems: true + uniqueItems: true, + maxItems: 100 } }, required: ['transactions'] @@ -29,8 +31,7 @@ module.exports = { type: 'object', properties: { peers: { - type: 'array', - uniqueItems: true + type: 'array' } }, required: ['peers'] @@ -54,10 +55,24 @@ module.exports = { maximum: 3 }, os: { - type: 'string' + type: 'string', + format: 'os', + minLength: 1, + maxLength: 64 }, version: { - type: 'string' + type: 'string', + format: 'version', + minLength: 5, + maxLength: 12 + }, + broadhash: { + type: 'string', + format: 'hex' + }, + height: { + type: 'integer', + minimum: 1 } }, required: ['ip', 'port', 'state'] @@ -68,7 +83,7 @@ module.exports = { properties: { height: { type: 'integer', - minimum: 0 + minimum: 1 } }, required: ['height'] diff --git a/schema/multisignatures.js b/schema/multisignatures.js index 98581b58257..cccc0cb00da 100644 --- a/schema/multisignatures.js +++ b/schema/multisignatures.js @@ -42,7 +42,10 @@ module.exports = { format: 'publicKey' }, transactionId: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['transactionId', 'secret'] diff --git a/schema/peers.js b/schema/peers.js index 0ac2287e414..7d83101cc9c 100644 --- a/schema/peers.js +++ b/schema/peers.js @@ -7,8 +7,7 @@ module.exports = { type: 'object', properties: { peers: { - type: 'array', - uniqueItems: true + type: 'array' } }, required: ['peers'] @@ -29,15 +28,27 @@ module.exports = { state: { type: 'integer', minimum: 0, - maximum: 3 + maximum: 2 }, os: { type: 'string', + format: 'os', + minLength: 1, maxLength: 64 }, version: { type: 'string', - maxLength: 11 + format: 'version', + minLength: 5, + maxLength: 12 + }, + broadhash: { + type: 'string', + format: 'hex' + }, + height: { + type: 'integer', + minimum: 1 } }, required: ['ip', 'port', 'state'] @@ -47,6 +58,10 @@ module.exports = { id: 'peer.getPeers', type: 'object', properties: { + ip: { + type: 'string', + format: 'ip' + }, port: { type: 'integer', minimum: 1, @@ -55,22 +70,34 @@ module.exports = { state: { type: 'integer', minimum: 0, - maximum: 3 + maximum: 2 }, os: { type: 'string', + format: 'os', + minLength: 1, maxLength: 64 }, version: { type: 'string', - maxLength: 11 + format: 'version', + minLength: 5, + maxLength: 12 + }, + broadhash: { + type: 'string', + format: 'hex' + }, + height: { + type: 'integer', + minimum: 1 }, orderBy: { type: 'string' }, limit: { type: 'integer', - minimum: 0, + minimum: 1, maximum: 100 }, offset: { diff --git a/schema/signatures.js b/schema/signatures.js index 80738f65702..2f8c8b62f6f 100644 --- a/schema/signatures.js +++ b/schema/signatures.js @@ -7,11 +7,13 @@ module.exports = { properties: { secret: { type: 'string', - minLength: 1 + minLength: 1, + maxLength: 100 }, secondSecret: { type: 'string', - minLength: 1 + minLength: 1, + maxLength: 100 }, publicKey: { type: 'string', diff --git a/schema/transactions.js b/schema/transactions.js index 2b05e34d9a4..514d7501fa1 100644 --- a/schema/transactions.js +++ b/schema/transactions.js @@ -8,24 +8,21 @@ module.exports = { type: 'object', properties: { blockId: { - type: 'string' - }, - limit: { - type: 'integer', - minimum: 0, - maximum: 100 + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, type: { type: 'integer', minimum: 0, maximum: 10 }, - orderBy: { - type: 'string' - }, - offset: { - type: 'integer', - minimum: 0 + senderId: { + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 }, senderPublicKey: { type: 'string', @@ -36,13 +33,16 @@ module.exports = { format: 'publicKey' }, ownerAddress: { - type: 'string' - }, - senderId: { - type: 'string' + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 }, recipientId: { - type: 'string' + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 }, amount: { type: 'integer', @@ -53,6 +53,18 @@ module.exports = { type: 'integer', minimum: 0, maximum: constants.fixedPoint + }, + orderBy: { + type: 'string' + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100 + }, + offset: { + type: 'integer', + minimum: 0 } } }, @@ -62,24 +74,28 @@ module.exports = { properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['id'] }, - getUnconfirmedTransaction: { - id: 'transactions.getUnconfirmedTransaction', + getPooledTransaction: { + id: 'transactions.getPooledTransaction', type: 'object', properties: { id: { type: 'string', - minLength: 1 + format: 'id', + minLength: 1, + maxLength: 20 } }, required: ['id'] }, - getUnconfirmedTransactions: { - id: 'transactions.getUnconfirmedTransactions', + getPooledTransactions: { + id: 'transactions.getPooledTransactions', type: 'object', properties: { senderPublicKey: { @@ -87,7 +103,10 @@ module.exports = { format: 'publicKey' }, address: { - type: 'string' + type: 'string', + format: 'address', + minLength: 1, + maxLength: 22 } } }, @@ -107,7 +126,9 @@ module.exports = { }, recipientId: { type: 'string', - minLength: 1 + format: 'address', + minLength: 1, + maxLength: 22 }, publicKey: { type: 'string', diff --git a/schema/transport.js b/schema/transport.js index 269199e9de9..99001698b42 100644 --- a/schema/transport.js +++ b/schema/transport.js @@ -1,5 +1,7 @@ 'use strict'; +var constants = require('../helpers/constants.js'); + module.exports = { headers: { id: 'transport.headers', @@ -16,18 +18,30 @@ module.exports = { }, os: { type: 'string', + format: 'os', + minLength: 1, maxLength: 64 }, + version: { + type: 'string', + format: 'version', + minLength: 5, + maxLength: 12 + }, nethash: { type: 'string', maxLength: 64 }, - version: { + broadhash: { type: 'string', - maxLength: 11 + format: 'hex' + }, + height: { + type: 'integer', + minimum: 1 } }, - required: ['ip', 'port', 'nethash', 'version'] + required: ['ip', 'port', 'version', 'nethash'] }, commonBlock: { id: 'transport.commonBlock', @@ -45,19 +59,49 @@ module.exports = { type: 'object', properties: { lastBlockId: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 } }, }, + transactions: { + id: 'transport.transactions', + type: 'object', + properties: { + transactions: { + type: 'array', + minItems: 1, + maxItems: 25 + } + }, + required: ['transactions'] + }, signatures: { id: 'transport.signatures', type: 'object', + properties: { + signatures: { + type: 'array', + minItems: 1, + maxItems: 25 + } + }, + required: ['signatures'] + }, + signature: { + id: 'transport.signature', + type: 'object', properties: { signature: { type: 'object', properties: { transaction: { - type: 'string' + type: 'string', + format: 'id', + minLength: 1, + maxLength: 20 }, signature: { type: 'string', diff --git a/sql/blocks.js b/sql/blocks.js index fca183c0a0c..f755195b7d8 100644 --- a/sql/blocks.js +++ b/sql/blocks.js @@ -73,9 +73,7 @@ var BlocksSql = { getBlockId: 'SELECT "id" FROM blocks WHERE "id" = ${id}', - getTransactionId: 'SELECT "id" FROM trs WHERE "id" = ${id}', - - simpleDeleteAfterBlock: 'DELETE FROM blocks WHERE "height" >= (SELECT "height" FROM blocks WHERE "id" = ${id});' + deleteAfterBlock: 'DELETE FROM blocks WHERE "height" >= (SELECT "height" FROM blocks WHERE "id" = ${id});' }; module.exports = BlocksSql; diff --git a/sql/loader.js b/sql/loader.js index a32b5ef0ffe..b777856c60f 100644 --- a/sql/loader.js +++ b/sql/loader.js @@ -3,13 +3,15 @@ var LoaderSql = { countBlocks: 'SELECT COUNT("rowId")::int FROM blocks', + getGenesisBlock: 'SELECT "id", "payloadHash", "blockSignature" FROM blocks WHERE "height" = 1', + countMemAccounts: 'SELECT COUNT(*)::int FROM mem_accounts WHERE "blockId" = (SELECT "id" FROM "blocks" ORDER BY "height" DESC LIMIT 1)', getMemRounds: 'SELECT "round" FROM mem_round GROUP BY "round"', - updateMemAccounts: 'UPDATE mem_accounts SET "u_isDelegate" = "isDelegate", "u_secondSignature" = "secondSignature", "u_username" = "username", "u_balance" = "balance", "u_delegates" = "delegates", "u_multisignatures" = "multisignatures";', + updateMemAccounts: 'UPDATE mem_accounts SET "u_isDelegate" = "isDelegate", "u_secondSignature" = "secondSignature", "u_username" = "username", "u_balance" = "balance", "u_delegates" = "delegates", "u_multisignatures" = "multisignatures", "u_multimin" = "multimin", "u_multilifetime" = "multilifetime";', - getOrphanedMemAccounts: 'SELECT a."blockId", b."id" FROM mem_accounts a LEFT OUTER JOIN blocks b ON b."id" = a."blockId" WHERE a."blockId" IS NOT NULL AND b."id" IS NULL', + getOrphanedMemAccounts: 'SELECT a."blockId", b."id" FROM mem_accounts a LEFT OUTER JOIN blocks b ON b."id" = a."blockId" WHERE a."blockId" IS NOT NULL AND a."blockId" != \'0\' AND b."id" IS NULL', getDelegates: 'SELECT ENCODE("publicKey", \'hex\') FROM mem_accounts WHERE "isDelegate" = 1' }; diff --git a/sql/migrations/20161016133824_addBroadhashColumnToPeers.sql b/sql/migrations/20161016133824_addBroadhashColumnToPeers.sql new file mode 100644 index 00000000000..3e292fab38d --- /dev/null +++ b/sql/migrations/20161016133824_addBroadhashColumnToPeers.sql @@ -0,0 +1,11 @@ +/* Add Broadhash Column to Peers + * + */ + +BEGIN; + +ALTER TABLE "peers" ADD COLUMN "broadhash" bytea; + +CREATE INDEX IF NOT EXISTS "peers_broadhash" ON "peers"("broadhash"); + +COMMIT; diff --git a/sql/migrations/20161016133824_addHeightColumnToPeers.sql b/sql/migrations/20161016133824_addHeightColumnToPeers.sql new file mode 100644 index 00000000000..eaca3d25cf0 --- /dev/null +++ b/sql/migrations/20161016133824_addHeightColumnToPeers.sql @@ -0,0 +1,11 @@ +/* Add Height Column to Peers + * + */ + +BEGIN; + +ALTER TABLE "peers" ADD COLUMN "height" INT; + +CREATE INDEX IF NOT EXISTS "peers_height" ON "peers"("height"); + +COMMIT; diff --git a/sql/multisignatures.js b/sql/multisignatures.js index 85834ddddd4..832c0cffd68 100644 --- a/sql/multisignatures.js +++ b/sql/multisignatures.js @@ -1,7 +1,7 @@ 'use strict'; var MultisignaturesSql = { - getAccounts: 'SELECT ARRAY_AGG("accountId") AS "accountId" FROM mem_accounts2multisignatures WHERE "dependentId" = ${publicKey}' + getAccountIds: 'SELECT ARRAY_AGG("accountId") AS "accountIds" FROM mem_accounts2multisignatures WHERE "dependentId" = ${publicKey}' }; module.exports = MultisignaturesSql; diff --git a/sql/peers.js b/sql/peers.js index a74870fc39e..d707cf83831 100644 --- a/sql/peers.js +++ b/sql/peers.js @@ -1,42 +1,40 @@ 'use strict'; var PeersSql = { - sortFields: ['ip', 'port', 'state', 'os', 'version'], + sortFields: ['ip', 'port', 'state', 'os', 'version', 'broadhash', 'height'], count: 'SELECT COUNT(*)::int FROM peers', - banManager: 'UPDATE peers SET "state" = 1, "clock" = null WHERE ("state" = 0 AND "clock" - ${now} < 0);', + banManager: 'UPDATE peers SET "state" = 1, "clock" = null WHERE ("state" = 0 AND "clock" - ${now} < 0)', getByFilter: function (params) { return [ - 'SELECT "ip", "port", "state", "os", "version" FROM peers', + 'SELECT "ip", "port", "state", "os", "version", ENCODE("broadhash", \'hex\') AS "broadhash", "height" FROM peers', (params.where.length ? 'WHERE ' + params.where.join(' AND ') : ''), - (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : 'ORDER BY random()'), + (params.sortField ? 'ORDER BY ' + [params.sortField, params.sortMethod].join(' ') : 'ORDER BY RANDOM()'), 'LIMIT ${limit} OFFSET ${offset}' ].filter(Boolean).join(' '); }, randomList: function (params) { return [ - 'SELECT p."ip", p."port", p."state", p."os", p."version" FROM peers p', + 'SELECT p."ip", p."port", p."state", p."os", p."version", ENCODE(p."broadhash", \'hex\') AS "broadhash", p."height" FROM peers p', (params.dappid ? 'INNER JOIN peers_dapp AS pd ON p."id" = pd."peerId" AND pd."dappid" = ${dappid}' : ''), - 'WHERE p."state" > 0 ORDER BY RANDOM() LIMIT ${limit}' + 'WHERE p."state" > 0', + (params.broadhash ? 'AND "broadhash" ' + (params.attempt === 0 ? '=' : '!=') + ' DECODE(${broadhash}, \'hex\')' : 'AND "broadhash" IS NULL'), + 'ORDER BY RANDOM(), "height" DESC LIMIT ${limit}' ].filter(Boolean).join(' '); }, - state: 'UPDATE peers SET "state" = ${state}, "clock" = ${clock} WHERE "ip" = ${ip} AND "port" = ${port};', + state: 'UPDATE peers SET "state" = ${state}, "clock" = ${clock} WHERE "ip" = ${ip} AND "port" = ${port}', - remove: 'DELETE FROM peers WHERE "ip" = ${ip} AND "port" = ${port};', + remove: 'DELETE FROM peers WHERE "ip" = ${ip} AND "port" = ${port}', getByIdPort: 'SELECT "id" FROM peers WHERE "ip" = ${ip} AND "port" = ${port}', - addDapp: 'INSERT INTO peers_dapp ("peerId", "dappid") VALUES (${peerId}, ${dappId}) ON CONFLICT DO NOTHING;', + addDapp: 'INSERT INTO peers_dapp ("peerId", "dappid") VALUES (${peerId}, ${dappId}) ON CONFLICT DO NOTHING', - upsertWithState: 'INSERT INTO peers ("ip", "port", "state", "os", "version") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "state", "os", "version") = (${ip}, ${port}, (CASE WHEN EXCLUDED."state" = 0 THEN EXCLUDED."state" ELSE ${state} END), ${os}, ${version})', - - upsertWithoutState: 'INSERT INTO peers ("ip", "port", "os", "version") VALUES (${ip}, ${port}, ${os}, ${version}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "os", "version") = (${ip}, ${port}, ${os}, ${version})', - - insertSeed: 'INSERT INTO peers("ip", "port", "state") VALUES(${ip}, ${port}, ${state}) ON CONFLICT DO NOTHING;', + upsert: 'INSERT INTO peers AS p ("ip", "port", "state", "os", "version", "broadhash", "height") VALUES (${ip}, ${port}, ${state}, ${os}, ${version}, ${broadhash}, ${height}) ON CONFLICT ("ip", "port") DO UPDATE SET ("ip", "port", "state", "os", "version", "broadhash", "height") = (${ip}, ${port}, (CASE WHEN p."state" = 0 THEN p."state" ELSE ${state} END), ${os}, ${version}, (CASE WHEN ${broadhash} IS NULL THEN p."broadhash" ELSE ${broadhash} END), (CASE WHEN ${height} IS NULL THEN p."height" ELSE ${height} END))' }; module.exports = PeersSql; diff --git a/sql/rounds.js b/sql/rounds.js index ae486fe52a6..886e0f17aac 100644 --- a/sql/rounds.js +++ b/sql/rounds.js @@ -5,12 +5,20 @@ var RoundsSql = { truncateBlocks: 'DELETE FROM blocks WHERE "height" > (${height})::bigint;', - updateMissedBlocks: 'UPDATE mem_accounts SET "missedblocks" = "missedblocks" + 1 WHERE "address" IN ($1:csv);', + updateMissedBlocks: function (backwards) { + return [ + 'UPDATE mem_accounts SET "missedblocks" = "missedblocks"', + (backwards ? '- 1' : '+ 1'), + 'WHERE "address" IN ($1:csv);' + ].join(' '); + }, getVotes: 'SELECT d."delegate", d."amount" FROM (SELECT m."delegate", SUM(m."amount") AS "amount", "round" FROM mem_round m GROUP BY m."delegate", m."round") AS d WHERE "round" = (${round})::bigint', updateVotes: 'UPDATE mem_accounts SET "vote" = "vote" + (${amount})::bigint WHERE "address" = ${address};', + updateBlockId: 'UPDATE mem_accounts SET "blockId" = ${newId} WHERE "blockId" = ${oldId};', + summedRound: 'SELECT SUM(b."totalFee")::bigint AS "fees", ARRAY_AGG(b."reward") AS "rewards", ARRAY_AGG(ENCODE(b."generatorPublicKey", \'hex\')) AS "delegates" FROM blocks b WHERE (SELECT (CAST(b."height" / ${activeDelegates} AS INTEGER) + (CASE WHEN b."height" % ${activeDelegates} > 0 THEN 1 ELSE 0 END))) = ${round}' }; diff --git a/sql/system.js b/sql/system.js new file mode 100644 index 00000000000..eef397194cf --- /dev/null +++ b/sql/system.js @@ -0,0 +1,7 @@ +'use strict'; + +var SystemSql = { + getBroadhash: 'SELECT "id" FROM blocks ORDER BY "height" DESC LIMIT ${limit}' +}; + +module.exports = SystemSql; diff --git a/test/api/accounts.js b/test/api/accounts.js index b5704271b6a..4a5b9b4b9c9 100644 --- a/test/api/accounts.js +++ b/test/api/accounts.js @@ -121,7 +121,7 @@ describe('GET /api/accounts/getBalance?address=', function () { it('using invalid address should fail', function (done) { getBalance('thisIsNOTALiskAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.eql('Invalid address'); + node.expect(res.body).to.have.property('error').to.eql('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); done(); }); }); @@ -161,7 +161,7 @@ describe('GET /api/accounts/getPublicKey?address=', function () { it('using invalid address should fail', function (done) { getPublicKey('thisIsNOTALiskAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.contain('Invalid address'); + node.expect(res.body).to.have.property('error').to.contain('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); done(); }); }); @@ -281,10 +281,10 @@ describe('GET /accounts?address=', function () { }); it('using invalid address should fail', function (done) { - getAccounts('thisIsNOTAValidLiskAddress', function (err, res) { + getAccounts('thisIsNOTALiskAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error'); - node.expect(res.body.error).to.contain('Invalid address'); + node.expect(res.body.error).to.contain('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); done(); }); }); diff --git a/test/api/blocks.js b/test/api/blocks.js index eaba0a8f2b8..881c47b9f26 100644 --- a/test/api/blocks.js +++ b/test/api/blocks.js @@ -12,6 +12,16 @@ var block = { var testBlocksUnder101 = false; +describe('GET /api/blocks/getBroadhash', function () { + + it('should be ok', function (done) { + node.get('/api/blocks/getBroadhash', function (err, res) { + node.expect(res.body).to.have.property('broadhash').to.be.a('string'); + done(); + }); + }); +}); + describe('GET /api/blocks/getEpoch', function () { it('should be ok', function (done) { @@ -116,6 +126,7 @@ describe('GET /api/blocks/getStatus', function () { it('should be ok', function (done) { node.get('/api/blocks/getStatus', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('broadhash').to.be.a('string'); node.expect(res.body).to.have.property('epoch').to.be.a('string'); node.expect(res.body).to.have.property('height').to.be.a('number'); node.expect(res.body).to.have.property('fee').to.be.a('number'); diff --git a/test/api/dapps.js b/test/api/dapps.js index 063730de381..ddabecd91ff 100644 --- a/test/api/dapps.js +++ b/test/api/dapps.js @@ -466,9 +466,10 @@ describe('PUT /api/dapps/withdrawal', function () { var validParams; beforeEach(function (done) { - var keys = node.lisk.crypto.getKeys(account.password); + var randomAccount = node.randomTxAccount(); + var keys = node.lisk.crypto.getKeys(randomAccount.password); var recipientId = node.lisk.crypto.getAddress(keys.publicKey); - var transaction = node.lisk.transaction.createTransaction('1L', 100000000, account.password); + var transaction = node.lisk.transaction.createTransaction(randomAccount.address, 100000000, account.password); validParams = { secret: account.password, @@ -481,8 +482,6 @@ describe('PUT /api/dapps/withdrawal', function () { done(); }); - var randomAccount = node.randomTxAccount(); - it('using no secret should fail', function (done) { delete validParams.secret; @@ -600,7 +599,7 @@ describe('PUT /api/dapps/withdrawal', function () { putWithdrawal(validParams, function (err, res) { node.expect(res.body).to.have.property('success').to.not.be.ok; - node.expect(res.body).to.have.property('error').to.equal('Application not found: 1L'); + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format id: 1L'); done(); }); }); @@ -660,7 +659,7 @@ describe('PUT /api/dapps/withdrawal', function () { putWithdrawal(validParams, function (err, res) { node.expect(res.body).to.have.property('success').to.not.be.ok; - node.expect(res.body).to.have.property('error').to.equal('Invalid outTransfer transactionId'); + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format id: 1L'); done(); }); }); @@ -710,7 +709,7 @@ describe('PUT /api/dapps/withdrawal', function () { putWithdrawal(validParams, function (err, res) { node.expect(res.body).to.have.property('success').to.not.be.ok; - node.expect(res.body).to.have.property('error').to.equal('String is too short (1 chars), minimum 2'); + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format address: 1'); done(); }); }); @@ -730,7 +729,7 @@ describe('PUT /api/dapps/withdrawal', function () { putWithdrawal(validParams, function (err, res) { node.expect(res.body).to.have.property('success').to.not.be.ok; - node.expect(res.body).to.have.property('error').to.equal('Invalid recipient'); + node.expect(res.body).to.have.property('error').to.match(/Object didn\'t pass validation for format address: [0-9]+/); done(); }); }); @@ -753,7 +752,7 @@ describe('PUT /api/dapps/withdrawal', function () { }); }); - it('using same params twice within current block should fail', function (done) { + it('using same valid params twice should fail', function (done) { putWithdrawal(validParams, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('transactionId').to.not.be.empty; diff --git a/test/api/delegates.js b/test/api/delegates.js index f9bcea81cc8..71281fb0833 100644 --- a/test/api/delegates.js +++ b/test/api/delegates.js @@ -51,7 +51,7 @@ describe('PUT /api/accounts/delegates without funds', function () { delegates: ['-' + node.eAccount.publicKey] }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.match(/Failed to remove vote/); + node.expect(res.body).to.have.property('error').to.match(/Account does not have enough LSK: [0-9]+L balance: 0/); done(); }); }); @@ -89,9 +89,7 @@ describe('PUT /api/accounts/delegates with funds', function () { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('transactionId'); node.expect(res.body.transactionId).to.be.not.empty; - node.onNewBlock(function (err) { - done(); - }); + done(); }); }); @@ -391,108 +389,6 @@ describe('GET /api/delegates', function () { }); }); - it('using string limit should fail', function (done) { - var limit = 'one'; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); - done(); - }); - }); - - it('using limit == -1 should fail', function (done) { - var limit = -1; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 1'); - done(); - }); - }); - - it('using limit == 0 should fail', function (done) { - var limit = 0; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Value 0 is less than minimum 1'); - done(); - }); - }); - - it('using limit == 1 should be ok', function (done) { - var limit = 1; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('delegates').that.is.an('array'); - node.expect(res.body.delegates).to.have.lengthOf(1); - done(); - }); - }); - - it('using limit == 101 should be ok', function (done) { - var limit = 101; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('delegates').that.is.an('array'); - node.expect(res.body.delegates).to.have.lengthOf(101); - done(); - }); - }); - - it('using limit > 101 should fail', function (done) { - var limit = 102; - var params = 'limit=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Value 102 is greater than maximum 101'); - done(); - }); - }); - - it('using string offset should fail', function (done) { - var limit = 'one'; - var params = 'offset=' + limit; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); - done(); - }); - }); - - it('using offset == 1 should be ok', function (done) { - var offset = 1; - var params = 'offset=' + offset; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('delegates').that.is.an('array'); - node.expect(res.body.delegates).to.have.lengthOf(101); - done(); - }); - }); - - it('using offset == -1 should fail', function (done) { - var offset = -1; - var params = 'offset=' + offset; - - node.get('/api/delegates?' + params, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 0'); - done(); - }); - }); - it('using orderBy == "unknown:asc" should fail', function (done) { var orderBy = 'unknown:asc'; var params = 'orderBy=' + orderBy; @@ -631,6 +527,108 @@ describe('GET /api/delegates', function () { done(); }); }); + + it('using string limit should fail', function (done) { + var limit = 'one'; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); + done(); + }); + }); + + it('using limit == -1 should fail', function (done) { + var limit = -1; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 1'); + done(); + }); + }); + + it('using limit == 0 should fail', function (done) { + var limit = 0; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value 0 is less than minimum 1'); + done(); + }); + }); + + it('using limit == 1 should be ok', function (done) { + var limit = 1; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); + node.expect(res.body.delegates).to.have.lengthOf(1); + done(); + }); + }); + + it('using limit == 101 should be ok', function (done) { + var limit = 101; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); + node.expect(res.body.delegates).to.have.lengthOf(101); + done(); + }); + }); + + it('using limit > 101 should fail', function (done) { + var limit = 102; + var params = 'limit=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value 102 is greater than maximum 101'); + done(); + }); + }); + + it('using string offset should fail', function (done) { + var limit = 'one'; + var params = 'offset=' + limit; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); + done(); + }); + }); + + it('using offset == 1 should be ok', function (done) { + var offset = 1; + var params = 'offset=' + offset; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('delegates').that.is.an('array'); + node.expect(res.body.delegates).to.have.lengthOf(101); + done(); + }); + }); + + it('using offset == -1 should fail', function (done) { + var offset = -1; + var params = 'offset=' + offset; + + node.get('/api/delegates?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 0'); + done(); + }); + }); }); describe('GET /api/delegates/count', function () { diff --git a/test/api/multisignatures.js b/test/api/multisignatures.js index cb6a5da5287..f1906c56332 100644 --- a/test/api/multisignatures.js +++ b/test/api/multisignatures.js @@ -343,17 +343,31 @@ describe('GET /api/multisignatures/pending', function () { var flag = 0; for (var i = 0; i < res.body.transactions.length; i++) { - if (res.body.transactions[i].transaction.senderPublicKey === multisigAccount.publicKey) { - flag += 1; - node.expect(res.body.transactions[i].transaction).to.have.property('type').to.equal(node.txTypes.MULTI); - node.expect(res.body.transactions[i].transaction).to.have.property('amount').to.equal(0); - node.expect(res.body.transactions[i].transaction).to.have.property('asset').that.is.an('object'); - node.expect(res.body.transactions[i].transaction).to.have.property('fee').to.equal(node.fees.multisignatureRegistrationFee * (Keys.length + 1)); - node.expect(res.body.transactions[i].transaction).to.have.property('id').to.equal(multiSigTx.txId); - node.expect(res.body.transactions[i].transaction).to.have.property('senderPublicKey').to.equal(multisigAccount.publicKey); - node.expect(res.body.transactions[i]).to.have.property('lifetime').to.equal(multiSigTx.lifetime); - node.expect(res.body.transactions[i]).to.have.property('min').to.equal(multiSigTx.min); - } + flag += 1; + + var pending = res.body.transactions[i]; + + node.expect(pending).to.have.property('max').that.is.equal(0); + node.expect(pending).to.have.property('min').that.is.equal(0); + node.expect(pending).to.have.property('lifetime').that.is.equal(0); + node.expect(pending).to.have.property('signed').that.is.true; + + node.expect(pending.transaction).to.have.property('type').that.is.equal(node.txTypes.MULTI); + node.expect(pending.transaction).to.have.property('amount').that.is.equal(0); + node.expect(pending.transaction).to.have.property('senderPublicKey').that.is.equal(multisigAccount.publicKey); + node.expect(pending.transaction).to.have.property('requesterPublicKey').that.is.null; + node.expect(pending.transaction).to.have.property('timestamp').that.is.a('number'); + node.expect(pending.transaction).to.have.property('asset').that.is.an('object'); + node.expect(pending.transaction.asset).to.have.property('multisignature').that.is.an('object'); + node.expect(pending.transaction.asset.multisignature).to.have.property('min').that.is.a('number'); + node.expect(pending.transaction.asset.multisignature).to.have.property('keysgroup').that.is.an('array'); + node.expect(pending.transaction.asset.multisignature).to.have.property('lifetime').that.is.a('number'); + node.expect(pending.transaction).to.have.property('recipientId').that.is.null; + node.expect(pending.transaction).to.have.property('signature').that.is.a('string'); + node.expect(pending.transaction).to.have.property('id').that.is.equal(multiSigTx.txId); + node.expect(pending.transaction).to.have.property('fee').that.is.equal(node.fees.multisignatureRegistrationFee * (Keys.length + 1)); + node.expect(pending.transaction).to.have.property('senderId').that.is.eql(multisigAccount.address); + node.expect(pending.transaction).to.have.property('receivedAt').that.is.a('string'); } node.expect(flag).to.equal(1); @@ -362,7 +376,7 @@ describe('GET /api/multisignatures/pending', function () { }); }); -describe('PUT /api/api/transactions/', function () { +describe('PUT /api/transactions', function () { it('when group transaction is pending should be ok', function (done) { sendLISKFromMultisigAccount(100000000, node.gAccount.address, function (err, transactionId) { @@ -382,6 +396,10 @@ describe('POST /api/multisignatures/sign (group)', function () { var validParams; + var passphrases = accounts.map(function (account) { + return account.password; + }); + beforeEach(function (done) { validParams = { secret: accounts[0].password, @@ -418,10 +436,6 @@ describe('POST /api/multisignatures/sign (group)', function () { }); it('using one less than total signatures should not confirm transaction', function (done) { - var passphrases = accounts.map(function (account) { - return account.password; - }); - confirmTransaction(multiSigTx.txId, passphrases.slice(0, (passphrases.length - 1)), function () { node.onNewBlock(function (err) { node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { @@ -432,11 +446,26 @@ describe('POST /api/multisignatures/sign (group)', function () { }); }); - it('using one more signature should confirm transaction', function (done) { - var passphrases = accounts.map(function (account) { - return account.password; + it('using same signature again should fail', function (done) { + node.post('/api/multisignatures/sign', validParams, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Transaction already signed'); + done(); }); + }); + it('using same signature again should not confirm transaction', function (done) { + node.post('/api/multisignatures/sign', validParams, function (err, res) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + done(); + }); + }); + }); + }); + + it('using one more signature should confirm transaction', function (done) { confirmTransaction(multiSigTx.txId, passphrases.slice(-1), function () { node.onNewBlock(function (err) { node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { @@ -452,18 +481,30 @@ describe('POST /api/multisignatures/sign (group)', function () { describe('POST /api/multisignatures/sign (transaction)', function () { + var validParams; + + var passphrases = accounts.map(function (account) { + return account.password; + }); + before(function (done) { sendLISKFromMultisigAccount(100000000, node.gAccount.address, function (err, transactionId) { multiSigTx.txId = transactionId; - done(); + node.onNewBlock(function (err) { + done(); + }); }); }); - it('using one less than minimum signatures should not confirm transaction', function (done) { - var passphrases = accounts.map(function (account) { - return account.password; - }); + beforeEach(function (done) { + validParams = { + secret: accounts[0].password, + transactionId: multiSigTx.txId + }; + done(); + }); + it('using one less than minimum signatures should not confirm transaction', function (done) { confirmTransaction(multiSigTx.txId, passphrases.slice(0, (multiSigTx.min - 1)), function () { node.onNewBlock(function (err) { node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { @@ -474,11 +515,26 @@ describe('POST /api/multisignatures/sign (transaction)', function () { }); }); - it('using one more signature should confirm transaction', function (done) { - var passphrases = accounts.map(function (account) { - return account.password; + it('using same signature again should fail', function (done) { + node.post('/api/multisignatures/sign', validParams, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Transaction already signed'); + done(); }); + }); + it('using same signature again should not confirm transaction', function (done) { + node.post('/api/multisignatures/sign', validParams, function (err, res) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + done(); + }); + }); + }); + }); + + it('using one more signature should confirm transaction', function (done) { confirmTransaction(multiSigTx.txId, passphrases.slice(-1), function () { node.onNewBlock(function (err) { node.get('/api/transactions/get?id=' + multiSigTx.txId, function (err, res) { diff --git a/test/api/peer.js b/test/api/peer.js index a419562816d..1791d29fea0 100644 --- a/test/api/peer.js +++ b/test/api/peer.js @@ -19,18 +19,33 @@ describe('GET /peer/list', function () { }); }); + it('using incompatible version in headers should fail', function (done) { + node.get('/peer/list') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + it('using valid headers should be ok', function (done) { node.get('/peer/list') .end(function (err, res) { node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('peers').that.is.an('array'); - node.expect(res.body.peers).to.have.length.of.at.least(1); res.body.peers.forEach(function (peer) { node.expect(peer).to.have.property('ip').that.is.a('string'); node.expect(peer).to.have.property('port').that.is.a('number'); node.expect(peer).to.have.property('state').that.is.a('number'); node.expect(peer).to.have.property('os'); node.expect(peer).to.have.property('version'); + node.expect(peer).to.have.property('broadhash'); + node.expect(peer).to.have.property('height'); }); done(); }); @@ -50,6 +65,19 @@ describe('GET /peer/height', function () { }); }); + it('using incompatible version in headers should fail', function (done) { + node.get('/peer/height') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + it('using valid headers should be ok', function (done) { node.get('/peer/height') .end(function (err, res) { diff --git a/test/api/peer.signatures.js b/test/api/peer.signatures.js index b6a1b76c3ba..7e1b72c05e4 100644 --- a/test/api/peer.signatures.js +++ b/test/api/peer.signatures.js @@ -2,80 +2,288 @@ var node = require('./../node.js'); +var owner = node.randomAccount(); +var coSigner1 = node.randomAccount(); +var coSigner2 = node.randomAccount(); + +function postTransaction (transaction, done) { + node.post('/peer/transactions', { + transaction: transaction + }, done); +} + +function postTransactions (transactions, done) { + node.post('/peer/transactions', { + transactions: transactions + }, done); +} + +function postSignature (transaction, signature, done) { + node.post('/peer/signatures', { + signature: { + transaction: transaction.id, + signature: signature + } + }, done); +} + describe('GET /peer/signatures', function () { - it('using incorrect nethash in headers should fail', function (done) { - node.get('/peer/signatures') - .set('nethash', 'incorrect') - .end(function (err, res) { - node.debug('> Response:'.grey, JSON.stringify(res.body)); - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body.expected).to.equal(node.config.nethash); - done(); - }); - }); - - it('using valid headers should be ok', function (done) { - node.get('/peer/signatures') - .end(function (err, res) { - node.debug('> Response:'.grey, JSON.stringify(res.body)); - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('signatures').that.is.an('array'); - done(); - }); - }); + it('using incorrect nethash in headers should fail', function (done) { + node.get('/peer/signatures') + .set('nethash', 'incorrect') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body.expected).to.equal(node.config.nethash); + done(); + }); + }); + + it('using incompatible version in headers should fail', function (done) { + node.get('/peer/signatures') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + + it('using valid headers should be ok', function (done) { + node.get('/peer/signatures') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('signatures').that.is.an('array'); + done(); + }); + }); }); describe('POST /peer/signatures', function () { - var validParams; - - var transaction = node.lisk.transaction.createTransaction('1L', 1, node.gAccount.password); - - beforeEach(function (done) { - validParams = { - signature: { - signature: transaction.signature, - transaction: transaction.id - } - }; - done(); - }); - - it('using incorrect nethash in headers should fail', function (done) { - node.post('/peer/signatures') - .set('nethash', 'incorrect') - .end(function (err, res) { - node.debug('> Response:'.grey, JSON.stringify(res.body)); - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body.expected).to.equal(node.config.nethash); - done(); - }); - }); - - it('using invalid signature schema should fail', function (done) { - delete validParams.signature.transaction; - - node.post('/peer/signatures', validParams) - .end(function (err, res) { - node.debug('> Response:'.grey, JSON.stringify(res.body)); - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Signature validation failed'); - done(); - }); - }); - - it('using unprocessable signature should fail', function (done) { - validParams.signature.transaction = 'invalidId'; - - node.post('/peer/signatures', validParams) - .end(function (err, res) { - node.debug('> Response:'.grey, JSON.stringify(res.body)); - node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.equal('Error processing signature'); - done(); - }); - }); - - it('using processable signature should be ok'); + var validParams; + + var transaction = node.lisk.transaction.createTransaction('1L', 1, node.gAccount.password); + + beforeEach(function (done) { + validParams = { + signature: { + signature: transaction.signature, + transaction: transaction.id + } + }; + done(); + }); + + it('using incorrect nethash in headers should fail', function (done) { + node.post('/peer/signatures') + .set('nethash', 'incorrect') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body.expected).to.equal(node.config.nethash); + done(); + }); + }); + + it('using incompatible version in headers should fail', function (done) { + node.post('/peer/signatures') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + + it('using invalid signature schema should fail', function (done) { + delete validParams.signature.transaction; + + node.post('/peer/signatures', validParams) + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Invalid signature body'); + done(); + }); + }); + + it('using unprocessable signature should fail', function (done) { + validParams.signature.transaction = '1'; + + node.post('/peer/signatures', validParams) + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Error processing signature: Transaction not found'); + done(); + }); + }); + + it('using processable signature should be ok'); + + describe('creating a new multisignature account', function () { + + var transaction; + + // Fund accounts + before(function (done) { + var transactions = []; + + node.async.eachSeries([owner, coSigner1, coSigner2], function (account, eachSeriesCb) { + transactions.push( + node.lisk.transaction.createTransaction(account.address, 100000000000, node.gAccount.password) + ); + eachSeriesCb(); + }, function (err) { + postTransactions(transactions, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.onNewBlock(function (err) { + done(); + }); + }); + }); + }); + + // Create multisignature group + before(function (done) { + var keysgroup = ['+' + coSigner1.publicKey, '+' + coSigner2.publicKey]; + var lifetime = 72; + var min = 2; + + transaction = node.lisk.multisignature.createMultisignature(owner.password, null, keysgroup, lifetime, min); + + postTransactions([transaction], function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.onNewBlock(function (err) { + done(); + }); + }); + }); + + it('using processable signature for owner should fail', function (done) { + var signature = node.lisk.multisignature.signTransaction(transaction, owner.password); + + postSignature(transaction, signature, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Error processing signature: Failed to verify signature'); + done(); + }); + }); + + it('using processable signature for coSigner1 should be ok', function (done) { + var signature = node.lisk.multisignature.signTransaction(transaction, coSigner1.password); + + postSignature(transaction, signature, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using processable signature for coSigner1 should not confirm the transaction', function (done) { + node.onNewBlock(function (err) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + done(); + }); + }); + }); + }); + + it('using processable signature for coSigner2 should be ok', function (done) { + var signature = node.lisk.multisignature.signTransaction(transaction, coSigner2.password); + + postSignature(transaction, signature, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using processable signature for coSigner2 should confirm the transaction', function (done) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transaction'); + node.expect(res.body.transaction).to.have.property('id').to.equal(transaction.id); + done(); + }); + }); + }); + }); + + describe('sending transaction from multisignature account', function () { + + var transaction; + + before(function (done) { + node.onNewBlock(done); + }); + + // Send transaction + before(function (done) { + transaction = node.lisk.multisignature.createTransaction('1L', 1, owner.password); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.onNewBlock(function (err) { + done(); + }); + }); + }); + + it('using processable signature for coSigner1 should be ok', function (done) { + var signature = node.lisk.multisignature.signTransaction(transaction, coSigner1.password); + + postSignature(transaction, signature, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using processable signature for coSigner1 should not confirm the transaction', function (done) { + node.onNewBlock(function (err) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + done(); + }); + }); + }); + }); + + it('using processable signature for coSigner2 should be ok', function (done) { + var signature = node.lisk.multisignature.signTransaction(transaction, coSigner2.password); + + postSignature(transaction, signature, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using processable signature for coSigner2 should confirm the transaction', function (done) { + node.onNewBlock(function (err) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transaction'); + node.expect(res.body.transaction).to.have.property('id').to.equal(transaction.id); + done(); + }); + }); + }); + }); + + describe('using multiple signatures', function () { + it('with unprocessable signature should fail'); + + it('with processable signature should be ok'); + }); }); diff --git a/test/api/peer.transactions.collision.js b/test/api/peer.transactions.collision.js new file mode 100644 index 00000000000..77591376d68 --- /dev/null +++ b/test/api/peer.transactions.collision.js @@ -0,0 +1,85 @@ +'use strict'; /*jslint mocha:true, expr:true */ + +var crypto = require('crypto'); +var node = require('./../node.js'); + +function postTransaction (transaction, done) { + node.post('/peer/transactions', { + transaction: transaction + }, done); +} + +describe('POST /peer/transactions', function () { + + describe('when two passphrases collide into the same address', function () { + + var collision = { + address: '13555181540209512417L', + passphrases: [ + 'merry field slogan sibling convince gold coffee town fold glad mix page', + 'annual youth lift quote off olive uncle town chief poverty extend series' + ] + }; + + before(function (done) { + var transaction = node.lisk.transaction.createTransaction(collision.address, 220000000, node.gAccount.password); + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.onNewBlock(done); + }); + }); + + describe('when transaction is invalid', function () { + + it('should fail for passphrase two', function (done) { + var transaction = node.lisk.transaction.createTransaction(node.gAccount.address, 100000000, collision.passphrases[1]); + transaction.signature = crypto.randomBytes(64).toString('hex'); + transaction.id = node.lisk.crypto.getId(transaction); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Invalid sender public key: b26dd40ba33e4785e49ddc4f106c0493ed00695817235c778f487aea5866400a expected: ce33db918b059a6e99c402963b42cf51c695068007ef01d8c383bb8a41270263'); + done(); + }); + }); + + it('should fail for passphrase one', function (done) { + var transaction = node.lisk.transaction.createTransaction(node.gAccount.address, 100000000, collision.passphrases[0]); + transaction.signature = crypto.randomBytes(64).toString('hex'); + transaction.id = node.lisk.crypto.getId(transaction); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Failed to verify signature'); + done(); + }); + }); + }); + + describe('when transaction is valid', function () { + + beforeEach(function (done) { + node.onNewBlock(done); + }); + + it('should be ok for passphrase one', function (done) { + var transaction = node.lisk.transaction.createTransaction(node.gAccount.address, 100000000, collision.passphrases[0]); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('should fail for passphrase two', function (done) { + var transaction = node.lisk.transaction.createTransaction(node.gAccount.address, 100000000, collision.passphrases[1]); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Invalid sender public key: b26dd40ba33e4785e49ddc4f106c0493ed00695817235c778f487aea5866400a expected: ce33db918b059a6e99c402963b42cf51c695068007ef01d8c383bb8a41270263'); + done(); + }); + }); + }); + }); +}); diff --git a/test/api/peer.transactions.delegates.js b/test/api/peer.transactions.delegates.js index 4af08544110..917d3cd9692 100644 --- a/test/api/peer.transactions.delegates.js +++ b/test/api/peer.transactions.delegates.js @@ -117,7 +117,7 @@ describe('POST /peer/transactions', function () { }); }); - describe('twice within the same block', function () { + describe('twice for the same account', function () { before(function (done) { sendLISK({ @@ -137,9 +137,11 @@ describe('POST /peer/transactions', function () { postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; - postTransaction(transaction2, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - done(); + node.onNewBlock(function () { + postTransaction(transaction2, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + done(); + }); }); }); }); diff --git a/test/api/peer.transactions.js b/test/api/peer.transactions.main.js similarity index 80% rename from test/api/peer.transactions.js rename to test/api/peer.transactions.main.js index 2da2efd38fa..8eb8b98067c 100644 --- a/test/api/peer.transactions.js +++ b/test/api/peer.transactions.main.js @@ -28,6 +28,19 @@ describe('GET /peer/transactions', function () { }); }); + it('using incompatible version in headers should fail', function (done) { + node.get('/peer/transactions') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + it('using valid headers should be ok', function (done) { node.get('/peer/transactions') .end(function (err, res) { @@ -51,8 +64,22 @@ describe('POST /peer/transactions', function () { }); }); + it('using incompatible version in headers should fail', function (done) { + node.post('/peer/transactions') + .set('version', '0.1.0a') + .end(function (err, res) { + node.debug('> Response:'.grey, JSON.stringify(res.body)); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Request is made from incompatible version'); + node.expect(res.body).to.have.property('expected').to.eql('0.0.0a'); + node.expect(res.body).to.have.property('received').to.eql('0.1.0a'); + done(); + }); + }); + it('using valid headers should be ok', function (done) { - var transaction = node.lisk.transaction.createTransaction('1L', 1, node.gAccount.password); + var account = node.randomAccount(); + var transaction = node.lisk.transaction.createTransaction(account.address, 1, node.gAccount.password); postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -61,23 +88,25 @@ describe('POST /peer/transactions', function () { }); }); - it('using already processed transaction should be ok', function (done) { - var transaction = node.lisk.transaction.createTransaction('1L', 1, node.gAccount.password); + it('using already processed transaction should fail', function (done) { + var account = node.randomAccount(); + var transaction = node.lisk.transaction.createTransaction(account.address, 1, node.gAccount.password); postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('transactionId').to.equal(transaction.id); postTransaction(transaction, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('transactionId').to.equal(transaction.id); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.match(/Transaction is already processed: [0-9]+/); done(); }); }); }); - it('using already confirmed transaction should be ok', function (done) { - var transaction = node.lisk.transaction.createTransaction('1L', 1, node.gAccount.password); + it('using already confirmed transaction should fail', function (done) { + var account = node.randomAccount(); + var transaction = node.lisk.transaction.createTransaction(account.address, 1, node.gAccount.password); postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -85,8 +114,8 @@ describe('POST /peer/transactions', function () { node.onNewBlock(function (err) { postTransaction(transaction, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('transactionId').to.equal(transaction.id); + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.match(/Transaction is already confirmed: [0-9]+/); done(); }); }); @@ -125,7 +154,17 @@ describe('POST /peer/transactions', function () { postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('message'); + node.expect(res.body).to.have.property('message').to.eql('Missing recipient'); + done(); + }); + }); + + it('using transaction with invalid recipientId should fail', function (done) { + var transaction = node.lisk.transaction.createTransaction('0123456789001234567890L', 1, node.gAccount.password); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.eql('Invalid transaction body'); done(); }); }); @@ -256,6 +295,12 @@ describe('POST /peer/transactions', function () { }); }); + describe('using multiple transactions', function () { + it('with invalid transaction should fail'); + + it('with valid transaction should be ok'); + }); + describe('when two passphrases collide into the same address', function () { var collision = { @@ -303,6 +348,10 @@ describe('POST /peer/transactions', function () { describe('when transaction is valid', function () { + beforeEach(function (done) { + node.onNewBlock(done); + }); + it('should be ok for passphrase one', function (done) { var transaction = node.lisk.transaction.createTransaction(node.gAccount.address, 100000000, collision.passphrases[0]); diff --git a/test/api/peer.transactions.signatures.js b/test/api/peer.transactions.signatures.js index 4d3a4dc66ac..fb19c4d8c46 100644 --- a/test/api/peer.transactions.signatures.js +++ b/test/api/peer.transactions.signatures.js @@ -108,17 +108,19 @@ describe('POST /peer/transactions', function () { postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Missing sender second signature'); done(); }); }); - it('using fake second passphrase should fail', function (done) { - var transaction = node.lisk.transaction.createTransaction('1L', 1, account.password, account2.secondPassword); + it('using fake second signature should fail', function (done) { + var transaction = node.lisk.transaction.createTransaction('1L', 1, account.password, account.secondPassword); transaction.signSignature = crypto.randomBytes(64).toString('hex'); transaction.id = node.lisk.crypto.getId(transaction); postTransaction(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('message').to.equal('Failed to verify second signature'); done(); }); }); diff --git a/test/api/peer.transactions.stress.js b/test/api/peer.transactions.stress.js new file mode 100644 index 00000000000..22837df2eda --- /dev/null +++ b/test/api/peer.transactions.stress.js @@ -0,0 +1,107 @@ +'use strict'; /*jslint mocha:true, expr:true */ + +var node = require('./../node.js'); + +function postTransaction (transaction, done) { + node.post('/peer/transactions', { + transaction: transaction + }, done); +} + +function postTransactions (transactions, done) { + node.post('/peer/transactions', { + transactions: transactions + }, done); +} + +describe('POST /peer/transactions', function () { + + describe('sending 1000 bundled transfers to random addresses', function () { + + var transactions = []; + var maximum = 1000; + var count = 1; + + before(function (done) { + node.async.doUntil(function (next) { + var bundled = []; + + for (var i = 0; i < node.config.broadcasts.releaseLimit; i++) { + var transaction = node.lisk.transaction.createTransaction( + node.randomAccount().address, + node.randomNumber(100000000, 1000000000), + node.gAccount.password + ); + + transactions.push(transaction); + bundled.push(transaction); + count++; + } + + postTransactions(bundled, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + next(); + }); + }, function () { + return (count >= maximum); + }, function (err) { + done(err); + }); + }); + + it('should confirm all transactions', function (done) { + var blocksToWait = maximum / node.constants.maxTxsPerBlock + 1; + node.waitForBlocks(blocksToWait, function (err) { + node.async.eachSeries(transactions, function (transaction, eachSeriesCb) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transaction').that.is.an('object'); + return setImmediate(eachSeriesCb); + }); + }, done); + }); + }).timeout(500000); + }); + + describe('sending 1000 single transfers to random addresses', function () { + + var transactions = []; + var maximum = 1000; + var count = 1; + + before(function (done) { + node.async.doUntil(function (next) { + var transaction = node.lisk.transaction.createTransaction( + node.randomAccount().address, + node.randomNumber(100000000, 1000000000), + node.gAccount.password + ); + + postTransaction(transaction, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactionId').to.equal(transaction.id); + transactions.push(transaction); + count++; + next(); + }); + }, function () { + return (count >= maximum); + }, function (err) { + done(err); + }); + }); + + it('should confirm all transactions', function (done) { + var blocksToWait = maximum / node.constants.maxTxsPerBlock + 1; + node.waitForBlocks(blocksToWait, function (err) { + node.async.eachSeries(transactions, function (transaction, eachSeriesCb) { + node.get('/api/transactions/get?id=' + transaction.id, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transaction').that.is.an('object'); + return setImmediate(eachSeriesCb); + }); + }, done); + }); + }).timeout(500000); + }); +}); diff --git a/test/api/peer.transactions.votes.js b/test/api/peer.transactions.votes.js index 52122d96ad2..b5b2565933b 100644 --- a/test/api/peer.transactions.votes.js +++ b/test/api/peer.transactions.votes.js @@ -91,7 +91,7 @@ describe('POST /peer/transactions', function () { }, done); }); - before(function (done) { + beforeEach(function (done) { getDelegates(function (err, res) { delegates = res.body.delegates.map(function (delegate) { return delegate.publicKey; @@ -103,7 +103,7 @@ describe('POST /peer/transactions', function () { }); }); - before(function (done) { + beforeEach(function (done) { getVotes(account.address, function (err, res) { votedDelegates = res.body.delegates.map(function (delegate) { return delegate.publicKey; @@ -144,36 +144,6 @@ describe('POST /peer/transactions', function () { }); }); - it('voting for a delegate and then removing again within same block should fail', function (done) { - node.onNewBlock(function (err) { - var transaction = node.lisk.vote.createVote(account.password, ['+' + delegate]); - postVote(transaction, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - - var transaction2 = node.lisk.vote.createVote(account.password, ['-' + delegate]); - postVote(transaction2, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - done(); - }); - }); - }); - }); - - it('removing votes from a delegate and then voting again within same block should fail', function (done) { - node.onNewBlock(function (err) { - var transaction = node.lisk.vote.createVote(account.password, ['-' + delegate]); - postVote(transaction, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - - var transaction2 = node.lisk.vote.createVote(account.password, ['+' + delegate]); - postVote(transaction2, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - done(); - }); - }); - }); - }); - it('voting twice for a delegate should fail', function (done) { async.series([ function (seriesCb) { @@ -181,19 +151,36 @@ describe('POST /peer/transactions', function () { var transaction = node.lisk.vote.createVote(account.password, ['+' + delegate]); postVote(transaction, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; - done(); + seriesCb(); }); }); }, function (seriesCb) { - node.onNewBlock(function (err) { - var transaction2 = node.lisk.vote.createVote(account.password, ['+' + delegate]); - postVote(transaction2, function (err, res) { - node.expect(res.body).to.have.property('success').to.be.not.ok; - done(); - }); + setTimeout(seriesCb, 1000); + }, + function (seriesCb) { + var transaction2 = node.lisk.vote.createVote(account.password, ['+' + delegate]); + postVote(transaction2, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + seriesCb(); }); }, + function (seriesCb) { + return node.onNewBlock(seriesCb); + }, + function (seriesCb) { + var transaction2 = node.lisk.vote.createVote(account.password, ['+' + delegate]); + postVote(transaction2, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + seriesCb(); + }); + }, + function (seriesCb) { + getVotes(account.address, function (err, res) { + node.expect(res.body).to.have.property('delegates').that.has.lengthOf(1); + seriesCb(err); + }); + } ], function (err) { return done(err); }); @@ -281,15 +268,17 @@ describe('POST /peer/transactions', function () { }); it('removing votes from 101 delegates separately should be ok', function (done) { - postVotes({ - delegates: delegates, - passphrase: account.password, - action: '-', - voteCb: function (err, res) { - node.expect(res.body).to.have.property('success').to.be.ok; - node.expect(res.body).to.have.property('transactionId').that.is.a('string'); - } - }, done); + node.onNewBlock(function (err) { + postVotes({ + delegates: delegates, + passphrase: account.password, + action: '-', + voteCb: function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactionId').that.is.a('string'); + } + }, done); + }); }); }); @@ -329,8 +318,14 @@ describe('POST /peer/transactions after registering a new delegate', function () }); }); - it('exceeding maximum of 101 votes within same block should fail', function (done) { + it('exceeding maximum of 101 votes should fail', function (done) { async.series([ + function (seriesCb) { + getVotes(account.address, function (err, res) { + node.expect(res.body).to.have.property('delegates').that.has.lengthOf(1); + seriesCb(err); + }); + }, function (seriesCb) { var slicedDelegates = delegates.slice(0, 76); node.expect(slicedDelegates).to.have.lengthOf(76); @@ -344,6 +339,9 @@ describe('POST /peer/transactions after registering a new delegate', function () } }, seriesCb); }, + function (seriesCb) { + return node.onNewBlock(seriesCb); + }, function (seriesCb) { var slicedDelegates = delegates.slice(-25); node.expect(slicedDelegates).to.have.lengthOf(25); @@ -357,6 +355,12 @@ describe('POST /peer/transactions after registering a new delegate', function () node.expect(res.body).to.have.property('message').to.equal('Maximum number of 101 votes exceeded (1 too many)'); seriesCb(); }); + }, + function (seriesCb) { + getVotes(account.address, function (err, res) { + node.expect(res.body).to.have.property('delegates').that.has.lengthOf(77); + seriesCb(err); + }); } ], function (err) { return done(err); diff --git a/test/api/peers.js b/test/api/peers.js index a47c0a22f43..7109e25ae64 100644 --- a/test/api/peers.js +++ b/test/api/peers.js @@ -16,25 +16,76 @@ describe('GET /api/peers/version', function () { describe('GET /api/peers', function () { - it('using empty parameters should fail', function (done) { - var params = [ - 'state=', - 'os=', - 'shared=', - 'version=', - 'limit=', - 'offset=', - 'orderBy=' - ]; + it('using invalid ip should fail', function (done) { + var ip = 'invalid'; + var params = 'ip=' + ip; - node.get('/api/peers?' + params.join('&'), function (err, res) { + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format ip: invalid'); + done(); + }); + }); + + it('using valid ip should be ok', function (done) { + var ip = '0.0.0.0'; + var params = 'ip=' + ip; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using port < 1 should fail', function (done) { + var port = 0; + var params = 'port=' + port; + + node.get('/api/peers?' + params, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error'); + node.expect(res.body).to.have.property('error').to.equal('Value 0 is less than minimum 1'); done(); }); }); - it('using state should be ok', function (done) { + it('using port == 65535 be ok', function (done) { + var port = 65535; + var params = 'port=' + port; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using port > 65535 should fail', function (done) { + var port = 65536; + var params = 'port=' + port; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value 65536 is greater than maximum 65535'); + done(); + }); + }); + + it('using state == 0 should be ok', function (done) { + var state = 0; + var params = 'state=' + state; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('peers').that.is.an('array'); + if (res.body.peers.length > 0) { + for (var i = 0; i < res.body.peers.length; i++) { + node.expect(res.body.peers[i].state).to.equal(parseInt(state)); + } + } + done(); + }); + }); + + it('using state == 1 should be ok', function (done) { var state = 1; var params = 'state=' + state; @@ -50,19 +101,221 @@ describe('GET /api/peers', function () { }); }); - it('using limit should be ok', function (done) { - var limit = 3; - var params = 'limit=' + limit; + it('using state == 2 should be ok', function (done) { + var state = 2; + var params = 'state=' + state; node.get('/api/peers?' + params, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('peers').that.is.an('array'); - node.expect(res.body.peers.length).to.be.at.most(limit); + if (res.body.peers.length > 0) { + for (var i = 0; i < res.body.peers.length; i++) { + node.expect(res.body.peers[i].state).to.equal(parseInt(state)); + } + } + done(); + }); + }); + + it('using state > 2 should fail', function (done) { + var state = 3; + var params = 'state=' + state; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value 3 is greater than maximum 2'); + done(); + }); + }); + + it('using os with length == 1 should be ok', function (done) { + var os = 'b'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os with length == 64 should be ok', function (done) { + var os = 'battle-claw-lunch-confirm-correct-limb-siege-erode-child-libert'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os with length > 64 should be ok', function (done) { + var os = 'battle-claw-lunch-confirm-correct-limb-siege-erode-child-liberty-'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('String is too long (65 chars), maximum 64'); + done(); + }); + }); + + it('using os == "freebsd10" should be ok', function (done) { + var os = 'freebsd10'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os == "freebsd10.3" should be ok', function (done) { + var os = 'freebsd10.3'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os == "freebsd10.3-" should be ok', function (done) { + var os = 'freebsd10.3-'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os == "freebsd10.3_" should be ok', function (done) { + var os = 'freebsd10.3_'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os == "freebsd10.3_RELEASE" should be ok', function (done) { + var os = 'freebsd10.3_RELEASE'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using os == "freebsd10.3_RELEASE-p7" should be ok', function (done) { + var os = 'freebsd10.3_RELEASE-p7'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; done(); }); }); - it('using orderBy should be ok', function (done) { + it('using os == "freebsd10.3_RELEASE-p7-@" should fail', function (done) { + var os = 'freebsd10.3_RELEASE-p7-@'; + var params = 'os=' + os; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format os: freebsd10.3_RELEASE-p7-@'); + done(); + }); + }); + + it('using version == "999.999.999" characters should be ok', function (done) { + var version = '999.999.999'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using version == "9999.999.999" characters should fail', function (done) { + var version = '9999.999.999'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format version: 9999.999.999'); + done(); + }); + }); + + it('using version == "999.9999.999" characters should fail', function (done) { + var version = '999.9999.999'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format version: 999.9999.999'); + done(); + }); + }); + + it('using version == "999.999.9999" characters should fail', function (done) { + var version = '999.999.9999'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format version: 999.999.9999'); + done(); + }); + }); + + it('using version == "999.999.999a" characters should be ok', function (done) { + var version = '999.999.999a'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using version == "999.999.999ab" characters should fail', function (done) { + var version = '999.999.999ab'; + var params = 'version=' + version; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format version: 999.999.999ab'); + done(); + }); + }); + + it('using invalid broadhash should fail', function (done) { + var broadhash = 'invalid'; + var params = 'broadhash=' + broadhash; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Object didn\'t pass validation for format hex: invalid'); + done(); + }); + }); + + it('using valid broadhash should be ok', function (done) { + var broadhash = node.config.nethash; + var params = 'broadhash=' + broadhash; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + done(); + }); + }); + + it('using orderBy == "state:desc" should be ok', function (done) { var orderBy = 'state:desc'; var params = 'orderBy=' + orderBy; @@ -72,8 +325,8 @@ describe('GET /api/peers', function () { if (res.body.peers.length > 0) { for (var i = 0; i < res.body.peers.length; i++) { - if (res.body.peers[i+1] != null) { - node.expect(res.body.peers[i+1].state).to.at.most(res.body.peers[i].state); + if (res.body.peers[i + 1] != null) { + node.expect(res.body.peers[i + 1].state).to.be.at.most(res.body.peers[i].state); } } } @@ -82,31 +335,102 @@ describe('GET /api/peers', function () { }); }); + it('using string limit should fail', function (done) { + var limit = 'one'; + var params = 'limit=' + limit; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); + done(); + }); + }); + + it('using limit == -1 should fail', function (done) { + var limit = -1; + var params = 'limit=' + limit; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 1'); + done(); + }); + }); + + it('using limit == 0 should fail', function (done) { + var limit = 0; + var params = 'limit=' + limit; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Value 0 is less than minimum 1'); + done(); + }); + }); + + it('using limit == 1 should be ok', function (done) { + var limit = 1; + var params = 'limit=' + limit; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('peers').that.is.an('array'); + node.expect(res.body.peers.length).to.be.at.most(limit); + done(); + }); + }); + + it('using limit == 100 should be ok', function (done) { + var limit = 100; + var params = 'limit=' + limit; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('peers').that.is.an('array'); + node.expect(res.body.peers.length).to.be.at.most(limit); + done(); + }); + }); + it('using limit > 100 should fail', function (done) { var limit = 101; var params = 'limit=' + limit; node.get('/api/peers?' + params, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error'); + node.expect(res.body).to.have.property('error').to.equal('Value 101 is greater than maximum 100'); done(); }); }); - it('using invalid parameters should fail', function (done) { - var params = [ - 'state=invalid', - 'os=invalid', - 'shared=invalid', - 'version=invalid', - 'limit=invalid', - 'offset=invalid', - 'orderBy=invalid' - ]; + it('using string offset should fail', function (done) { + var offset = 'one'; + var params = 'offset=' + offset; - node.get('/api/peers?' + params.join('&'), function (err, res) { + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.not.ok; + node.expect(res.body).to.have.property('error').to.equal('Expected type integer but found type string'); + done(); + }); + }); + + it('using offset == -1 should fail', function (done) { + var offset = -1; + var params = 'offset=' + offset; + + node.get('/api/peers?' + params, function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error'); + node.expect(res.body).to.have.property('error').to.equal('Value -1 is less than minimum 0'); + done(); + }); + }); + + it('using offset == 1 should be ok', function (done) { + var offset = 1; + var params = 'offset=' + offset; + + node.get('/api/peers?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; done(); }); }); diff --git a/test/api/transactions.js b/test/api/transactions.js index 4a38efa2738..b91b738d2fe 100644 --- a/test/api/transactions.js +++ b/test/api/transactions.js @@ -73,7 +73,7 @@ describe('GET /api/transactions', function () { var orderBy = 'amount:asc'; var params = [ - 'blockId=', + 'blockId=' + '1', 'senderId=' + node.gAccount.address, 'recipientId=' + account.address, 'limit=' + limit, @@ -247,6 +247,56 @@ describe('GET /api/transactions/get?id=', function () { }); }); +describe('GET /api/transactions/queued/get?id=', function () { + + it('using unknown id should be ok', function (done) { + var params = 'id=' + '1234'; + + node.get('/api/transactions/queued/get?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.false; + node.expect(res.body).to.have.property('error').that.is.equal('Transaction not found'); + done(); + }); + }); +}); + +describe('GET /api/transactions/queued', function () { + + it('should be ok', function (done) { + node.get('/api/transactions/queued', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body).to.have.property('count').that.is.an('number'); + done(); + }); + }); +}); + +describe('GET /api/transactions/multisignatures/get?id=', function () { + + it('using unknown id should be ok', function (done) { + var params = 'id=' + '1234'; + + node.get('/api/transactions/multisignatures/get?' + params, function (err, res) { + node.expect(res.body).to.have.property('success').to.be.false; + node.expect(res.body).to.have.property('error').that.is.equal('Transaction not found'); + done(); + }); + }); +}); + +describe('GET /api/transactions/multisignatures', function () { + + it('should be ok', function (done) { + node.get('/api/transactions/multisignatures', function (err, res) { + node.expect(res.body).to.have.property('success').to.be.ok; + node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body).to.have.property('count').that.is.an('number'); + done(); + }); + }); +}); + describe('GET /api/transactions/unconfirmed/get?id=', function () { it('using valid id should be ok', function (done) { @@ -271,6 +321,7 @@ describe('GET /api/transactions/unconfirmed', function () { node.get('/api/transactions/unconfirmed', function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; node.expect(res.body).to.have.property('transactions').that.is.an('array'); + node.expect(res.body).to.have.property('count').that.is.an('number'); done(); }); }); diff --git a/test/config.json b/test/config.json index 594b1c0e8bc..bf3503796da 100644 --- a/test/config.json +++ b/test/config.json @@ -1,18 +1,20 @@ { "port": 4000, "address": "0.0.0.0", - "version": "0.0.0", + "version": "0.0.0a", + "minVersion": "0.0.0a", "fileLogLevel": "info", "logFileName": "logs/lisk.log", "consoleLogLevel": "debug", "trustProxy": false, + "topAccounts": false, "db": { "host": "localhost", "port": 5432, "database": "lisk_test", - "user": null, + "user": "", "password": "password", - "poolSize": 20, + "poolSize": 95, "poolIdleTimeout": 30000, "reapIntervalMillis": 1000, "logEvents": [ @@ -42,10 +44,16 @@ "delayAfter": 0, "windowMs": 60000 }, - "maxUpdatePeers": 20, "timeout": 5000 } }, + "broadcasts": { + "broadcastInterval": 5000, + "broadcastLimit": 20, + "parallelLimit": 20, + "releaseLimit": 25, + "relayLimit": 2 + }, "forging": { "force": true, "secret": [ @@ -175,5 +183,5 @@ "masterpassword": "", "autoexec": [] }, - "nethash":"198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d" + "nethash": "198f2b61a8eb95fbeed58b8216780b68f697f26b849acf00c8c93bb9b24f783d" } diff --git a/test/index.js b/test/index.js index d59b248f0fc..86fe0fcaa5f 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,6 @@ +require('./unit/helpers/request-limiter.js'); +require('./unit/logic/blockReward.js'); + require('./api/accounts.js'); require('./api/blocks.js'); require('./api/dapps.js'); @@ -5,13 +8,11 @@ require('./api/delegates.js'); require('./api/loader.js'); require('./api/multisignatures.js'); require('./api/peer.js'); +require('./api/peer.transactions.main.js'); +require('./api/peer.transactions.collision.js'); require('./api/peer.transactions.delegates.js'); -require('./api/peer.transactions.js'); require('./api/peer.transactions.signatures.js'); require('./api/peer.transactions.votes.js'); require('./api/peers.js'); require('./api/signatures.js'); require('./api/transactions.js'); - -require('./unit/helpers/request-limiter.js'); -require('./unit/logic/blockReward.js'); diff --git a/test/lisk-js b/test/lisk-js index 7ef53f0b271..da73fa32d11 160000 --- a/test/lisk-js +++ b/test/lisk-js @@ -1 +1 @@ -Subproject commit 7ef53f0b27153c6adea49ce8eb7391bf924e9498 +Subproject commit da73fa32d113379321fca83f19fb251e46b9681d diff --git a/test/node.js b/test/node.js index 6fd058e0dac..bd6ef163ce8 100644 --- a/test/node.js +++ b/test/node.js @@ -29,7 +29,7 @@ node.api = node.supertest(node.baseUrl); node.normalizer = 100000000; // Use this to convert LISK amount to normal value node.blockTime = 10000; // Block time in miliseconds node.blockTimePlus = 12000; // Block time + 2 seconds in miliseconds -node.version = '0.0.0'; // Node version +node.version = node.config.version; // Node version // Transaction fees node.fees = { @@ -128,13 +128,24 @@ node.onNewBlock = function (cb) { if (err) { return cb(err); } else { - node.waitForNewBlock(height, cb); + node.waitForNewBlock(height, 2, cb); + } + }); +}; + +// Waits for (n) blocks to be created +node.waitForBlocks = function (blocksToWait, cb) { + node.getHeight(function (err, height) { + if (err) { + return cb(err); + } else { + node.waitForNewBlock(height, blocksToWait, cb); } }); }; // Waits for a new block to be created -node.waitForNewBlock = function (height, cb) { +node.waitForNewBlock = function (height, blocksToWait, cb) { var actualHeight = height; var counter = 1; @@ -149,7 +160,7 @@ node.waitForNewBlock = function (height, cb) { return cb(['Received bad response code', res.status, res.url].join(' ')); } - if (height + 2 === res.body.height) { + if (height + blocksToWait === res.body.height) { height = res.body.height; } @@ -177,25 +188,26 @@ node.waitForNewBlock = function (height, cb) { // Adds peers to local node node.addPeers = function (numOfPeers, cb) { var operatingSystems = ['win32','win64','ubuntu','debian', 'centos']; - var ports = [4000, 5000, 7000, 8000]; - - var os, version, port; + var port = 4000; + var os, version; var i = 0; node.async.whilst(function () { return i < numOfPeers; }, function (next) { os = operatingSystems[node.randomizeSelection(operatingSystems.length)]; - version = node.config.version; - port = ports[node.randomizeSelection(ports.length)]; + version = node.version; var request = node.popsicle.get({ url: node.baseUrl + '/peer/height', headers: { - version: version, - port: port, + broadhash: node.config.nethash, + height: 1, nethash: node.config.nethash, - os: os + os: os, + ip: '0.0.0.0', + port: port, + version: version } }); @@ -214,7 +226,10 @@ node.addPeers = function (numOfPeers, cb) { return next(err); }); }, function (err) { - return cb(err, {os: os, version: version, port: port}); + // Wait for peer to be swept to db + setTimeout(function () { + return cb(err, {os: os, version: version, port: port}); + }, 3000); }); }; @@ -309,6 +324,7 @@ function abstractRequest (options, done) { request.set('Accept', 'application/json'); request.set('version', node.version); request.set('nethash', node.config.nethash); + request.set('ip', '0.0.0.0'); request.set('port', node.config.port); request.expect('Content-Type', /json/);