Commit 0f394695 authored by Cédric Moreau's avatar Cédric Moreau

Refactoring: (huge changes) now handle multiple community algos

parent d1bf6504
......@@ -54,28 +54,6 @@ function RegistryBinding (registryServer, conf) {
}, null, " "));
};
this.amendmentCurrent = function (req, res) {
var am = ContractService.proposed();
req.params.am_number = ((am && am.number) || 0).toString();
that.amendmentNext(req, res);
};
this.amendmentNext = function (req, res) {
async.waterfall([
function (next){
ParametersService.getAmendmentNumber(req, next);
},
function (amNumber, next){
Amendment.getTheOneToBeVoted(amNumber, conf.sync.Algorithm, next);
},
], function (err, am) {
http.answer(res, 404, err, function () {
// Render the amendment
res.end(JSON.stringify(am.json(), null, " "));
});
});
};
this.membershipPost = function (req, res) {
var onError = http400(res);
http2raw.membership(req, onError)
......@@ -251,6 +229,27 @@ function RegistryBinding (registryServer, conf) {
});
}
this.askSelf = function (req, res) {
async.waterfall([
function (next){
ParametersService.getAmendmentNumberAndAlgo(req, next);
},
function (amNumber, algo, next){
if (amNumber == 0)
SyncService.getOrCreateAM0(algo, next);
else
Amendment.getTheOneToBeVoted(amNumber, algo, next);
},
], function (err, am) {
http.answer(res, 404, err, function () {
// Render the amendment
res.end(JSON.stringify(am.json(), null, " "));
});
});
};
this.askFlow = function (req, res) {
var that = this;
async.waterfall([
......
var async = require('async');
var common = require('./common');
var hexstrdump = require('../../hexstrdump');
var jpgp = require('../../jpgp');
var parsers = require('../../streams/parsers/doc');
module.exports = function (isMemberFunc, getPubkeyFunc) {
return function (pkey, ctx, amNext, done) {
async.waterfall([
function (next){
parsers.parsePubkey(next).asyncWrite(pkey.raw, next);
},
function (betterPubkey, next) {
async.detect(betterPubkey.udid2s, function (udid2, cb) {
var nbMatching = 0;
async.forEach(udid2.signatures || [], function (certification, cb2) {
var issuer = hexstrdump(certification.issuerKeyId.bytes).toUpperCase();
async.waterfall([
function (next){
getPubkeyFunc(issuer, next);
},
function (issuerPubkey, next){
isMemberFunc(issuerPubkey.fingerprint, function (err, isOK) {
next(err || (!isOK && "Signatory is not a member"), issuerPubkey);
});
},
function (issuerPubkey, next) {
var certSignatory = jpgp().certificate(issuerPubkey.raw);
var certOwner = jpgp().certificate(pkey.raw);
var verified = certification.verify(certSignatory.key.primaryKey, { userid: udid2.user.userId, key: certOwner.key.primaryKey });
next((!verified && "Certification verification gives FALSE") || null, verified);
}
], function (err, verified) {
if (verified) nbMatching++;
cb2(err);
});
}, function (err) {
cb(nbMatching >= 1);
});
}, function (detected) {
if (detected != undefined)
next(null, { nbVerifiedSigs: 1 });
else
next(null, { nbVerifiedSigs: 0 });
});
},
function (virtualPubkey, next){
common.computeIndicators(virtualPubkey, ctx, amNext, context2AnalyticalMembership, context2AnalyticalVoting, next);
},
], done);
};
}
var VTExpires = 3600*24*90; // Every 90 days
/**
* Converts member context vars to analytical expression parameters (for computing functions' namespace)
*/
function context2AnalyticalMembership (pubkey, context, done) {
var ctx = context || { currentMembership: null, nextMembership: null };
var isMember = ctx.currentMembership && ctx.currentMembership.membership == 'IN';
var ms = [
isMember ? 1 : 0,
!isMember ? 1 : 0,
];
var hasInvalidKey = (pubkey.nbVerifiedSigs || 0) < 1;
var hasNextIn = ctx.nextMembership && ctx.nextMembership.membership == 'IN';
var hasNextOut = ctx.nextMembership && ctx.nextMembership.membership == 'OUT';
var p = [
hasInvalidKey ? 1 : 0,
hasNextIn ? 1 : 0,
hasNextOut ? 1 : 0,
];
done(null, ms, p);
}
/**
* Converts voter context vars to analytical expression parameters (for computing functions' namespace)
*/
function context2AnalyticalVoting (context, amNext, memberLeaving, done) {
var ctx = context || { currentVoting: null, nextVoting: null };
var isVoter = ctx.currentVoting;
var isTooOldVT = (isVoter && ctx.currentVoting.date < getVTExclusionDate(amNext));
var vt = [
isVoter ? 0 : 1,
isVoter ? 1 : 0,
isTooOldVT ? 1 : 0
];
var hasNextVoting = ctx.nextVoting;
var p = [
1,
hasNextVoting ? 1 : 0,
memberLeaving == 1 ? 1 : 0
];
done(null, vt, p);
}
function getVTExclusionDate (amNext) {
var nextTimestamp = amNext.generated;
var exclusionDate = new Date();
exclusionDate.setTime(nextTimestamp*1000 - VTExpires*1000);
return exclusionDate;
}
var common = require('./common');
module.exports = function (pubkey, ctx, amNext, done) {
common.computeIndicators(pubkey, ctx, amNext, context2AnalyticalMembership, context2AnalyticalVoting, done);
}
var VTExpires = 3600*24*90; // Every 90 days
/**
* Converts member context vars to analytical expression parameters (for computing functions' namespace)
*/
function context2AnalyticalMembership (pubkey, context, done) {
var ctx = context || { currentMembership: null, nextMembership: null };
var isMember = ctx.currentMembership && ctx.currentMembership.membership == 'IN';
var ms = [
isMember ? 1 : 0,
!isMember ? 1 : 0,
];
var hasInvalidKey = false; // Key never expires in such algorithm
var hasNextIn = ctx.nextMembership && ctx.nextMembership.membership == 'IN';
var hasNextOut = ctx.nextMembership && ctx.nextMembership.membership == 'OUT';
var p = [
hasInvalidKey ? 1 : 0,
hasNextIn ? 1 : 0,
hasNextOut ? 1 : 0,
];
done(null, ms, p);
}
/**
* Converts voter context vars to analytical expression parameters (for computing functions' namespace)
*/
function context2AnalyticalVoting (context, amNext, memberLeaving, done) {
var ctx = context || { currentVoting: null, nextVoting: null };
var isVoter = ctx.currentVoting;
var isTooOldVT = (isVoter && ctx.currentVoting.date < getVTExclusionDate(amNext));
var vt = [
isVoter ? 0 : 1,
isVoter ? 1 : 0,
isTooOldVT ? 1 : 0
];
var hasNextVoting = ctx.nextVoting;
var p = [
1,
hasNextVoting ? 1 : 0,
memberLeaving == 1 ? 1 : 0
];
done(null, vt, p);
}
function getVTExclusionDate (amNext) {
var nextTimestamp = amNext.generated;
var exclusionDate = new Date();
exclusionDate.setTime(nextTimestamp*1000 - VTExpires*1000);
return exclusionDate;
}
var async = require('async');
var computing = require('./computing');
module.exports = { computeIndicators: computeIndicators };
/**
* Compute member's indicators according to a given context.
*/
function computeIndicators (pubkey, ctx, amNext, context2AnalyticalMembership, context2AnalyticalVoting, done) {
var res = {};
async.waterfall([
async.apply(context2AnalyticalMembership, pubkey, ctx),
async.apply(computing.Membership.Delta),
function (msIndicator, next){
res.membership = Math.max(-1, Math.min(1, msIndicator));
context2AnalyticalVoting(ctx, amNext, res.membership == -1, next);
},
async.apply(computing.Voting),
function (vtIndicator, next) {
res.key = vtIndicator;
next();
}
], function (err) {
// Mark out indicators to -1 and 1
res.key = Math.max(-1, Math.min(1, res.key));
done(err, res);
});
}
var Computing = { Membership: {}, Voting: {} };
/**
* Computes changes for a key, given its current state + changes.
* @parameter ms Array of 4 Integers: [currentNone, currentIn, currentOut, currentInTooOld].
* Each integer is either 1 or 0:
* * currentNone: 1 if current membership of a key doesn't exist, 0 otherwise
* * currentIn: 1 if current membership of a key is a valid IN, 0 otherwise
* * currentOut: 1 if current membership of a key is OUT, 0 otherwise
* * currentInTooOld: 1 if current membership of a key is a too old IN, 0 otherwise
* __Sum of those 4 integers MUST always be 1.__
*
* @parameter p Array of 4 Integers: [newNone, newIn, newOut, newInCancelled, newOutCancelled].
* Each integer is either 1 or 0:
* * newNone: 1 if new membership of a key doesn't exist, 0 otherwise
* * newIn: 1 if new membership of a key is IN, 0 otherwise
* * newOut: 1 if new membership of a key is OUT, 0 otherwise
* * newInCancelled: 1 if new membership of a key is IN, which has been cancelled, 0 otherwise
* * newOutCancelled: 1 if new membership of a key is OUT, which has been cancelled, 0 otherwise
*/
Computing.Membership.Delta = function (ms, p, done) {
if (ms.reduce(function(a,b){ return a + b; }) !== 1) {
done('Wrong membership state array: should be either in, out, in too old or no membership at all.');
}
/**
* Computes changes for a key given changes with and initial state
* @param m 1 if initial state is no membership, 0 otherwise
* @param p array of changes
*/
function IsMember (p) {
return - 2*p[0] - p[2];
}
/**
* Computes changes for a key given changes with and initial state
* @param m 1 if initial state is no membership, 0 otherwise
* @param p array of changes
*/
function IsNotMember (p) {
return - 2*p[0] + p[1];
}
done(null, ms[0]*IsMember(p) + ms[1]*IsNotMember(p));
}
/**
* Computes changes for a voter, using given voting & membership events.
* @parameter vt Array of 2 Integers: [wasNotVoter, wasVoter].
* Each integer is either 1 or 0:
* * wasNotVoter: 1 if key was not voter, 0 otherwise
* * wasVoter: 1 if key was voter, 0 otherwise
* __Sum of those 2 integers MUST always be 1.__
* @parameter p Array of 4 Integers: [hasNotVoted, hasVoted, hasNewVoting, isLeavingMember].
* Each integer is either 1 or 0:
* * hasNotVoted: 1 if voter has voted current amendment, 0 otherwise
* * hasVoted: 1 if voter has voted current amendment, 0 otherwise
* * hasNewVoting: 1 if member submitted new voting key, 0 otherwise
* * isLeavingMember: 1 if member is leaving, 0 otherwise
*/
Computing.Voting = function (vt, p, done) {
done(null, vt[0]*IsNotVoter(p) + vt[1]*IsVoter(p) + vt[2]*IsVoterTooOld(p));
}
function IsNotVoter (p) {
return p[1] - 2*p[2];
}
function IsVoter (p) {
return - 2*p[2];
}
function IsVoterTooOld (p) {
return - p[0] + p[1] - 2*p[2];
}
module.exports = Computing;
module.exports = function hexstrdump(str) {
if (str == null)
return "";
var r=[];
var e=str.length;
var c=0;
var h;
while(c<e){
h=str[c++].charCodeAt().toString(16);
while(h.length<2) h="0"+h;
r.push(""+h);
}
return r.join('');
}
......@@ -8,6 +8,8 @@ var jpgp = require('../../../jpgp');
module.exports = PubkeyParser;
var UDID2_FORMAT = /\(udid2;c;([A-Z-]*);([A-Z-]*);(\d{4}-\d{2}-\d{2});(e\+\d{2}\.\d{2}-\d{3}\.\d{2});(\d+)(;?)\)/;
function PubkeyParser (onError) {
var captures = [];
......@@ -17,12 +19,17 @@ function PubkeyParser (onError) {
this._parse = function (str, obj) {
obj.raw = str;
obj.hash = sha1(str).toUpperCase();
// Extracting email, name and comment
var k = jpgp().certificate(obj.raw);
// Extract udid2
obj.udid2s = getSignedUdid2s(k.key);
if (obj.udid2s.length) {
obj.udid2 = obj.udid2s[0];
}
// Extracting email, name and comment
if (k.fingerprint) {
obj.fingerprint = k.fingerprint;
obj.hash = sha1(obj.raw).toUpperCase();
var uid = k.uids[0];
var uid = (obj.udid2 && obj.udid2.uid) || k.uids[0];
var extract = uid.match(/([\s\S]*) \(([\s\S]*)\) <([\s\S]*)>/);
if(extract && extract.length === 4){
obj.name = extract[1];
......@@ -49,6 +56,7 @@ function PubkeyParser (onError) {
}
}
}
obj.subkeys = k.subkeys;
};
this._verify = function (obj) {
......@@ -58,4 +66,25 @@ function PubkeyParser (onError) {
};
}
function getSignedUdid2s (key) {
var validsUdid2 = [];
for (var i = 0; i < key.users.length; i++) {
if (!key.users[i].userId || !key.users[i].userId.userid.match(UDID2_FORMAT) || !key.users[i].getValidSelfCertificate(key.primaryKey)) {
continue;
}
validsUdid2.push({ uid: key.users[i].userId.userid, user: key.users[i], nbSigs: (key.users[i].otherCertifications || []).length , signatures: key.users[i].otherCertifications});
}
// sort by number of signatures
validsUdid2 = validsUdid2.sort(function(a, b) {
if (a.nbSigs > b.nbSigs) {
return -1;
} else if (a.nbSigs < b.nbSigs) {
return 1;
} else {
return 0;
}
});
return validsUdid2;
}
util.inherits(PubkeyParser, GenericParser);
......@@ -66,6 +66,24 @@ KeySchema.statics.wasMember = function(fingerprint, amNumber, done){
});
}
KeySchema.statics.getMembersOn = function(amNumber, done){
var Key = this.model('Key');
Key.find({ "asMember.joins": { $lte: amNumber }, $where: memberHasNotLeftSince }, done);
}
KeySchema.statics.getVotersOn = function(amNumber, done){
var Key = this.model('Key');
Key.find({ "asVoter.joins": { $lte: amNumber }, $where: voterHasNotLeftSince }, done);
}
function memberHasNotLeftSince() {
return this.asMember.leaves.length == 0 || _(this.asMember.leaves).max() < _(this.asMember.joins).max();
}
function voterHasNotLeftSince() {
return this.asVoter.leaves.length == 0 || _(this.asVoter.leaves).max() < _(this.asVoter.joins).max();
}
KeySchema.statics.voterJoin = function(amNumber, fingerprint, done){
var Key = this.model('Key');
Key.update({ fingerprint: fingerprint }, { $push: { "asVoter.joins": amNumber }}, done);
......
......@@ -29,6 +29,38 @@ PublicKeySchema.pre('save', function (next) {
next();
});
PublicKeySchema.virtual('nbVerifiedSigs').get(function () {
return this._nbVerifiedSigs;
});
PublicKeySchema.virtual('nbVerifiedSigs').set(function (nbVerifiedSigs) {
this._nbVerifiedSigs = nbVerifiedSigs;
});
PublicKeySchema.virtual('udid2').get(function () {
return this._udid2;
});
PublicKeySchema.virtual('udid2').set(function (udid2) {
this._udid2 = udid2;
});
PublicKeySchema.virtual('udid2s').get(function () {
return this._udid2s;
});
PublicKeySchema.virtual('udid2s').set(function (udid2s) {
this._udid2s = udid2s;
});
PublicKeySchema.virtual('udid2sigs').get(function () {
return this._udid2sigs || [];
});
PublicKeySchema.virtual('udid2sigs').set(function (sigs) {
this._udid2sigs = (sigs && sigs.length) || [sigs];
});
PublicKeySchema.methods = {
json: function () {
......
......@@ -3,11 +3,11 @@ var async = require('async');
var _ = require('underscore');
var logger = require('../lib/logger')('amendment');
module.exports.get = function (conn, conf, ContractService, SyncService) {
return new StrategyService(conn, conf, ContractService, SyncService);
module.exports.get = function (conn, conf, ContractService, SyncService, alertDeamon) {
return new StrategyService(conn, conf, ContractService, SyncService, alertDeamon);
};
function StrategyService (conn, conf, ContractService, SyncService) {
function StrategyService (conn, conf, ContractService, SyncService, alertDeamon) {
var Amendment = conn.model('Amendment');
var Membership = conn.model('Membership');
......@@ -184,11 +184,11 @@ function StrategyService (conn, conf, ContractService, SyncService) {
}, next);
},
function (next){
if (SyncService && conf.createNext) {
SyncService.createNext(am, next);
} else {
next();
if (alertDeamon) {
var now = new Date().timestamp();
alertDeamon((am.generated + conf.sync.AMFreq - now)*1000);
}
next();
},
], cb);
}, done);
......
This diff is collapsed.
......@@ -100,7 +100,7 @@ program
program
.command('sync [host] [port]')
.description('Tries to synchronise data with remote uCoin node')
.action(connect(function (host, port, server, conf) {
.action(connect(DO_NOT_LISTEN_HTTP, ucoin.createRegistryServer, function (host, port, server, conf) {
// Disable daemon
conf.sync.AMDaemon = "OFF";
......
......@@ -46,12 +46,11 @@
* [community/voters (POST)](#communityvoters-post)
* [community/voters/[PGP_FINGERPRINT]/current](#communityvoterspgp_fingerprintcurrent)
* [community/voters/[PGP_FINGERPRINT]/history](#communityvoterspgp_fingerprinthistory)
* [amendment](#amendment)
* [amendment/[AM_NUMBER]](#amendmentam_number)
* [amendment/[AM_NUMBER]/[ALGO]/members/in](#amendmentam_numberalgomembersin)
* [amendment/[AM_NUMBER]/[ALGO]/members/out](#amendmentam_numberalgomembersout)
* [amendment/[AM_NUMBER]/[ALGO]/voters/in](#amendmentam_numberalgovotersin)
* [amendment/[AM_NUMBER]/[ALGO]/voters/out](#amendmentam_numberalgovotersout)
* [amendment/[AM_NUMBER]/[ALGO]/self](#amendmentam_numberalgoself)
* [amendment/[AM_NUMBER]/[ALGO]/flow](#amendmentam_numberalgoflow)
* [amendment/[AM_NUMBER]/[ALGO]/vote](#amendmentam_numberalgovote)
......@@ -116,7 +115,7 @@ Data is made accessible through an HTTP API mainly inspired from [OpenUDC_exchan
| | `-- [PGP_FINGERPRINT]/
| | |-- history
| | `-- current
| |-- voters/
| `-- voters/
| `-- [PGP_FINGERPRINT]/
| |-- history
| `-- current
......@@ -129,6 +128,7 @@ Data is made accessible through an HTTP API mainly inspired from [OpenUDC_exchan
|-- voters
| |-- in
| `-- out
|-- self
|-- flow
`-- vote
......@@ -137,7 +137,7 @@ Data is made accessible through an HTTP API mainly inspired from [OpenUDC_exchan
Merkle URL is a special kind of URL applicable for resources:
* `pks/all`
* `network/peers (GET)`
* `network/peering/peers (GET)`
* `network/wallet (GET)`
* `hdc/amendments/view/[AMENDMENT_ID]/signatures`
* `hdc/transactions/sender/[PGP_FINGERPRINT]`
......@@ -1658,78 +1658,6 @@ A list of posted voting requests + posted signatures.
}
```
#### `amendment`
**Goal**
GET the next amendment *this node* would vote for when time has come.
**Parameters**
*None*.
**Returns**
Amendment to be voted by this node if voting happened.
```json
{
"version": "1",
"currency": "beta_brousouf",
"number": "2",
"generated": 1400588975,
"previousHash": "0F45DFDA214005250D4D2CBE4C7B91E60227B0E5",
"dividend": "100",
"votersRoot": "DC7A9229DFDABFB9769789B7BFAE08048BCB856F",
"votersCount": "2",
"votersChanges": [
"-C73882B64B7E72237A2F460CE9CAB76D19A8651E"
],
"membersRoot": "F92B6F81C85200250EE51783F5F9F6ACA57A9AFF",
"membersCount": "4",
"membersChanges": [
"+31A6302161AC8F5938969E85399EB3415C237F93"
],
"raw": "Version: 1\r\n...+31A6302161AC8F5938969E85399EB3415C237F93\r\n"
}
```
#### `amendment/[AM_NUMBER]`
**Goal**
GET the next amendment *this node* would vote for when time has come with given AM number.
**Parameters**
Name | Value | Method
----------------- | ------------------------------------------------------------- | ------
`AM_NUMBER` | The amendment number to be promoted. | URL
**Returns**
Amendment to be voted by this node if voting happened.
```json
{
"version": "1",
"currency": "beta_brousouf",
"number": "2",
"generated": 1400588975,
"previousHash": "0F45DFDA214005250D4D2CBE4C7B91E60227B0E5",
"dividend": "100",
"votersRoot": "DC7A9229DFDABFB9769789B7BFAE08048BCB856F",
"votersCount": "2",
"votersChanges": [
"-C73882B64B7E72237A2F460CE9CAB76D19A8651E"
],
"membersRoot": "F92B6F81C85200250EE51783F5F9F6ACA57A9AFF",
"membersCount": "4",
"membersChanges": [