From c3ca7c1fd292a68df1a6e3bf108518adb110fd5f Mon Sep 17 00:00:00 2001 From: Omer Akram Date: Tue, 21 May 2019 17:56:02 +0500 Subject: [PATCH] Initial XBR protocol implementation (#424) * 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 --- lib/autobahn.js | 2 + lib/connection.js | 37 +------------ lib/util.js | 54 +++++++++++++++++++ lib/xbr/buyer.js | 125 +++++++++++++++++++++++++++++++++++++++++++ lib/xbr/keyseries.js | 50 +++++++++++++++++ lib/xbr/seller.js | 89 ++++++++++++++++++++++++++++++ lib/xbr/xbr.js | 2 + package.json | 1 + 8 files changed, 324 insertions(+), 36 deletions(-) create mode 100644 lib/xbr/buyer.js create mode 100644 lib/xbr/keyseries.js create mode 100644 lib/xbr/seller.js create mode 100644 lib/xbr/xbr.js diff --git a/lib/autobahn.js b/lib/autobahn.js index 09a15c1a..b4bda24f 100644 --- a/lib/autobahn.js +++ b/lib/autobahn.js @@ -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; @@ -71,3 +72,4 @@ exports.nacl = nacl; exports.util = util; exports.log = log; +exports.xbr = xbr; diff --git a/lib/connection.js b/lib/connection.js index fd040c54..f9891d55 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -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 diff --git a/lib/util.js b/lib/util.js index c29c91a1..31f6a760 100644 --- a/lib/util.js +++ b/lib/util.js @@ -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; diff --git a/lib/xbr/buyer.js b/lib/xbr/buyer.js new file mode 100644 index 00000000..06ced7d2 --- /dev/null +++ b/lib/xbr/buyer.js @@ -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; diff --git a/lib/xbr/keyseries.js b/lib/xbr/keyseries.js new file mode 100644 index 00000000..9743e102 --- /dev/null +++ b/lib/xbr/keyseries.js @@ -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; diff --git a/lib/xbr/seller.js b/lib/xbr/seller.js new file mode 100644 index 00000000..3ab5876e --- /dev/null +++ b/lib/xbr/seller.js @@ -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; diff --git a/lib/xbr/xbr.js b/lib/xbr/xbr.js new file mode 100644 index 00000000..bc971b69 --- /dev/null +++ b/lib/xbr/xbr.js @@ -0,0 +1,2 @@ +exports.SimpleBuyer = require('./buyer.js').SimpleBuyer; +exports.SimpleSeller = require('./seller.js').SimpleSeller; diff --git a/package.json b/package.json index 56709a73..b4e8c3af 100644 --- a/package.json +++ b/package.json @@ -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" },