diff --git a/demos/run-test.sh b/demos/run-test.sh index 79e9009ec..2bffdb6df 100755 --- a/demos/run-test.sh +++ b/demos/run-test.sh @@ -6,7 +6,7 @@ if [ "$1" == "*" ]; then EXIT_CODE=0 for i in demos/*; do if [ -f "$i/test.js" ]; then - if ! [[ "$i" =~ ^demos/(pca|mpc-web|template|mpc-as-a-service)$ ]]; then + if ! [[ "$i" =~ ^demos/(pca|restAPI|template|mpc-as-a-service)$ ]]; then sleep 2 npm run-script test-demo -- "$i" CODE=$? diff --git a/lib/jiff-client.js b/lib/jiff-client.js index fe9784371..16f5fd186 100644 --- a/lib/jiff-client.js +++ b/lib/jiff-client.js @@ -859,14 +859,19 @@ jiff.emit(share_id + 'length', receivers_list, array.length.toString(10)); } - jiff.listen(share_id + 'length', function (sender, message) { - lengths[sender] = parseInt(message, 10); - total++; - if (total === senders_list.length) { - jiff.remove_listener(share_id + 'length'); - share_array_deferred.resolve(lengths); - } - }); + if (receivers_list.indexOf(jiff.id) > -1) { + jiff.listen(share_id + 'length', function (sender, message) { + lengths[sender] = parseInt(message, 10); + total++; + if (total === senders_list.length) { + jiff.remove_listener(share_id + 'length'); + share_array_deferred.resolve(lengths); + } + }); + } else if (senders_list[0] === jiff.id && senders_list.length === 1) { + lengths[jiff.id] = array.length; + share_array_deferred.resolve(lengths); + } } else if (typeof(lengths) === 'number') { // All arrays are of the same length var l = lengths; diff --git a/lib/jiff-server.js b/lib/jiff-server.js index c302f0714..3efbdaaf7 100644 --- a/lib/jiff-server.js +++ b/lib/jiff-server.js @@ -358,7 +358,7 @@ exports.make_jiff = function (http, options) { }; jiff.custom = function (computation_id, from_id, msg) { - jiff.hooks.log(jiff, 'custom from ' + computation_id + '-' + from_id + ' : ' + msg); + jiff.hooks.log(jiff, 'custom from ' + computation_id + '-' + from_id + ' : ' + JSON.stringify(msg)); try { msg = jiff.execute_array_hooks('beforeOperation', [jiff, 'custom', computation_id, from_id, msg], 4); diff --git a/package.json b/package.json index 918fda7db..f8665d882 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "jsdoc": "^3.5.5", "marked": "^0.3.9", "minimist": "^1.2.0", - "mocha": "^4.0.1" + "mocha": "^4.0.1", + "nunjucks": "^3.1.7" } } diff --git a/presentation/least-squares/analyst/analyst.html b/presentation/least-squares/analyst/analyst.html new file mode 100644 index 000000000..d25e4becc --- /dev/null +++ b/presentation/least-squares/analyst/analyst.html @@ -0,0 +1,52 @@ + + + + + Linear Least Squares + + + + + + + + + + + + + + + + + + + + +

Analyst

+

Analysis:

+
+

Output:

+ + + + + diff --git a/presentation/least-squares/analyst/analyst.js b/presentation/least-squares/analyst/analyst.js new file mode 100644 index 000000000..a488d6d5d --- /dev/null +++ b/presentation/least-squares/analyst/analyst.js @@ -0,0 +1,103 @@ +/** + * Do not modify this file unless you have to. + * This file has UI handlers. + */ + +// eslint-disable-next-line no-unused-vars +var computes = []; +function connect(party_id, party_count, compute_count, computation_id) { + // Compute parties ids + for (var c = 1; c <= compute_count; c++) { + computes.push(c); + } + + // jiff options + var options = { party_count: party_count, party_id: party_id }; + options.onError = function (error) { + $('#output').append('

'+error+'

'); + }; + options.onConnect = function () { + $('#output').append('

Connected!

'); + }; + + // host name + var hostname = window.location.hostname.trim(); + var port = window.location.port; + if (port == null || port === '') { + port = '80'; + } + if (!(hostname.startsWith('http://') || hostname.startsWith('https://'))) { + hostname = 'http://' + hostname; + } + if (hostname.endsWith('/')) { + hostname = hostname.substring(0, hostname.length-1); + } + if (hostname.indexOf(':') > -1 && hostname.lastIndexOf(':') > hostname.indexOf(':')) { + hostname = hostname.substring(0, hostname.lastIndexOf(':')); + } + + hostname = hostname + ':' + port; + + // eslint-disable-next-line no-undef + mpc.connect(hostname, computation_id, options, computes, function (id, cols) { + var schema = 'Party ' + (id - computes.length - 1) + ': ' + cols.join(' | ') + '
'; + document.getElementById('schema').innerHTML += schema; + }); +} + +// eslint-disable-next-line no-unused-vars +function submit() { + $('#output').append('

Starting...

'); + $('#submit').attr('disabled', true); + + // eslint-disable-next-line no-undef + mpc.compute().then(plot); +} + +/** + * Helpers for drawing lines in a chart + */ +// The limits of the graph. The minimum and maximum X and Y values. +var minX = -60; +var maxX = 60; +var minY = -60; +var maxY = 60; + +// Chart for drawing +function initChart(line) { + var ctx = document.getElementById('myChart').getContext('2d'); + + var scales = { + yAxes: [{ ticks: { min: minY, max: maxY, maxTicksLimit: 5 } }], + xAxes: [{ type: 'linear', position: 'bottom', ticks: { min: minX, max: maxX, maxTicksLimit: 20 } }] + }; + + // eslint-disable-next-line no-undef + var myChart = new Chart(ctx, { + type: 'line', + data: { + datasets: [ + { id: 'least-line', label: '', data: line, fill: false, pointBackgroundColor: '#000099', borderColor: '#000099', pointRadius: 0 } + ] + }, + options: { + elements: { line: { tension: 1 } }, + scales: scales + } + }); + myChart.update(); +} + +function plot(data) { + console.log(data.slope.toString(), data.yIntercept.toString()); + var m = data.slope; + var p = data.yIntercept; + + var points = []; + for (var i = minX; i <= maxX; i+=1) { + points.push({x: i, y: m.times(i).plus(p).toNumber()}); + } + + $('#output').show(); + initChart(points); +} \ No newline at end of file diff --git a/presentation/least-squares/analyst/mpc.js b/presentation/least-squares/analyst/mpc.js new file mode 100644 index 000000000..8b8825d00 --- /dev/null +++ b/presentation/least-squares/analyst/mpc.js @@ -0,0 +1,36 @@ +(function (exports) { + var jiff_instance; + var computes; + + /** + * Connect to the server and initialize the jiff instance + */ + exports.connect = function (hostname, computation_id, options, _computes, schema_callback) { + computes = _computes; + var opt = Object.assign({}, options); + opt.Zp = '4503599627370449'; + opt.integer_digits = 9; + opt.decimal_digits = 3; + + // eslint-disable-next-line no-undef + jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_bignumber, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_fixedpoint, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_negativenumber, opt); + }; + + /** + * The MPC computation + */ + exports.compute = function () { + var promise1 = jiff_instance.receive_open(computes); + var promise2 = jiff_instance.receive_open(computes); + + return Promise.all([promise1, promise2]).then(function (res) { + return {slope: res[0], yIntercept: res[1]}; + }); + }; +}(this.mpc = {})); diff --git a/presentation/least-squares/compute/mpc.js b/presentation/least-squares/compute/mpc.js new file mode 100644 index 000000000..a918ef813 --- /dev/null +++ b/presentation/least-squares/compute/mpc.js @@ -0,0 +1,96 @@ +// Configurations +var decimal_digits = 3; + +var config = require('../config.json'); +var computes = []; +for (var c = 1; c <= config.compute; c++) { + computes.push(c); +} +var analyst = computes.length + 1; +var inputs = []; +for (var i = config.compute + 2; i <= config.total; i++) { + inputs.push(i); +} + +/** + * Connect to the server and initialize the jiff instance + */ +exports.connect = function (hostname, computation_id, options) { + var opt = Object.assign({}, options); + opt.Zp = '4503599627370449'; + opt.integer_digits = 9; + opt.decimal_digits = decimal_digits; + + var jiff = require('../../../lib/jiff-client'); + var jiff_bignumber = require('../../../lib/ext/jiff-client-bignumber'); + var jiff_fixedpoint = require('../../../lib/ext/jiff-client-fixedpoint'); + var jiff_negativenumber = require('../../../lib/ext/jiff-client-negativenumber'); + + var jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + jiff_instance.apply_extension(jiff_bignumber, opt); + jiff_instance.apply_extension(jiff_fixedpoint, opt); + jiff_instance.apply_extension(jiff_negativenumber, opt); + jiff_instance.wait_for(computes, function () { + compute(jiff_instance); + }); + jiff_instance.wait_for(['s1'], function () { + console.log('This is compute party ', jiff_instance.id); + }) +}; + +/** + * The MPC computation + */ +function compute(jiff_instance) { + var avgs = jiff_instance.share(null, computes.length, computes, inputs); + var xAvg = avgs[inputs[0]]; + var yAvg = avgs[inputs[1]]; + + var xBar = jiff_instance.share_array([], null, computes.length, computes, [inputs[0]]); + var yBar = jiff_instance.share_array([], null, computes.length, computes, [inputs[1]]); + xBar.then(function (arr) { + console.log('First input party secret shared their data!'); + var share = arr[inputs[0]][0]; + if (share.value != null) { + console.log('First secret share value ', share.value.toString()); + } else { + share.promise.then(function () { + console.log('First secret share value ', share.value.toString()); + }); + } + }); + yBar.then(function (arr) { + console.log('Second input party secret shared their data!'); + var share = arr[inputs[1]][0]; + if (share.value != null) { + console.log('First secret share value ', share.value.toString()); + } else { + share.promise.then(function () { + console.log('First secret share value ', share.value.toString()); + }); + } + }); + + var xBarSquare = jiff_instance.share(null, computes.length, computes, [inputs[0]])[inputs[0]]; + + Promise.all([xBar, yBar]).then(function (res) { + xBar = res[0][inputs[0]]; + yBar = res[1][inputs[1]]; + + var numerator = xBar[0].smult(yBar[0], null, false); + for (var i = 1; i < xBar.length; i++) { + numerator = numerator.sadd(xBar[i].smult(yBar[i], null, false)); + } + var denumerator = xBarSquare.cmult(jiff_instance.helpers.magnitude(decimal_digits)); + + // Compute slope and y Intercept of line + var slope = numerator.sdiv(denumerator); + var yIntercept = yAvg.ssub(slope.smult(xAvg, 'main-smult')); + + // Done + jiff_instance.wait_for([analyst], function () { + jiff_instance.open(slope, [analyst]); + jiff_instance.open(yIntercept, [analyst]); + }); + }); +} diff --git a/presentation/least-squares/compute/party.js b/presentation/least-squares/compute/party.js new file mode 100644 index 000000000..cf3466b4d --- /dev/null +++ b/presentation/least-squares/compute/party.js @@ -0,0 +1,16 @@ +/** + * Do not change this unless you have to. + * This code parses input command line arguments, + * and calls the appropriate initialization and MPC protocol from ./mpc.js + */ +var mpc = require('./mpc'); +var config = require('../config.json'); + +// JIFF options +var options = { + initialization: {role: 'compute'}, + party_count: config.total +}; + +// Connect +mpc.connect('http://localhost:8080', config.computation_id, options); diff --git a/presentation/least-squares/config.json b/presentation/least-squares/config.json new file mode 100644 index 000000000..cf6e14706 --- /dev/null +++ b/presentation/least-squares/config.json @@ -0,0 +1,5 @@ +{ + "computation_id": "test", + "compute": 2, + "total": 5 +} \ No newline at end of file diff --git a/presentation/least-squares/input/input.html b/presentation/least-squares/input/input.html new file mode 100644 index 000000000..5381de14a --- /dev/null +++ b/presentation/least-squares/input/input.html @@ -0,0 +1,78 @@ + + + + + Linear Least Squares + + + + + + + + + + + + + + + + + +

Input party {{ id - config.compute - 1}}

+

Linear Least Squares

+ +
+ +

+ +
+ +
+
+
+ + + + diff --git a/presentation/least-squares/input/input.js b/presentation/least-squares/input/input.js new file mode 100644 index 000000000..5a88e1a4d --- /dev/null +++ b/presentation/least-squares/input/input.js @@ -0,0 +1,103 @@ +/** + * Do not modify this file unless you have to. + * This file has UI handlers. + */ + +// eslint-disable-next-line no-unused-vars +var computes = []; +function connect(party_id, party_count, compute_count, computation_id) { + def = defaults[party_id]; + + // Compute parties ids + for (var c = 1; c <= compute_count; c++) { + computes.push(c); + } + + // jiff options + var options = { party_count: party_count, party_id: party_id }; + options.onError = function (error) { + $('#output').append('

'+error+'

'); + }; + + // host name + var hostname = window.location.hostname.trim(); + var port = window.location.port; + if (port == null || port === '') { + port = '80'; + } + if (!(hostname.startsWith('http://') || hostname.startsWith('https://'))) { + hostname = 'http://' + hostname; + } + if (hostname.endsWith('/')) { + hostname = hostname.substring(0, hostname.length-1); + } + if (hostname.indexOf(':') > -1 && hostname.lastIndexOf(':') > hostname.indexOf(':')) { + hostname = hostname.substring(0, hostname.lastIndexOf(':')); + } + + hostname = hostname + ':' + port; + + // eslint-disable-next-line no-undef + var jiff_instance = mpc.connect(hostname, computation_id, options, computes); + + // Display connection + jiff_instance.wait_for(computes, function () { + $('#output').append('

Connected!

'); + }); +} + +// eslint-disable-next-line no-unused-vars +function submit() { + $('#output').append('

Starting...

'); + $('#submit').attr('disabled', true); + + var parsed = parseInput(); + // eslint-disable-next-line no-undef + mpc.compute(parsed); + + $('#output').append('

Shared data successfully!

'); +} + +/** + * Helpers for HTML data generation and parsing + */ +function generateTable(points, id) { + $('#generate').attr('disabled', true); + $('#submit').attr('disabled', false); + + var table = '
'; + // Header + table += ''; + table += ''; + table += ''; + + // Generate Body + for (var j = 0; j < points; j++) { + table += ''; + var input = ''; + table += ''; + table += ''; + } + + table += '
' + input + '


'; + $('#input_area').html(table); +} +function parseInput() { + var data = []; + + var table = document.getElementById('input_table'); + + // body + for (var i = 2; i < table.rows.length; i++) { + var row = table.rows[i]; + data.push(Number(row.getElementsByTagName('input')[0].value)); + } + + return data; +} + +var defaults = { + 4: [ 1, 10, -5.3, -30.3 ], + 5: [ 2, 22.1, -9.813, -45.2 ] +}; +var def; \ No newline at end of file diff --git a/presentation/least-squares/input/mpc.js b/presentation/least-squares/input/mpc.js new file mode 100644 index 000000000..f5e0f4847 --- /dev/null +++ b/presentation/least-squares/input/mpc.js @@ -0,0 +1,53 @@ +/* global BigNumber */ +(function (exports) { + var jiff_instance; + var computes; + + /** + * Connect to the server and initialize the jiff instance + */ + exports.connect = function (hostname, computation_id, options, _computes) { + computes = _computes; + var opt = Object.assign({}, options); + opt.Zp = '4503599627370449'; + opt.integer_digits = 9; + opt.decimal_digits = 3; + + // eslint-disable-next-line no-undef + jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_bignumber, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_fixedpoint, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_negativenumber, opt); + return jiff_instance; + }; + + /** + * The MPC computation + */ + exports.compute = function (data) { + var avg = 0; + for (var i = 0; i < data.length; i++) { + avg += data[i]; + } + avg = new BigNumber(avg.toString()).div(data.length); + + // Secret share average + jiff_instance.share(avg, computes.length, computes, [computes.length + 2, computes.length + 3]); + + var bar = []; + var squared = new BigNumber(0); + for (var j = 0; j < data.length; j++) { + bar[j] = new BigNumber(data[j].toString()).minus(avg); + squared = squared.plus(bar[j].times(bar[j])); + } + + // Secret share array of (avg - diff) + jiff_instance.share_array(bar, null, computes.length, computes, [ jiff_instance.id ]); + + // Secret share sum of difference square (denumerator), only used for x. + jiff_instance.share(squared, computes.length, computes, [ jiff_instance.id ]); + }; +}(this.mpc = {})); diff --git a/presentation/least-squares/server.js b/presentation/least-squares/server.js new file mode 100644 index 000000000..78360f2b6 --- /dev/null +++ b/presentation/least-squares/server.js @@ -0,0 +1,66 @@ +var express = require('express'); +var app = express(); + +var http = require('http').Server(app); +http.listen(8080, function () { + console.log('listening on *:8080'); +}); + +// nunjucks for rendering html template +var nunjucks = require('nunjucks'); +nunjucks.configure(__dirname, { + autoescape: true, + express: app +}); + +// read configurations +var config = require('./config.json'); + +//Serve static files +app.use('/bignumber.js', express.static(__dirname + '/../../node_modules/bignumber.js')); +app.use('/lib', express.static(__dirname + '/../../lib')); + +// Server input and analyst files +app.use('/static/input', express.static(__dirname + '/input')); +app.use('/static/analyst', express.static(__dirname + '/analyst')); +app.get('/input/:id', function (req, res) { + var party_id = parseInt(req.params.id, 10); + res.render('input/input.html', {id: party_id + config.compute + 1, config: config}); +}); +app.get('/analyst', function (req, res) { + res.render('analyst/analyst.html', {id: config.compute + 1, config: config}); +}); + +// Manage roles / IDS +// Keep track of assigned ids +var assignedCompute = 1; +var initialize = function (jiff, computation_id, msg, params) { + if (params.party_id != null) { + return params; + } + + if (msg.role === 'input') { + return params; + } + if (msg.role === 'analyst') { + params['party_id'] = config.compute + 1; + return params; + } + if (msg.role === 'compute' && assignedCompute <= config.compute) { + params['party_id'] = assignedCompute; + assignedCompute++; + return params; + } + + throw new Error('Unrecognized role and party id options'); +}; + +var jiff_instance = require('../../lib/jiff-server.js').make_jiff(http, {logs: true, hooks: { beforeInitialization: [initialize] }}); +jiff_instance.apply_extension(require('../../lib/ext/jiff-server-bignumber.js')); + + +// Instructions +console.log('Direct your browser to http://localhost/input/ for inputing data'); +console.log('Direct your browser to http://localhost/analyst for analyst UI'); +console.log('Run compute parties with node compute/party.js'); +console.log(); \ No newline at end of file diff --git a/presentation/relational/analyst/analyst.html b/presentation/relational/analyst/analyst.html new file mode 100644 index 000000000..0c29568c9 --- /dev/null +++ b/presentation/relational/analyst/analyst.html @@ -0,0 +1,57 @@ + + + + + Relational MPC Workflow + + + + + + + + + + + + + + + + +

Analyst

+

Input Parties Schemas:

+
+ +

Analysis:

+ +
+ +
+

Output:

+
+ + + + diff --git a/presentation/relational/analyst/analyst.js b/presentation/relational/analyst/analyst.js new file mode 100644 index 000000000..f82d80c45 --- /dev/null +++ b/presentation/relational/analyst/analyst.js @@ -0,0 +1,86 @@ +/** + * Do not modify this file unless you have to. + * This file has UI handlers. + */ + +// eslint-disable-next-line no-unused-vars +var computes = []; +function connect(party_id, party_count, compute_count, computation_id) { + // Compute parties ids + for (var c = 1; c <= compute_count; c++) { + computes.push(c); + } + + // jiff options + var options = { party_count: party_count, party_id: party_id }; + options.onError = function (error) { + $('#output').append('

'+error+'

'); + }; + options.onConnect = function () { + $('#output').append('

Connected!

'); + }; + + // host name + var hostname = window.location.hostname.trim(); + var port = window.location.port; + if (port == null || port === '') { + port = '80'; + } + if (!(hostname.startsWith('http://') || hostname.startsWith('https://'))) { + hostname = 'http://' + hostname; + } + if (hostname.endsWith('/')) { + hostname = hostname.substring(0, hostname.length-1); + } + if (hostname.indexOf(':') > -1 && hostname.lastIndexOf(':') > hostname.indexOf(':')) { + hostname = hostname.substring(0, hostname.lastIndexOf(':')); + } + + hostname = hostname + ':' + port; + + // eslint-disable-next-line no-undef + mpc.connect(hostname, computation_id, options, computes, function (id, cols) { + var schema = 'Party ' + (id - computes.length - 1) + ': ' + cols.join(' | ') + '
'; + document.getElementById('schema').innerHTML += schema; + }); +} + +// eslint-disable-next-line no-unused-vars +function submit() { + $('#output').append('

Starting...

'); + $('#submit').attr('disabled', true); + + // eslint-disable-next-line no-undef + generateTable(mpc.compute()); +} + +/** + * Helpers for HTML data generation and parsing + */ +function generateTable(result) { + result.promise.then(function (data) { + var cols = result.cols; + + var table = '
'; + // Header + table += ''; + for (var i = 0; i < cols.length; i++) { + table += ''; + } + table += ''; + + // Generate Body + for (var j = 0; j < data.length; j++) { + table += ''; + for (var k = 0; k < cols.length; k++) { + var val = data[j][cols[k]]; + var input = ''; + table += ''; + } + table += ''; + } + + table += '
' + input + '
'; + $('#output').html(table); + }); +} \ No newline at end of file diff --git a/presentation/relational/analyst/mpc.js b/presentation/relational/analyst/mpc.js new file mode 100644 index 000000000..71c24e2c4 --- /dev/null +++ b/presentation/relational/analyst/mpc.js @@ -0,0 +1,66 @@ +var GROUP_BY_DOMAIN = ['Group A', 'Group B', 'Group C']; + +(function (exports) { + var jiff_instance; + var computes; + var schemas = {}; + + /** + * Connect to the server and initialize the jiff instance + */ + exports.connect = function (hostname, computation_id, options, _computes, schema_callback) { + computes = _computes; + var opt = Object.assign({}, options); + + opt.Zp = '16381'; + opt.integer_digits = 2; + opt.decimal_digits = 2; + + // eslint-disable-next-line no-undef + jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_bignumber, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_fixedpoint, opt); + + jiff_instance.listen('headers', function (id, cols) { + cols = JSON.parse(cols); + schemas[id] = cols; + + if (id > computes.length) { + schema_callback(id, cols); + } + }); + }; + + /** + * The MPC computation + */ + exports.compute = function () { + var inputs = []; + for (var i = computes.length + 2; i <= jiff_instance.party_count; i++) { + inputs.push(i); + } + + // Schema + var output_schema = ['GROUP BY', 'AVG']; + + // Receive result + var promises = []; + for (var g = 0; g < GROUP_BY_DOMAIN.length; g++) { + var promise1 = jiff_instance.receive_open(computes); + var promise2 = jiff_instance.receive_open(computes); + + (function scope(g) { + promises.push(Promise.all([promise1, promise2]).then(function (res) { + var obj = {}; + obj[output_schema[0]] = GROUP_BY_DOMAIN[g]; + obj[output_schema[1]] = res[1] > 0 ? res[0] / res[1] : '-'; + return obj; + })); + }(g)); + } + + return { promise: Promise.all(promises), cols: output_schema }; + }; +}(this.mpc = {})); diff --git a/presentation/relational/compute/join.js b/presentation/relational/compute/join.js new file mode 100644 index 000000000..e4bd25c4e --- /dev/null +++ b/presentation/relational/compute/join.js @@ -0,0 +1,51 @@ +var binary_search = function (jiff_instance, id, table, schema) { + var idCol = schema[0]; + var valCol = schema[1]; + + while (table.length > 1) { + // comparison + var mid = Math.floor(table.length / 2); + var cmp = id.slt(table[mid][idCol]); + + // Slice array in half, choose slice depending on cmp + var nTable = []; + for (var i = 0; i < mid; i++) { + var obj = {}; + obj[idCol] = cmp.if_else(table[i][idCol], table[mid + i][idCol]); + obj[valCol] = cmp.if_else(table[i][valCol], table[mid + i][valCol]); + nTable.push(obj); + } + + // watch out for off by 1 errors if length is odd. + if (2 * mid < table.length) { + var obj2 = {}; + obj2[idCol] = table[2 * mid][idCol]; + obj2[valCol] = table[2 * mid][valCol]; + nTable.push(obj2); + } + + table = nTable; + } + + var row = table[0]; + var found = row[idCol].seq(id); + var val = found.if_else(row[valCol], 0); + return { found: found, val: val }; +}; + +module.exports = function (jiff_instance, table1, table2, schema1, schema2) { + for (var d = 0; d < table1.length; d++) { + var row = table1[d]; + var id = row[schema1[0]]; + var res = binary_search(jiff_instance, id, table2, schema2); + + var found = res.found; + for (var i = 2; i < schema1.length; i++) { + var col = schema1[i]; + row[col] = found.if_else(row[col], 0); + } + row[schema2[1]] = res.val; + } + + return table1; +}; \ No newline at end of file diff --git a/presentation/relational/compute/mpc.js b/presentation/relational/compute/mpc.js new file mode 100644 index 000000000..dae47ad6d --- /dev/null +++ b/presentation/relational/compute/mpc.js @@ -0,0 +1,204 @@ +var FILTER_VALUE = 1; +var GROUP_BY_DOMAIN = [1, 2, 3]; + +// Configurations +var config = require('../config.json'); +var computes = []; +for (var c = 1; c <= config.compute; c++) { + computes.push(c); +} +var analyst = computes.length + 1; +var inputs = []; +for (var i = config.compute + 2; i <= config.total; i++) { + inputs.push(i); +} + +// Schemas +var schemas = {}; +var total = 0; + +/** + * Connect to the server and initialize the jiff instance + */ +exports.connect = function (hostname, computation_id, options) { + var opt = Object.assign({}, options); + + // eslint-disable-next-line no-undef + opt.Zp = '16381'; + opt.integer_digits = 2; + opt.decimal_digits = 2; + + var jiff = require('../../../lib/jiff-client'); + var jiff_bignumber = require('../../../lib/ext/jiff-client-bignumber'); + var jiff_fixedpoint = require('../../../lib/ext/jiff-client-fixedpoint'); + + // eslint-disable-next-line no-undef + var jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_bignumber, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_fixedpoint, opt); + + jiff_instance.listen('headers', function (id, cols) { + cols = JSON.parse(cols); + schemas[id] = cols; + + total++; + if (total === inputs.length) { + compute(jiff_instance); + } + }); + + jiff_instance.wait_for(['s1'], function () { + console.log('This is compute party ', jiff_instance.id); + }); +}; + +function transform(table) { + var nTable = []; + for (var key in table) { + if (!table.hasOwnProperty(key)) { + continue; + } + + for (var i = 0; i < table[key].length; i++) { + if (nTable[i] == null) { + nTable[i] = {}; + } + + nTable[i][key] = table[key][i]; + } + } + return nTable; +} + +/** + * The MPC computation + */ +function compute(jiff_instance) { + var join = require('./join.js'); + + // ids + var in0 = inputs[0]; + var in1 = inputs[1]; + var in2 = inputs[2]; + var in3 = inputs[3]; + + // Receive shares + var promises = []; + for (var i = 0; i < inputs.length; i++) { + var input_id = inputs[i]; + var party_promises = []; + for (var c = 0; c < schemas[input_id].length; c++) { + party_promises.push(jiff_instance.share_array([], null, computes.length, computes, [ input_id ])); + } + + (function scope(input_id) { + var promise = Promise.all(party_promises).then(function (results) { + + var matrix = {}; + for (var k = 0; k < schemas[input_id].length; k++) { + var col = schemas[input_id][k]; + matrix[col] = results[k][input_id]; + } + return matrix; + }); + promises.push(promise); + }(input_id)); + } + + // Begin Work + Promise.all(promises).then(function (results) { + // Formatting + var matrices = {}; + for (var i = 0; i < inputs.length; i++) { + matrices[inputs[i]] = results[i]; + } + + // Concatenate first and second parties input + for (var c = 0; c < schemas[in0].length; c++) { + var col0 = schemas[in0][c]; + var col1 = schemas[in1][c]; + matrices[in1][col1] = matrices[in0][col0].concat(matrices[in1][col1]); + } + delete matrices[in0]; + return matrices; + + }).then(function (matrices) { + + // projections + for (var p = 1; p < inputs.length; p++) { + var input_id = inputs[p]; + for (var c = 2; c < schemas[input_id].length; c++) { + delete matrices[input_id][schemas[input_id][c]]; + } + + matrices[input_id] = transform(matrices[input_id]); + } + + return matrices; + + }).then(function (matrices) { + // have 3 tables, each containing [id, ] + var table = join(jiff_instance, matrices[in1], matrices[in2], schemas[in1], schemas[in2]); + table = join(jiff_instance, table, matrices[in3], schemas[in1].concat([schemas[in2][1]]), schemas[in3]); + return table; + }).then(function (table) { + // Output is an average and a group. + var output_schema = [schemas[in1][1], schemas[in3][1]]; + + // Filter + var filter_key = schemas[in2][1]; + var avg_key = output_schema[0]; + var group_key = output_schema[1]; + for (var i = 0; i < table.length; i++) { + var row = table[i]; + var outrow = {}; + + var condition = row[filter_key].ceq(FILTER_VALUE); + outrow[avg_key] = row[avg_key]; + outrow[group_key] = condition.if_else(row[group_key], 0); + + table[i] = outrow; + } + return table; + + }).then(function (table) { + // Output is an average and a group. + var output_schema = [schemas[in1][1], schemas[in3][1]]; + + // Group by and average + var avg_col = []; + var count_col = []; + + var avg_key = output_schema[0]; + var group_key = output_schema[1]; + for (var z = 0; z < GROUP_BY_DOMAIN.length; z++) { + avg_col.push(0); + count_col.push(0); + } + + for (var d = 0; d < table.length; d++) { + var row = table[d]; + for (var g = 0; g < GROUP_BY_DOMAIN.length; g++) { + var condition = row[group_key].ceq(GROUP_BY_DOMAIN[g]); + var if_ = condition.if_else(row[avg_key], 0); + + avg_col[g] = if_.add(avg_col[g]); + count_col[g] = condition.add(count_col[g]); + } + } + + return { avg: avg_col, count: count_col }; + }).then(function (results) { + jiff_instance.wait_for([analyst], function () { + var output_schema = ['avg', 'count']; + // All is done, open the result. + for (var gi = 0; gi < GROUP_BY_DOMAIN.length; gi++) { + for (var si = 0; si < output_schema.length; si++) { + jiff_instance.open(results[output_schema[si]][gi], [analyst]); + } + } + }); + }); +} diff --git a/presentation/relational/compute/party.js b/presentation/relational/compute/party.js new file mode 100644 index 000000000..37930f969 --- /dev/null +++ b/presentation/relational/compute/party.js @@ -0,0 +1,17 @@ +/** + * Do not change this unless you have to. + * This code parses input command line arguments, + * and calls the appropriate initialization and MPC protocol from ./mpc.js + */ +var mpc = require('./mpc'); +var config = require('../config.json'); + +// JIFF options +var options = { + initialization: {role: 'compute'}, + party_count: config.total, + warn: false +}; + +// Connect +mpc.connect('http://localhost:8080', config.computation_id, options); diff --git a/presentation/relational/config.json b/presentation/relational/config.json new file mode 100644 index 000000000..150f583f1 --- /dev/null +++ b/presentation/relational/config.json @@ -0,0 +1,5 @@ +{ + "computation_id": "test", + "compute": 3, + "total": 8 +} \ No newline at end of file diff --git a/presentation/relational/input/input.html b/presentation/relational/input/input.html new file mode 100644 index 000000000..3159e3f70 --- /dev/null +++ b/presentation/relational/input/input.html @@ -0,0 +1,128 @@ + + + + + Relational MPC Workflow + + + + + + + + + + + + + + + + +

Input party {{ id - config.compute - 1}}

+

Join / Filter / Group / Average under MPC

+ +
+
+ +

+ + +
+ +
+
+
+ + + + diff --git a/presentation/relational/input/input.js b/presentation/relational/input/input.js new file mode 100644 index 000000000..3beb4d97e --- /dev/null +++ b/presentation/relational/input/input.js @@ -0,0 +1,139 @@ +/** + * Do not modify this file unless you have to. + * This file has UI handlers. + */ + +// eslint-disable-next-line no-unused-vars +var computes = []; +function connect(party_id, party_count, compute_count, computation_id) { + def = defaults[party_id]; + hed = headers[party_id]; + document.getElementById('rows').value = def.length / 2; + + // Compute parties ids + for (var c = 1; c <= compute_count; c++) { + computes.push(c); + } + + // jiff options + var options = { party_count: party_count, party_id: party_id }; + options.onError = function (error) { + $('#output').append('

'+error+'

'); + }; + + // host name + var hostname = window.location.hostname.trim(); + var port = window.location.port; + if (port == null || port === '') { + port = '80'; + } + if (!(hostname.startsWith('http://') || hostname.startsWith('https://'))) { + hostname = 'http://' + hostname; + } + if (hostname.endsWith('/')) { + hostname = hostname.substring(0, hostname.length-1); + } + if (hostname.indexOf(':') > -1 && hostname.lastIndexOf(':') > hostname.indexOf(':')) { + hostname = hostname.substring(0, hostname.lastIndexOf(':')); + } + + hostname = hostname + ':' + port; + + // eslint-disable-next-line no-undef + var jiff_instance = mpc.connect(hostname, computation_id, options, computes); + + // Display connection + jiff_instance.wait_for(computes, function () { + $('#output').append('

Connected!

'); + }); +} + +// eslint-disable-next-line no-unused-vars +function submit() { + $('#output').append('

Starting...

'); + $('#submit').attr('disabled', true); + + var parsed = parseInput(); + // eslint-disable-next-line no-undef + mpc.compute(parsed['cols'], parsed['data']); + + $('#output').append('

Shared data successfully!

'); +} + +/** + * Helpers for HTML data generation and parsing + */ +function generateTable(cols, rows) { + $('#generate').attr('disabled', true); + $('#submit').attr('disabled', false); + + var table = '
'; + // Header + table += ''; + for (var i = 0; i < cols; i++) { + table += ''; + } + table += ''; + + // Generate Body + for (var j = 0; j < rows; j++) { + table += ''; + for (var k = 0; k < cols; k++) { + var input = ''; + table += ''; + } + table += ''; + } + + table += '
' + input + '


'; + $('#input_area').html(table); +} +function parseInput() { + var cols = []; + var data = []; + + var table = document.getElementById('input_table'); + + // headers + for (var c = 0; c < table.rows[0].cells.length; c++) { + var col = table.rows[0].cells[c].getElementsByTagName('input')[0].value; + cols.push(col); + } + + // body + for (var i = 2; i < table.rows.length; i++) { + var row = table.rows[i]; + var obj = {}; + for (var j = 0; j < cols.length; j++) { + var cell = row.cells[j]; + obj[cols[j]] = cell.getElementsByTagName('input')[0].value; + } + data.push(obj); + } + + return { cols: cols, data: data}; +} + +/* var defaults = { + 5: ['A', 10, 'B', 5, 'C', 2], + 6: ['Z', 5], + 7: ['A', 'True', 'B', 'True', 'Z', 'True'], + 8: ['A', 'Group A', 'B', 'Group B', 'Z', 'Group C'] +}; */ + +var defaults = { + 5: [ 'Alice', 10.23, 'Lora', 20.30, 'Bob', 0.12 ], + 6: [ 'Kinan', 30, 'Caroline', 5.99 ], + 7: [ 'Alice', 'False', 'Kinan', 'True', 'John', 'True', 'Lora', 'True', 'Caroline', 'True' ], + 8: [ 'Alice', 'Group A', 'John', 'Group A', 'Kinan', 'Group B', 'Caroline', 'Group B', 'Lora', 'Group C', 'Sam', 'Group B' ] +}; + +var headers = { + 5: ['ID', 'VALUE'], + 6: ['ID', 'VALUE'], + 7: ['ID', 'FILTER'], + 8: ['ID', 'GROUP'] +}; + +var def; +var hed; \ No newline at end of file diff --git a/presentation/relational/input/mpc.js b/presentation/relational/input/mpc.js new file mode 100644 index 000000000..d815aec59 --- /dev/null +++ b/presentation/relational/input/mpc.js @@ -0,0 +1,80 @@ +(function (exports) { + var jiff_instance; + var computes; + + /** + * Connect to the server and initialize the jiff instance + */ + exports.connect = function (hostname, computation_id, options, _computes) { + computes = _computes; + var opt = Object.assign({}, options); + + opt.Zp = '16381'; + opt.integer_digits = 2; + opt.decimal_digits = 2; + + // eslint-disable-next-line no-undef + jiff_instance = jiff.make_jiff(hostname, computation_id, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_bignumber, opt); + // eslint-disable-next-line no-undef + jiff_instance.apply_extension(jiff_fixedpoint, opt); + + return jiff_instance; + }; + + /** + * The MPC computation + */ + exports.compute = function (cols, data) { + var scoped_cols = []; + for (var i = 0; i < cols.length; i++) { + scoped_cols[i] = (jiff_instance.id - computes.length - 1) + '.' + cols[i]; + } + + // Parsing + var l; + for (l = 0; l < data.length; l++) { + data[l][cols[0]] = data[l][cols[0]].toUpperCase().trim(); + data[l][cols[0]] = data[l][cols[0]].charCodeAt(0) - 'A'.charCodeAt(0) + 1; + } + + if (jiff_instance.id === 5 || jiff_instance.id === 6) { + for (l = 0; l < data.length; l++) { + data[l][cols[1]] = Number(data[l][cols[1]]); + } + } else if (jiff_instance.id === 7) { + for (l = 0; l < data.length; l++) { + data[l][cols[1]] = data[l][cols[1]].toLowerCase() === 'true' ? 1 : 2; + } + } else if (jiff_instance.id === 8) { + for (l = 0; l < data.length; l++) { + data[l][cols[1]] = data[l][cols[1]].toUpperCase().trim(); + var ln = data[l][cols[1]].length; + data[l][cols[1]] = data[l][cols[1]].charCodeAt(ln-1) - 'A'.charCodeAt(0) + 1; + } + } + + // Compute party and analyst ids. + var copy = computes.slice(); + copy.push(computes.length + 1); + + // Send headers / schema + jiff_instance.emit('headers', copy, JSON.stringify(scoped_cols), false); + + // Sort by key + data.sort(function (x, y) { + return x[cols[0]] - y[cols[0]]; + }); + + // Split Array + for (var c = 0; c < cols.length; c++) { + var column = []; + for (var d = 0; d < data.length; d++) { + column.push(data[d][cols[c]]); + } + + jiff_instance.share_array(column, null, computes.length, computes, [ jiff_instance.id ]); + } + }; +}(this.mpc = {})); diff --git a/presentation/relational/server.js b/presentation/relational/server.js new file mode 100644 index 000000000..baaf6411f --- /dev/null +++ b/presentation/relational/server.js @@ -0,0 +1,66 @@ +var express = require('express'); +var app = express(); + +var http = require('http').Server(app); +http.listen(8080, function () { + console.log('listening on *:8080'); +}); + +// nunjucks for rendering html template +var nunjucks = require('nunjucks'); +nunjucks.configure(__dirname, { + autoescape: true, + express: app +}); + +// read configurations +var config = require('./config.json'); + +//Serve static files +app.use('/bignumber.js', express.static(__dirname + '/../../node_modules/bignumber.js')); +app.use('/lib', express.static(__dirname + '/../../lib')); + +// Server input and analyst files +app.use('/static/input', express.static(__dirname + '/input')); +app.use('/static/analyst', express.static(__dirname + '/analyst')); +app.get('/input/:id', function (req, res) { + var party_id = parseInt(req.params.id, 10); + res.render('input/input.html', {id: party_id + config.compute + 1, config: config}); +}); +app.get('/analyst', function (req, res) { + res.render('analyst/analyst.html', {id: config.compute + 1, config: config}); +}); + +// Manage roles / IDS +// Keep track of assigned ids +var assignedCompute = 1; +var initialize = function (jiff, computation_id, msg, params) { + if (params.party_id != null) { + return params; + } + + if (msg.role === 'input') { + return params; + } + if (msg.role === 'analyst') { + params['party_id'] = config.compute + 1; + return params; + } + if (msg.role === 'compute' && assignedCompute <= config.compute) { + params['party_id'] = assignedCompute; + assignedCompute++; + return params; + } + + throw new Error('Unrecognized role and party id options'); +}; + +var jiff_instance = require('../../lib/jiff-server').make_jiff(http, {logs: true, hooks: { beforeInitialization: [initialize] }}); +jiff_instance.apply_extension(require('../../lib/ext/jiff-server-bignumber.js')); + + +// Instructions +console.log('Direct your browser to http://localhost/input/ for inputing data'); +console.log('Direct your browser to http://localhost/analyst for analyst UI'); +console.log('Run compute parties with node compute/party.js'); +console.log(); \ No newline at end of file