Skip to content

Commit

Permalink
feat: relay quotes for unknown ledgers
Browse files Browse the repository at this point in the history
  • Loading branch information
sentientwaffle committed Aug 16, 2016
1 parent e328ffc commit 07d8913
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 201 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ 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_ROUTE_BROADCAST_ENABLED` (default: `1`) whether or not to broadcast known routes.
* `CONNECTOR_ROUTE_BROADCAST_INTERVAL` (default: `30000`) the frequency at which the connector broadcasts its routes to adjacent connectors.
* `CONNECTOR_ROUTE_CLEANUP_INTERVAL` (default: `1000`) the frequency at which the connector checks for expired routes.
* `CONNECTOR_ROUTE_EXPIRY` (default: `45000`) the maximum age of a route.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
"debug": "^2.2.0",
"eventemitter2": "^1.0.3",
"five-bells-condition": "^3.2.0",
"five-bells-routing": "~3.1.0",
"five-bells-routing": "~4.0.0",
"five-bells-shared": "^18.1.0",
"ilp-core": "~6.0.0",
"ilp-core": "~7.0.0",
"ilp-plugin-bells": "^4.0.0",
"ilp-plugin-virtual": "^4.0.1",
"koa": "^1.0.0",
Expand Down
6 changes: 5 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ function listen (koaApp, config, core, backend, routeBuilder, routeBroadcaster)
process.exit(1)
}
yield subscriptions.subscribePairs(config.get('tradingPairs'), core, config, routeBuilder)
yield routeBroadcaster.start()
if (config.routeBroadcastEnabled) {
yield routeBroadcaster.start()
} else {
yield routeBroadcaster.reloadLocalRoutes()
}
}).catch((err) => log.error(err))
}

Expand Down
1 change: 1 addition & 0 deletions src/controllers/quote.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const InvalidAmountSpecifiedError = require('../errors/invalid-amount-specified-
* @apiParam {Number} [source_expiry_duration="(Minimum allowed based on
* destination_expiry_duration)"] Number of milliseconds between when the
* destination transfer is proposed and when it expires
* @apiParam {Number} [slippage] Use a slippage other than the connector's default
*
* @apiDescription Get a quote from the connector based on either a fixed source
* or fixed destination amount.
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.post = function * () {
this.routeBroadcaster.addConnector(routes[0].connector)
}

if (gotNewRoute) {
if (gotNewRoute && this.config.routeBroadcastEnabled) {
co(this.routeBroadcaster.broadcast.bind(this.routeBroadcaster))
.catch(function (err) {
log.warn('error broadcasting routes: ' + err.message)
Expand Down
4 changes: 4 additions & 0 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ function getLocalConfig () {
// when using the ilp-quote backend)
const backendUri = Config.getEnv(envPrefix, 'BACKEND_URI')

const routeBroadcastEnabledString = Config.getEnv(envPrefix, 'ROUTE_BROADCAST_ENABLED')
const routeBroadcastEnabled =
routeBroadcastEnabledString ? Config.castBool(routeBroadcastEnabledString) : true
const routeBroadcastInterval =
Number(Config.getEnv(envPrefix, 'ROUTE_BROADCAST_INTERVAL')) || DEFAULT_ROUTE_BROADCAST_INTERVAL
const routeCleanupInterval =
Expand Down Expand Up @@ -237,6 +240,7 @@ function getLocalConfig () {
server,
backendUri,
notifications,
routeBroadcastEnabled,
routeBroadcastInterval,
routeCleanupInterval,
routeExpiry,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ module.exports = function (options) {
const routingTables = options.routingTables

const core = new ilpCore.Core({routingTables})
Object.keys(options.config.ledgerCredentials).forEach((ledgerPrefix) => {
const creds = _.clone(options.config.ledgerCredentials[ledgerPrefix])
Object.keys(config.ledgerCredentials).forEach((ledgerPrefix) => {
const creds = _.clone(config.ledgerCredentials[ledgerPrefix])
const store = creds.store && newSqliteStore(creds.store)

creds.prefix = ledgerPrefix
Expand Down
132 changes: 61 additions & 71 deletions src/lib/route-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const UnacceptableAmountError = require('../errors/unacceptable-amount-error')
const UnacceptableRateError = require('../errors/unacceptable-rate-error')
const getDeterministicUuid = require('../lib/utils').getDeterministicUuid
const log = require('../common/log').create('route-builder')
const addressToLedger = require('./utils').addressToLedger

class RouteBuilder {
/**
Expand All @@ -32,55 +31,64 @@ class RouteBuilder {
}

/**
* @param {Object} query
* @param {String} query.sourceLedger
* @param {String} query.sourceAmount
* @param {String} query.destinationLedger
* @param {String} query.destinationAmount
* @param {Object} query.destinationPrecisionAndScale optional
* @param {Object} params
* @param {String} params.sourceAddress
* @param {String} [params.sourceAmount]
* @param {Number} [params.sourceExpiryDuration]
* @param {String} params.destinationAddress
* @param {String} [params.destinationAmount]
* @param {Number} [params.destinationExpiryDuration]
* @param {Object} [params.destinationPrecisionAndScale]
* @param {Object} [params.slippage]
* @returns {Quote}
*/
* getQuote (query) {
log.info('creating quote sourceLedger=%s sourceAmount=%s ' +
'destinationLedger=%s destinationAmount=%s ' +
'destinationPrecisionAndScale=%s',
query.sourceLedger, query.sourceAmount,
query.destinationLedger, query.destinationAmount,
query.destinationPrecisionAndScale)
* getQuote (params) {
log.info('creating quote sourceAddress=%s sourceAmount=%s ' +
'destinationAddress=%s destinationAmount=%s ' +
'destinationPrecisionAndScale=%s slippage=%s',
params.sourceAddress, params.sourceAmount,
params.destinationAddress, params.destinationAmount,
params.destinationPrecisionAndScale, params.slippage)
const info = {}
const _nextHop = this._findNextHop(query)
if (!_nextHop) throwAssetsNotTradedError()
if (query.sourceAmount) {
const amount = new BigNumber(_nextHop.finalAmount)
const amountWithSlippage = amount.times(1 - this.slippage)
_nextHop.finalAmount = amountWithSlippage.toString()
info.slippage = (amount - amountWithSlippage).toString()
const quote = yield this.core.quote({
sourceAddress: params.sourceAddress,
sourceAmount: params.sourceAmount,
destinationAddress: params.destinationAddress,
destinationAmount: params.destinationAmount,
sourceExpiryDuration: params.sourceExpiryDuration,
destinationExpiryDuration: params.destinationExpiryDuration,
destinationPrecisionAndScale: params.destinationPrecisionAndScale
})
if (!quote) throwAssetsNotTradedError()

const slippage = params.slippage ? +params.slippage : this.slippage
if (params.sourceAmount) {
const amount = new BigNumber(quote.destinationAmount)
const amountWithSlippage = amount.times(1 - slippage)
quote.destinationAmount = amountWithSlippage.toString()
info.slippage = amount.minus(amountWithSlippage).toString()
} else { // fixed destinationAmount
const amount = new BigNumber(_nextHop.sourceAmount)
const amountWithSlippage = amount.times(1 + this.slippage)
_nextHop.sourceAmount = amountWithSlippage.toString()
info.slippage = (amount - amountWithSlippage).toString()
const amount = new BigNumber(quote.sourceAmount)
const amountWithSlippage = amount.times(1 + slippage)
quote.sourceAmount = amountWithSlippage.toString()
info.slippage = amount.minus(amountWithSlippage).toString()
}

// Round in favor of the connector (source amount up; destination amount down)
// to ensure it doesn't lose any money. The amount is quoted using the unshifted rate.
const nextHop = yield this._roundHop(_nextHop, 'up', 'down',
query.destinationPrecisionAndScale)

const quote = {
source_connector_account:
this.core.getPlugin(nextHop.sourceLedger).getAccount(),
source_ledger: nextHop.sourceLedger,
source_amount: nextHop.sourceAmount,
destination_ledger: nextHop.finalLedger,
destination_amount: nextHop.finalAmount,
_hop: nextHop
}

if (query.explain) {
quote.additional_info = _.assign({}, nextHop.additionalInfo, info)
}

return quote
const roundedSourceAmount = yield this._roundAmount(
'source', 'up', quote.sourceLedger, quote.sourceAmount)
const roundedDestinationAmount = yield this._roundAmount(
'destination', 'down', quote.destinationLedger, quote.destinationAmount,
params.destinationPrecisionAndScale)

return _.omitBy(Object.assign(quote, {
sourceAmount: roundedSourceAmount,
destinationAmount: roundedDestinationAmount,
sourceExpiryDuration: quote.sourceExpiryDuration.toString(),
destinationExpiryDuration: quote.destinationExpiryDuration.toString(),
additionalInfo: _.assign({}, quote.additionalInfo, info)
}), _.isUndefined)
}

/**
Expand All @@ -106,21 +114,23 @@ class RouteBuilder {
}

const sourceLedger = sourceTransfer.ledger
const finalLedger = addressToLedger(ilpHeader.account)
// Use `findBestHopForSourceAmount` since the source amount includes the slippage.
const _nextHopBySourceAmount = this.routingTables.findBestHopForSourceAmount(
sourceLedger, finalLedger, sourceTransfer.amount)
if (!_nextHopBySourceAmount) throwAssetsNotTradedError()
const nextHop = this.routingTables.findBestHopForSourceAmount(
sourceLedger, ilpHeader.account, sourceTransfer.amount)
if (!nextHop) throwAssetsNotTradedError()
// Round in favor of the connector. findBestHopForSourceAmount uses the
// local (unshifted) routes to compute the amounts, so the connector rounds
// in its own favor to ensure it won't lose money.
const nextHop = yield this._roundHop(_nextHopBySourceAmount, 'up', 'down')
nextHop.destinationAmount = yield this._roundAmount('destination', 'down',
nextHop.destinationLedger, nextHop.destinationAmount)

// Check if this connector can authorize the final transfer.
if (nextHop.isFinal) {
const roundedFinalAmount = yield this._roundAmount('destination', 'down',
nextHop.finalLedger, nextHop.finalAmount)
// Verify ilpHeader.amount ≤ nextHop.finalAmount
const expectedFinalAmount = new BigNumber(ilpHeader.amount)
if (expectedFinalAmount.greaterThan(nextHop.finalAmount)) {
if (expectedFinalAmount.greaterThan(roundedFinalAmount)) {
throw new UnacceptableRateError('Payment rate does not match the rate currently offered')
}
// TODO: Verify atomic mode notaries are trusted
Expand Down Expand Up @@ -160,34 +170,13 @@ class RouteBuilder {
}, _.isUndefined)
}

_findNextHop (query) {
return query.sourceAmount
? this.routingTables.findBestHopForSourceAmount(
query.sourceLedger, query.destinationLedger, query.sourceAmount)
: this.routingTables.findBestHopForDestinationAmount(
query.sourceLedger, query.destinationLedger, query.destinationAmount)
}

_getDestinationExpiry (sourceExpiry) {
if (!sourceExpiry) return
const sourceExpiryTime = (new Date(sourceExpiry)).getTime()
const minMessageWindow = this.minMessageWindow * 1000
return (new Date(sourceExpiryTime - minMessageWindow)).toISOString()
}

/**
* Round the hop's amounts according to the corresponding ledgers' scales/precisions.
*/
* _roundHop (hop, sourceUpDown, destinationUpDown, destinationPrecisionAndScale) {
hop.sourceAmount = yield this._roundAmount('source', sourceUpDown, hop.sourceLedger, hop.sourceAmount)
hop.destinationAmount = yield this._roundAmount('destination', destinationUpDown, hop.destinationLedger, hop.destinationAmount, destinationPrecisionAndScale)
// Only round the final amount if we know the precision/scale.
if (hop.isFinal || destinationPrecisionAndScale) {
hop.finalAmount = yield this._roundAmount('destination', destinationUpDown, hop.finalLedger, hop.finalAmount, destinationPrecisionAndScale)
}
return hop
}

/**
* Round amounts against the connector's favor. This cancels out part of the
* connector's rate curve shift by 1/10^scale.
Expand All @@ -196,10 +185,11 @@ class RouteBuilder {
* @param {String} upOrDown "up" or "down"
* @param {URI} ledger
* @param {String} amount
* @param {Object} _precisionAndScale optional
* @param {Object} [_precisionAndScale]
* @returns {String} rounded amount
*/
* _roundAmount (sourceOrDestination, upOrDown, ledger, amount, _precisionAndScale) {
if (!_precisionAndScale && !this.core.getPlugin(ledger)) return amount
const precisionAndScale = _precisionAndScale || (yield this.infoCache.get(ledger))
const roundedAmount = new BigNumber(amount).toFixed(precisionAndScale.scale,
upOrDown === 'down' ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP)
Expand Down
7 changes: 1 addition & 6 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,7 @@ function getDeterministicUuid (secret, input) {
return chars.join('')
}

function addressToLedger (address) {
return address.split('.').slice(0, -1).join('.') + '.'
}

module.exports = {
getPairs,
getDeterministicUuid,
addressToLedger
getDeterministicUuid
}
Loading

0 comments on commit 07d8913

Please sign in to comment.