Skip to content
This repository has been archived by the owner on Oct 30, 2018. It is now read-only.

Implement SIP6 #464

Merged
merged 55 commits into from
Aug 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c0d6af7
Start to fix offer timeouts
Jun 8, 2017
0813c46
Stub out test cases
Jun 14, 2017
7b003b0
Connect the dots
Jul 13, 2017
4918b45
Check for zero farmers
Jul 14, 2017
342082a
Fix response from publishContract and remove token
Jul 17, 2017
c922ee1
Create instances
Jul 17, 2017
960cfd9
Create instances earlier
Jul 17, 2017
a04a68a
Fix typo
Jul 17, 2017
deabca4
Stub methods for handling contact updates
Aug 2, 2017
462ede7
Update methods for contacts
Aug 10, 2017
361851e
Add pow middleware
Aug 10, 2017
dc15058
Remove extra file, start farmer auth middleware
Aug 10, 2017
df8f907
Spec out farmer auth middleware
Aug 10, 2017
fecb632
Change params for scrypt
Aug 10, 2017
0417cea
Add raw body for use in sighash
Aug 10, 2017
32cdf11
Rough implementation of farmer auth
Aug 10, 2017
01822bf
Add nodeID check
Aug 10, 2017
9957233
Add middleware to contact endpoints
Aug 10, 2017
a26a7e7
Timestamp check test
Aug 10, 2017
c4416a8
Test for nodeID check
Aug 10, 2017
0c29f90
Test for pubkey check
Aug 10, 2017
44389f9
Test farmer signature
Aug 10, 2017
0c76b7f
Tests for farmer auth middleware
Aug 10, 2017
36a7f7c
Rough implementation of contact endpoints
Aug 10, 2017
e38961f
Fix typos
aleitner Aug 24, 2017
724ecb8
Merge pull request #1 from aleitner/offers
braydonf Aug 24, 2017
f3ab00d
More fixes from integration testing
Aug 24, 2017
3a46555
Fix copy paste typo
Aug 25, 2017
925f000
Update tests from integration tests
Aug 25, 2017
7bfb207
Add activation of SIP6 for upload pointers
Aug 28, 2017
64e9f8b
Fix linting issues
Aug 28, 2017
a055ff3
;
Aug 28, 2017
a604091
Add tests for select farmers
Aug 28, 2017
9ad60b8
Set space available
Aug 28, 2017
f4359c8
Add tests for publish contract for sip6
Aug 28, 2017
c68bf4f
Update comment
Aug 29, 2017
8c28ede
Query for contacts with space available
Aug 29, 2017
d1f2bbd
Linting
Aug 29, 2017
d0fdc73
Configurable pow options
Aug 29, 2017
90e5a26
Check that timestamp and count are set during difficulty change
Aug 29, 2017
f711cc0
Set start pow settings
Aug 29, 2017
636d9a5
Fix this reference
Aug 29, 2017
55fb90b
Update storj-lib version
Aug 29, 2017
13c1503
Retarget on first run
Aug 29, 2017
e8964ed
Fix config ref
Aug 29, 2017
3aa7e37
Fix sorting bug
Aug 29, 2017
de27761
Initialize pow settings
Aug 30, 2017
ff7bce7
Fix callback
Aug 31, 2017
2e4b3b2
Add test for adjustment from init stats
Aug 31, 2017
c2e1149
Configurable
Aug 31, 2017
2868815
Bump storage models and adapter
Aug 31, 2017
82d8bf4
Merge branch 'master' into offers
Aug 31, 2017
ed37fdd
Bump to v6.0.0
Aug 31, 2017
6ff9582
Linting
Aug 31, 2017
99e9b79
Bump complex
Aug 31, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ const DEFAULTS = {
from: '[email protected]'
},
application: {
activateSIP6: false,
powOpts: {
retargetPeriod: 10000, // milliseconds
retargetCount: 10, // per retargetPeriod
},
publishBenchThreshold: 4000, // milliseconds
publishTotal: 36, // number of farmers to publish in active pool
publishBenchTotal: 9, // number of farmers to publish in bench pool
shardsPerMinute: 1000,
farmerTimeoutIgnore: '10m',
freeTier: {
Expand Down
6 changes: 6 additions & 0 deletions lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Config = require('./config');
const Storage = require('storj-service-storage-models');
const middleware = require('storj-service-middleware');
const Server = require('./server');
const pow = require('./server/middleware/pow');
const Mailer = require('storj-service-mailer');
const log = require('./logger');
const ComplexClient = require('storj-complex').createClient;
Expand Down Expand Up @@ -62,6 +63,11 @@ Engine.prototype.start = function(callback) {
this.redis = require('redis').createClient(this._config.redis);
this.redis.on('ready', () => {
log.info('connected to redis');
pow.checkInitTarget(this.redis, (err) => {
if (err) {
log.error('unable to initialize pow settings', err);
}
});
});
this.redis.on('error', (err) => {
log.error('error connecting to redis', err);
Expand Down
109 changes: 109 additions & 0 deletions lib/server/middleware/farmer-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

const errors = require('storj-service-error-types');
const crypto = require('crypto');
const secp256k1 = require('secp256k1');

const THRESHOLD = 300000;

function isHexString(a) {
if (typeof a !== 'string') {
return false;
}
return /^([0-9a-fA-F]{2})+$/.test(a);
}

function getSigHash(req) {
const hasher = crypto.createHash('sha256');
const timestamp = req.headers['x-node-timestamp'];
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
hasher.update(req.method);
hasher.update(url);
hasher.update(timestamp);
hasher.update(req.rawbody);
return hasher.digest();
}

function checkSig(req) {
const sighash = getSigHash(req);
let sigstr = req.headers['x-node-signature'];
if (!isHexString(sigstr)) {
return false;
}
const buf = Buffer.from(req.headers['x-node-signature'], 'hex');
const sig = secp256k1.signatureImport(buf);
const pubkey = Buffer.from(req.headers['x-node-pubkey'], 'hex');
return secp256k1.verify(sighash, sig, pubkey);
}

function checkPubkey(pubkey) {
if (!isHexString(pubkey)) {
return false;
}
const buf = Buffer.from(pubkey, 'hex');
return secp256k1.publicKeyVerify(buf);
}

function checkTimestamp(ts) {
const timestamp = parseInt(ts);
if (!Number.isSafeInteger(timestamp)) {
return false;
}
const now = Date.now();
if (timestamp < now - THRESHOLD || timestamp > now + THRESHOLD) {
return false;
}
return true;
}

function checkNodeID(nodeID, pubkey) {
if (!nodeID || nodeID.length !== 40 || !isHexString(nodeID)) {
return false;
}
const sha256 = crypto.createHash('sha256');
const rmd160 = crypto.createHash('rmd160');
sha256.update(Buffer.from(pubkey, 'hex'));
rmd160.update(sha256.digest());
if (rmd160.digest('hex') !== nodeID) {
return false;
}
return true;
}

function authFarmer(req, res, next) {
const nodeID = req.headers['x-node-id'];
const timestamp = req.headers['x-node-timestamp'];
const pubkey = req.headers['x-node-pubkey'];

if (!module.exports.checkTimestamp(timestamp)) {
return next(new errors.BadRequestError('Invalid timestamp header'));
}

if (!module.exports.checkPubkey(pubkey)) {
return next(new errors.BadRequestError('Invalid pubkey header'));
}

if (!module.exports.checkNodeID(nodeID, pubkey)) {
return next(new errors.BadRequestError('Invalid nodeID header'));
}

if (!req.rawbody || !Buffer.isBuffer(req.rawbody)) {
return next(new errors.BadRequestError('Invalid body'));
}

if (!module.exports.checkSig(req)) {
return next(new errors.BadRequestError('Invalid signature header'));
}

next();
}

module.exports = {
authFarmer: authFarmer,
getSigHash: getSigHash,
isHexString: isHexString,
checkTimestamp: checkTimestamp,
checkNodeID: checkNodeID,
checkPubkey: checkPubkey,
checkSig: checkSig
};
190 changes: 190 additions & 0 deletions lib/server/middleware/pow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
'use strict';

const async = require('async');
const assert = require('assert');
const scrypt = require('scrypt');
const crypto = require('crypto');
const BN = require('bn.js');

const CHALLENGE_TTL_SECONDS = 3600;
const MAX = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
const MAXBN = new BN(MAX, 16);

function getPOWMiddleware(db) {

return function(req, res, next) {

const challenge = req.headers['x-challenge'];
const nonce = req.headers['x-challenge-nonce'];

let salt = Buffer.alloc(8, 0);
salt.writeDoubleBE(parseInt(nonce));

const key = 'contact-' + challenge;
const scryptOpts = { N: Math.pow(2, 10), r: 1, p: 1 };

db.get(key, function(err, target) {
if (err) {
return next(err);
}

if (!target) {
return next(new Error('Challenge not found'));
}

scrypt.hash(challenge, scryptOpts, 32, salt, function(err, result) {
if (err) {
return next(err);
}

// Check the proof of work
if (result.toString('hex') > target) {
return next(new Error('Invalid proof of work'));
}

// Increase the count and remove the challenge
db.hincrby('contact-stats', 'count', 1);
db.del(key);

return next();
});
});
};
}

function checkInitTarget(db, callback) {
db.hgetall('contact-stats', function(err, stats) {
if (err) {
callback(err);
} else if (!stats) {
module.exports.initTarget(db, callback);
} else {
callback();
}
});
}

function initTarget(db, callback) {
const initialTarget = 'fffffffffffffffffffffffffffffff' +
'fffffffffffffffffffffffffffffffff';
async.series([
(next) => {
db.hset('contact-stats', 'target', initialTarget, next);
},
(next) => {
db.hset('contact-stats', 'timestamp', 0, next);
},
(next) => {
db.hset('contact-stats', 'count', 0, next);
},
], callback);
}

function getTarget(db, opts, callback) {

const precision = 100000000;
const precisionBN = new BN(precision);
const retargetPeriod = opts.retargetPeriod;
const count = opts.retargetCount;

assert(Number.isSafeInteger(count),
'retargetCount is expected to be a safe integer');
assert(Number.isSafeInteger(retargetPeriod),
'retargetPeriod is expected to be a safe integer');

/* jshint maxstatements: 100 */
db.hgetall('contact-stats', function(err, stats) {
if (err) {
return callback(err);
}

if (!stats) {
return callback(new Error('Unknown pow settings'));
}

if (!Number.isSafeInteger(parseInt(stats.count)) ||
!Number.isSafeInteger(parseInt(stats.timestamp))) {
return callback(new Error('Invalid pow settings'));
}

const now = Date.now();
const actual = parseInt(stats.count) || 1;
const timestamp = parseInt(stats.timestamp);

if (now > timestamp + retargetPeriod) {
const timeDelta = now - timestamp;
const expectedRatio = retargetPeriod / count;
const actualRatio = timeDelta / actual;
const adjustmentAmount = actualRatio / expectedRatio;

let adjustmentNum = adjustmentAmount * precision;
let adjustment = null;
if (adjustmentNum > Number.MAX_SAFE_INTEGER) {
adjustment = new BN(adjustmentAmount);
adjustment.mul(new BN(precision));
} else {
adjustment = new BN(adjustmentNum);
}

const target = new BN(stats.target, 16);
const newTargetBN = target.mul(adjustment).div(precisionBN);
let newTarget = null;
if (newTargetBN.cmp(MAXBN) > 0) {
newTarget = MAXBN.toString(16, 32);
} else {
newTarget = newTargetBN.toString(16, 32);
}

async.series([
(next) => {
db.hset('contact-stats', 'target', newTarget, next);
},
(next) => {
db.hset('contact-stats', 'timestamp', now, next);
},
(next) => {
db.hset('contact-stats', 'count', 0, next);
},
], (err) => {
if (err) {
return callback(err);
}
callback(null, newTarget);
});

} else {
callback(null, stats.target);
}

});

}

function getChallenge(db, opts, callback) {
module.exports.getTarget(db, opts, function(err, target) {
if (err) {
return callback(err);
}

const challenge = crypto.randomBytes(32).toString('hex');
const key = 'contact-' + challenge;

db.set(key, target, 'EX', CHALLENGE_TTL_SECONDS, function(err) {
if (err) {
return callback(err);
}
callback(null, {
challenge: challenge,
target: target
});
});
});
}

module.exports = {
getChallenge: getChallenge,
getTarget: getTarget,
checkInitTarget: checkInitTarget,
initTarget: initTarget,
getPOWMiddleware: getPOWMiddleware
};
22 changes: 22 additions & 0 deletions lib/server/middleware/raw-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const concat = require('concat-stream');

module.exports = function rawbody(req, res, next) {
// Do not buffer the request body for file uploads
if (req.get('Content-Type') === 'multipart/form-data') {
return next();
}

req.pipe(concat(function(data) {
req.rawbody = data;

try {
req.body = JSON.parse(req.rawbody.toString());
} catch (err) {
req.body = {};
}

next();
}));
};
Loading