Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BREAKING] destination routing #150

Merged
merged 3 commits into from
May 11, 2016
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ ex.
```

* `CONNECTOR_FX_SPREAD` (default: `0.002` =.2%) How much of a spread to add on top of the reference exchange rate. This determines the connector's margin.
* `CONNECTOR_SLIPPAGE` (default: `0.001` = 0.1%) The ratio for overestimating exchange rates to prevent payment failure if the rate changes.
* `CONNECTOR_MIN_MESSAGE_WINDOW` (default: `1`) Minimum time the connector wants to budget for getting a message to the ledgers its trading on. In seconds.
* `CONNECTOR_MAX_HOLD_TIME` (default: `10`) Maximum duration (seconds) the connector is willing to place funds on hold while waiting for the outcome of a transaction.
* `CONNECTOR_AUTH_CLIENT_CERT_ENABLED` (default `0`) whether or not to enable TLS Client Certificate authentication (requires HTTPS).
Expand All @@ -107,7 +108,6 @@ ex.
* `CONNECTOR_TLS_CERTIFICATE` (default: none) the path to the server certificate file. Required if using HTTPS.
* `CONNECTOR_TLS_CRL` (default: none) the path to the server certificate revokation list file. Optional if using HTTPS.
* `CONNECTOR_TLS_CA` (default: none) the path to a trusted certificate to be used in addition to using the [default list](https://github.com/nodejs/node/blob/v4.3.0/src/node_root_certs.h). Optional if using HTTPS.
* `CONNECTOR_QUOTE_FULL_PATH` (default: `''`) Feature to enable pathfinding for `/quote`.

#### Auto-funding

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@
"bignumber.js": "^2.0.7",
"co": "^4.1.0",
"co-body": "^4.0.0",
"co-defer": "^1.0.0",
"co-request": "^1.0.0",
"five-bells-condition": "^3.2.0",
"five-bells-pathfind": "~5.0.0",
"five-bells-shared": "^14.1.2",
"five-bells-routing": "~2.0.0",
"five-bells-shared": "^15.0.0",
"koa": "^1.0.0",
"koa-compress": "^1.0.6",
"koa-cors": "0.0.16",
Expand Down
6 changes: 4 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const metadata = require('./controllers/metadata')
const health = require('./controllers/health')
const pairs = require('./controllers/pairs')
const quote = require('./controllers/quote')
const routes = require('./controllers/routes')
const notifications = require('./controllers/notifications')
const subscriptions = require('./models/subscriptions')
const compress = require('koa-compress')
Expand All @@ -19,6 +20,7 @@ const Passport = require('koa-passport').KoaPassport
const cors = require('koa-cors')
const log = require('./common/log')
const backend = require('./services/backend')
const routeBroadcaster = require('./services/route-broadcaster')

function listen (koaApp, config, ledgers) {
if (config.getIn(['server', 'secure'])) {
Expand Down Expand Up @@ -58,7 +60,7 @@ function listen (koaApp, config, ledgers) {
// subscribes to all the ledgers in the background
co(function * () {
yield backend.connect()

yield routeBroadcaster.start()
yield subscriptions.subscribePairs(config.get('tradingPairs'), ledgers, config)
}).catch(function (err) {
log('app').error(typeof err === 'object' && err.stack || err)
Expand Down Expand Up @@ -87,7 +89,7 @@ function createApp (config, ledgers) {
koaApp.use(route.get('/pairs', pairs.getCollection))

koaApp.use(route.get('/quote', quote.get))
koaApp.use(route.get('/quote_local', quote.getLocal))
koaApp.use(route.post('/routes', routes.post))

koaApp.use(route.post('/notifications', notifications.post))

Expand Down
18 changes: 6 additions & 12 deletions src/controllers/quote.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,13 @@ const InvalidAmountSpecifiedError = require('../errors/invalid-amount-specified-

exports.get = function * () {
validateAmounts(this.query.source_amount, this.query.destination_amount)
if (!this.config.features.quoteFullPath) {
this.body = yield model.getLocalQuote(this.query, this.ledgers, this.config)
return
if (!this.query.source_account && !this.query.source_ledger) {
throw new InvalidUriParameterError('Missing required parameter: source_ledger or source_account')
}

if (!this.query.source_account) throw new InvalidUriParameterError('Missing required parameter: source_account')
if (!this.query.destination_account) throw new InvalidUriParameterError('Missing required parameter: destination_account')
this.body = yield model.getFullQuote(this.query, this.ledgers, this.config)
}

exports.getLocal = function * () {
validateAmounts(this.query.source_amount, this.query.destination_amount)
this.body = yield model.getLocalQuote(this.query, this.ledgers, this.config)
if (!this.query.destination_account && !this.query.destination_ledger) {
throw new InvalidUriParameterError('Missing required parameter: destination_ledger or destination_account')
}
this.body = yield model.getFullQuote(this.query, this.config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since src/models/quote.js only exposes one method, it may be clearer to call this getQuote.

}

function validateAmounts (sourceAmount, destinationAmount) {
Expand Down
23 changes: 23 additions & 0 deletions src/controllers/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict'

const requestUtil = require('five-bells-shared/utils/request')
const routingTables = require('../services/routing-tables')
const routeBroadcaster = require('../services/route-broadcaster')
const knownConnectors = {}

exports.post = function * () {
const routes = yield requestUtil.validateBody(this, 'Routes')

// TODO verify that POSTer of these routes matches route.connector.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sentientwaffle @justmoon How are connectors going to verify routes are coming from the right connector?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, eventually routes will arrive on a transfer's additional_info (as opposed to the dedicated /routes endpoint), so the sending connector's identity would be implicitly verified when they authorize the debit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, we'll have to talk more about that. I'm not sure it makes sense to add routes to the incoming transfer. Even if we want to have connectors pay one another it might make more sense to pre-pay a bit and draw down against a balance rather than pay for every advertisement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paying for advertisements should be a standard "paid HTTP" call. So let's make sure we record these requirements (e.g. "how do we know the advertisement is coming from a certain connector") for when we work on the paid-HTTP protocol.

It seems like what would happen is that the other connector would pay you (associating a sending identity with a token with a balance) and then use the token to pay for API calls. If that's the case, then you would be able to associate the token back to the sending identity.

for (const route of routes) {
routingTables.addRoute(route)
}

const connector = routes[0] && routes[0].connector
if (connector && !knownConnectors[connector]) {
yield routeBroadcaster.broadcast()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will only come back from the yield after all the notifications have been sent out to other connectors, shouldn't it respond to the connector that sent it this route update before sending out the other notifications?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right.

knownConnectors[connector] = true
}

this.status = 200
}
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ module.exports = {
_test: {
BalanceCache: require('./lib/balance-cache'),
balanceCache: balanceCache,
RoutingTables: require('./lib/routing-tables'),
RouteBroadcaster: require('./lib/route-broadcaster'),
RouteBuilder: require('./lib/route-builder'),
loadConnectorConfig: require('./lib/config'),
config: require('./services/config'),
logger: require('./common').log,
Expand Down
8 changes: 5 additions & 3 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ function getLocalConfig () {
JSON.parse(Config.getEnv(envPrefix, 'PAIRS') || 'false') || generateDefaultPairs(ledgers)

const features = {}
features.quoteFullPath = Config.castBool(Config.getEnv(envPrefix, 'QUOTE_FULL_PATH'))
features.debugAutoFund = Config.castBool(Config.getEnv(envPrefix, 'DEBUG_AUTOFUND'))

const adminEnv = parseAdminEnv()
Expand All @@ -195,12 +194,14 @@ function getLocalConfig () {

const expiry = {}
expiry.minMessageWindow =
Config.getEnv(envPrefix, 'MIN_MESSAGE_WINDOW') || 1 // seconds
expiry.maxHoldTime = Config.getEnv(envPrefix, 'MAX_HOLD_TIME') || 10 // seconds
+Config.getEnv(envPrefix, 'MIN_MESSAGE_WINDOW') || 1 // seconds
expiry.maxHoldTime = +Config.getEnv(envPrefix, 'MAX_HOLD_TIME') || 10 // seconds

// The spread is added to every quoted rate
const fxSpread = Number(Config.getEnv(envPrefix, 'FX_SPREAD')) || 0.002 // = .2%

const slippage = +Config.getEnv(envPrefix, 'SLIPPAGE') || 0.001 // = 0.1%
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you put these hard coded defaults up at the top of the file as const DEFAULT_MIN_MESSAGE_WINDOW or something like that?


// BACKEND_URI must be defined for backends that connect to an external
// component to retrieve the rate or amounts (it is therefore required
// when using the ilp-quote backend)
Expand Down Expand Up @@ -233,6 +234,7 @@ function getLocalConfig () {
backend,
ledgerCredentials,
fxSpread,
slippage,
expiry,
features,
admin,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ledgers/five-bells-ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ function FiveBellsLedger (options) {
this.config = options.config
}

FiveBellsLedger.validateTransfer = function (transfer) { validator.validate('TransferTemplate', transfer) }
FiveBellsLedger.prototype.validateTransfer = function (transfer) {
validator.validate('TransferTemplate', transfer)
}

// template - {amount}
FiveBellsLedger.prototype.makeFundTemplate = function (template) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ledgers/multiledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Multiledger.prototype.getType = function (ledgerId) {
// /////////////////////////////////////////////////////////////////////////////

Multiledger.prototype.validateTransfer = function (transfer) {
return this.ledger_types[transfer.type].validateTransfer(transfer)
return this.getLedger(transfer.ledger).validateTransfer(transfer)
}

Multiledger.prototype.makeFundTemplate = function (ledger, template) {
Expand Down
126 changes: 126 additions & 0 deletions src/lib/route-broadcaster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const co = require('co')
const defer = require('co-defer')
const _ = require('lodash')
const request = require('co-request')
const BROADCAST_INTERVAL = 30 * 1000 // milliseconds

class RouteBroadcaster {
/**
* @param {RoutingTables} routingTables
* @param {Backend} backend
* @param {Object} config
* @param {Object} config.ledgerCredentials
* @param {Object} config.tradingPairs
* @param {Number} config.minMessageWindow
*/
constructor (routingTables, backend, config) {
this.baseURI = routingTables.baseURI
this.routingTables = routingTables
this.backend = backend
this.ledgerCredentials = config.ledgerCredentials
this.tradingPairs = config.tradingPairs
this.minMessageWindow = config.minMessageWindow
this.adjacentConnectors = {}
this.adjacentLedgers = {}
for (const pair of config.tradingPairs) {
const destinationLedger = pair[1].split('@')[1]
this.adjacentLedgers[destinationLedger] = true
}
}

* start () {
yield this.crawlLedgers()
yield this.reloadLocalRoutes()
yield this.broadcast()
setInterval(() => this.routingTables.removeExpiredRoutes(), 1000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this interval should be configurable and/or have a default value defined at the top of the file

defer.setInterval(() => {
return this.reloadLocalRoutes().then(this.broadcast.bind(this))
}, BROADCAST_INTERVAL)
}

broadcast () {
const routes = this.routingTables.toJSON()
return Promise.all(
Object.keys(this.adjacentConnectors).map(
(adjacentConnector) => this._broadcastTo(adjacentConnector, routes)))
}

_broadcastTo (adjacentConnector, routes) {
return request({
method: 'POST',
uri: adjacentConnector + '/routes',
body: routes,
json: true
}).then((res) => {
if (res.statusCode !== 200) {
throw new Error('Unexpected status code: ' + res.statusCode)
}
})
}

crawlLedgers () {
return Object.keys(this.adjacentLedgers).map(this._crawlLedger, this)
}

* _crawlLedger (ledger) {
const res = yield request({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use the multiledger abstraction (if you don't want to change it here we can also make that change when everything is switched over to using the ledger plugin framework)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, its so far behind I'll just wait until we switch everything over.

method: 'GET',
uri: ledger + '/connectors',
json: true
})
if (res.statusCode !== 200) {
throw new Error('Unexpected status code: ' + res.statusCode)
}
const connectors = _.map(res.body, 'connector')
for (const connector of connectors) {
// Don't broadcast routes to ourselves.
if (connector === this.baseURI) continue
this.adjacentConnectors[connector] = true
}
}

/**
* @returns {Promise}
*/
reloadLocalRoutes () {
return this._getLocalRoutes().then(
(routes) => this.routingTables.addLocalRoutes(routes))
}

_getLocalRoutes () {
return Promise.all(this.tradingPairs.map((pair) => {
return this._tradingPairToQuote(pair)
.then((quote) => this._quoteToLocalRoute(quote))
}))
}

_tradingPairToQuote (pair) {
const sourceLedger = pair[0].split('@')[1]
const destinationLedger = pair[1].split('@')[1]
// TODO change the backend API to return curves, not points
return co(this.backend.getQuote.bind(this.backend), {
source_ledger: sourceLedger,
destination_ledger: destinationLedger,
source_amount: 100000000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 100000000? What if the backend says that's too much and doesn't return anything. I think I can see why you might be doing this here but then we should definitely make the change the backend API to return curves, not points soon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It creates a curve [[0, 0], [100000000, fx(100000000)]]. It is just a workaround until the backends are rewritten to return the curve directly.

})
}

_quoteToLocalRoute (quote) {
return {
source_ledger: quote.source_ledger,
destination_ledger: quote.destination_ledger,
connector: this.baseURI,
min_message_window: this.minMessageWindow,
source_account: this.ledgerCredentials[quote.source_ledger].account_uri,
destination_account: this.ledgerCredentials[quote.destination_ledger].account_uri,
points: [
[0, 0],
[+quote.source_amount, +quote.destination_amount]
]
}
}
}

module.exports = RouteBroadcaster
Loading