Skip to content

Commit

Permalink
Initial XBR protocol implementation (#424)
Browse files Browse the repository at this point in the history
* dump current working tree

* improvements

* More seller code

* Do some stuff on the network

* More implementation...

* bring things closer to Python impl

* Call the market maker with right parameters

* add preliminary xbr buyer code

* Bring the XBR Seller near completion

* Make improvements to the Buyer code

* Bring buyer close to the finish line

* Minor improvements here and there

* Actually encrypt/decrypt secretbox

* Move deferred factory to utils and reuse in XBR

* Fix context

* Nothing to resolve

* dont export the private _onRotate

* assign 'self' at a universal place
  • Loading branch information
om26er committed May 21, 2019
1 parent 35bcc2c commit c3ca7c1
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 36 deletions.
2 changes: 2 additions & 0 deletions lib/autobahn.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var serializer = require('./serializer.js');
var persona = require('./auth/persona.js');
var cra = require('./auth/cra.js');
var cryptosign = require('./auth/cryptosign.js');
var xbr = require('./xbr/xbr.js');

exports.version = pjson.version;

Expand Down Expand Up @@ -71,3 +72,4 @@ exports.nacl = nacl;

exports.util = util;
exports.log = log;
exports.xbr = xbr;
37 changes: 1 addition & 36 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,7 @@ var Connection = function (options) {

// Deferred factory
//
if (options && options.use_es6_promises) {

if ('Promise' in global) {
// ES6-based deferred factory
//
self._defer = function () {
var deferred = {};

deferred.promise = new Promise(function (resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});

return deferred;
};
} else {

log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

// whenjs-based deferred factory
//
self._defer = when.defer;
}

} else if (options && options.use_deferred) {

// use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
//
self._defer = options.use_deferred;

} else {

// whenjs-based deferred factory
//
self._defer = when.defer;
}
self._defer = util.deferred_factory(options);


// WAMP transport
Expand Down
54 changes: 54 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,63 @@ var new_global_id = function() {
return Math.floor(Math.random() * 9007199254740992) + 1;
};

var deferred_factory = function(options) {
var defer = null;

if (options && options.use_es6_promises) {

if ('Promise' in global) {
// ES6-based deferred factory
//
defer = function () {
var deferred = {};

deferred.promise = new Promise(function (resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});

return deferred;
};
} else {

log.debug("Warning: ES6 promises requested, but not found! Falling back to whenjs.");

// whenjs-based deferred factory
//
defer = when.defer;
}

} else if (options && options.use_deferred) {

// use explicit deferred factory, e.g. jQuery.Deferred or Q.defer
//
defer = options.use_deferred;

} else {

// whenjs-based deferred factory
//
defer = when.defer;
}

return defer;
};

var promise = function(d) {
if (d.promise.then) {
// whenjs has the actual user promise in an attribute
return d.promise;
} else {
return d;
}
};

exports.handle_error = handle_error;
exports.rand_normal = rand_normal;
exports.assert = assert;
exports.http_post = http_post;
exports.defaults = defaults;
exports.new_global_id = new_global_id;
exports.deferred_factory = deferred_factory;
exports.promise = promise;
125 changes: 125 additions & 0 deletions lib/xbr/buyer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
var cbor = require('cbor');
var nacl = require('tweetnacl');
var eth_accounts = require("web3-eth-accounts");
var eth_util = require("ethereumjs-util");
var util = require('../util.js');


var SimpleBuyer = function (buyerKey, maxPrice) {
this._running = false;
this._session = null;
this._channel = null;
this._balance = null;
this._keys = {};
this._maxPrice = maxPrice;
this._deferred_factory = util.deferred_factory();

var account = new eth_accounts.Accounts().privateKeyToAccount(buyerKey);
this._addr = eth_util.toBuffer(account.address);

this._keyPair = nacl.box.keyPair();
};

SimpleBuyer.prototype.start = function(session, consumerID) {
self = this;
self._session = session;
self._running = true;

var d = this._deferred_factory();

session.call('xbr.marketmaker.get_payment_channel', [self._addr]).then(
function (paymentChannel) {
self._channel = paymentChannel;
self._balance = paymentChannel['remaining'];
d.resolve(self._balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);

return util.promise(d);
};

SimpleBuyer.prototype.stop = function () {
this._running = false;
};

SimpleBuyer.prototype.balance = function () {
var d = this._deferred_factory();
this._session.call('xbr.marketmaker.get_payment_channel', [self._addr]).then(
function (paymentChannel) {
var balance = {
amount: paymentChannel['amount'],
remaining: paymentChannel['remaining'],
inflight: paymentChannel['inflight']
};
d.resolve(balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);
return util.promise(d);
};

SimpleBuyer.prototype.openChannel = function (buyerAddr, amount) {
var signature = nacl.randomBytes(64);
var d = this._deferred_factory();
this._session.call(
'xbr.marketmaker.open_payment_channel',
[buyerAddr, this._addr, amount, signature]
).then(
function (paymentChannel) {
var balance = {
amount: paymentChannel['amount'],
remaining: paymentChannel['remaining'],
inflight: paymentChannel['inflight']
};
d.resolve(balance);
},
function (error) {
console.log("Call failed:", error);
d.reject(error['error']);
}
);
return util.promise(d);
};

SimpleBuyer.prototype.closeChannel = function () {
};

SimpleBuyer.prototype.unwrap = function (keyID, ciphertext) {
self = this;
var d = self._deferred_factory();
if (!self._keys.hasOwnProperty(keyID)) {
self._keys[keyID] = false;
self._session.call(
'xbr.marketmaker.buy_key',
[self._addr, self._keyPair.publicKey, keyID, self._maxPrice, nacl.randomBytes(64)]
).then(
function (receipt) {
var sealedKey = receipt['sealed_key'];
try {
self._keys[keyID] = nacl.sealedbox.open(sealedKey, self._keyPair.publicKey,
self._keyPair.secretKey);
var nonce = ciphertext.slice(0, nacl.secretbox.nonceLength);
var message = ciphertext.slice(nacl.secretbox.nonceLength, ciphertext.length);
var decrypted = nacl.secretbox.open(message, nonce, self._keys[keyID]);
var payload = cbor.decode(decrypted);
d.resolve(payload);
} catch (e) {
d.reject(e)
}
},
function (error) {
d.reject(error['error'])
}
);
}
return util.promise(d);
};

exports.SimpleBuyer = SimpleBuyer;
50 changes: 50 additions & 0 deletions lib/xbr/keyseries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
var cbor = require('cbor');
var nacl = require('tweetnacl');
var sealedbox = require('tweetnacl-sealedbox-js');

var KeySeries = function(apiID, prefix, price, interval, onRotate) {
this.apiID = apiID;
this.price = price;
this.interval = interval;
this.prefix = prefix;
this.onRotate = onRotate;
this._archive = {};
this._started = false;
};

KeySeries.prototype.encrypt = function(payload) {
var nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
var box = nacl.secretbox(cbor.encode(payload), nonce, this._archive[this.keyID]);
var fullMessage = new Uint8Array(nonce.length + box.length);
fullMessage.set(nonce);
fullMessage.set(box, nonce.length);
return fullMessage;
};

KeySeries.prototype.encryptKey = function(keyID, buyerPubKey) {
return sealedbox.seal(this._archive[this.keyID], buyerPubKey)
};

KeySeries.prototype.start = function() {
if (!this._started) {
this._rotate(this);
this._started = true;
}
};

KeySeries.prototype._rotate = function(context) {
context.keyID = nacl.randomBytes(16);
context._archive[context.keyID] = nacl.randomBytes(nacl.secretbox.keyLength);
context.onRotate(context);
// Rotate the keys
// FIXME: make this to wait for the above onRotate callback to finish
setTimeout(context._rotate, context.interval, context);
};

KeySeries.prototype.stop = function() {
if (this._started) {
this._started = false;
}
};

exports.KeySeries = KeySeries;
89 changes: 89 additions & 0 deletions lib/xbr/seller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
var autobahn = require("../autobahn.js");
var eth_accounts = require("web3-eth-accounts");
var eth_util = require("ethereumjs-util");
var key_series = require('./keyseries');
var util = require('../util.js');


var Seller = function (sellerKey) {
self = this;
this.sellerKey = sellerKey;
this.keys = {};
this.keysMap = {};
this._providerID = eth_util.bufferToHex(eth_util.privateToPublic(sellerKey));
this._session = null;
this.sessionRegs = [];
this._deferred_factory = util.deferred_factory();

var account = new eth_accounts.Accounts().privateKeyToAccount(sellerKey);
this._addr = eth_util.toBuffer(account.address);
this._privateKey = eth_util.toBuffer(account.privateKey);
};

Seller.prototype.start = function (session) {
self._session = session;

var d = this._deferred_factory();
var procedure = 'xbr.protocol.' + self._providerID + '.sell';
session.register(procedure, self.sell).then(
function (registration) {
self.sessionRegs.push(registration);
for (var key in self.keys) {
self.keys[key].start();
}
d.resolve();
},
function (error) {
console.log("Registration failed:", error);
d.reject();
}
);
return util.promise(d);
};

Seller.prototype.sell = function (key_id, buyer_pubkey) {
if (!this.keysMap.hasOwnProperty(key_id)) {
throw "no key with ID " + key_id;
}
return this.keysMap[key_id].encryptKey(key_id, buyer_pubkey)
};

Seller.prototype.add = function (apiID, prefix, price, interval) {
var keySeries = new key_series.KeySeries(apiID, prefix, price, interval, _onRotate);
this.keys[apiID] = keySeries;
return keySeries;
};

var _onRotate = function (series) {
self.keysMap[series.keyID] = series;

self._session.call(
'xbr.marketmaker.place_offer',
[series.keyID, series.apiID, series.prefix, BigInt(Date.now() * 1000000 - 10 * 10 ** 9),
self._addr, autobahn.nacl.randomBytes(64)],
{price: series.price, provider_id: self._providerID}
).then(
function (result) {
console.log("Offer placed for key", result['key']);
},
function (error) {
console.log("Call failed:", error);
}
)
};

Seller.prototype.stop = function () {
for (var key in this.keys) {
this.keys[key].stop()
}

for (var i = 0; i < this.sessionRegs.length; i++) {
this.sessionRegs[i].unregister()
}
};

Seller.prototype.wrap = function (api_id, uri, payload) {
return this.keys[api_id].encrypt(payload)
};

exports.SimpleSeller = Seller;
2 changes: 2 additions & 0 deletions lib/xbr/xbr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exports.SimpleBuyer = require('./buyer.js').SimpleBuyer;
exports.SimpleSeller = require('./seller.js').SimpleSeller;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"randombytes": ">=2.0.6",
"tweetnacl": ">= 0.14.3",
"tweetnacl-sealedbox-js": ">=1.1.0",
"web3": ">=1.0.0-beta.53",
"when": ">= 3.7.7",
"ws": ">= 1.1.4"
},
Expand Down

0 comments on commit c3ca7c1

Please sign in to comment.