项目作者: interledger-deprecated

项目描述 :
Framework for creating payment-channel based ILP ledger plugins
高级语言: JavaScript
项目地址: git://github.com/interledger-deprecated/ilp-plugin-payment-channel-framework.git


ilp-plugin-payment-channel-framework npm circle codecov

ILP virtual ledger plugin for directly transacting connectors, including a
framework for attaching on-ledger settlement mechanisms.

Installation

  1. npm install --save ilp-plugin-payment-channel-framework

Usage in ILP Kit

This section explains how to use ilp-plugin-payment-channel-framework for an asymmetric trustline in ILP Kit.

To setup an asymmetric trustline server, add the following to ILP Kit’s configuration file (Note that all of the following configuration should go into a single line in your config file):

  1. CONNECTOR_LEDGERS={
  2. "g.us.usd.myledger": {
  3. // your five-bells-ledger goes here
  4. }
  5. },
  6. "g.eur.mytrustline.": {
  7. "currency": "EUR",
  8. "plugin": "ilp-plugin-payment-channel-framework",
  9. "options": {
  10. // listen for incoming connections
  11. "listener": {
  12. port: 1234,
  13. // if a certificate is provided, the server will listen using TLS
  14. // instead of plain websockets
  15. cert: '/tmp/snakeoil.crt',
  16. key: '/tmp/snakeoil.key',
  17. ca: '/tmp/snakeoil-ca.crt'
  18. },
  19. "incomingSecret": "shared_secret", // auth_token which the server expects the client to send
  20. // the server determines the properties of the trustline
  21. "maxBalance": "1000000000",
  22. "prefix": "g.eur.mytrustline.",
  23. "info": {
  24. "currencyScale": 9,
  25. "currencyCode": "EUR",
  26. "prefix": "g.eur.mytrustline.",
  27. "connectors": ["g.eur.mytrustline.server"]
  28. }
  29. },
  30. "store": true
  31. }
  32. }

Similarly, add the following to your ILP Kit’s config to setup an asymmetric trustline client (as with the server config, all of the following should go on a single line in your config file):

  1. CONNECTOR_LEDGERS={
  2. "g.us.usd.anotherledger": {
  3. // your five-bells-ledger goes here
  4. }
  5. },
  6. "g.eur.mytrustline.": {
  7. "currency": "EUR",
  8. "plugin": "ilp-plugin-payment-channel-framework",
  9. "options": {
  10. "server": "btp+wss://username:shared_secret@wallet1.example:1234/example_path"
  11. },
  12. "store": false
  13. }
  14. }

Note: You can provide both the server and listener properties in which
case the BTP peers will automatically elect one of them to be the server and
one of them to be the client.

ILP Plugin Payment Channel Framework

The plugin payment channel framework includes all the functionality of
ilp-plugin-virtual, but wrapped around a Payment Channel
Module
. A payment channel module includes
methods for securing a trustline balance, whether by payment channel claims or
by periodically sending unconditional payments. The common functionality, such
as implementing the ledger plugin interface, logging transfers, keeping
balances, etc. are handled by the payment channel framework itself.

ILP Plugin virtual exposes a field called makePaymentChannelPlugin. This function
takes a Payment Channel Module, and returns a
LedgerPlugin class.

Minimal client-server config example

  1. const ObjStore = require('./test/helpers/objStore')
  2. const Plugin = require('.')
  3. const port = 9000
  4. const incomingSecret = 'pass'
  5. const server = new Plugin({
  6. listener: { port },
  7. prefix: 'some.ledger.',
  8. info: {},
  9. incomingSecret,
  10. maxBalance: '10000',
  11. _store: new ObjStore()
  12. })
  13. const client = new Plugin({
  14. server: 'btp+ws://:' + incomingSecret + '@localhost:' + port,
  15. maxBalance: '10000',
  16. _store: new ObjStore()
  17. })
  18. server.connect()
  19. .then(() => client.connect())
  20. .then(() => client.disconnect())
  21. .then(() => server.disconnect())

Example Code with Claim-Based Settlement

Claim-based settlement is the simple case that this framework uses as its
abstraction for settlement. Claim based settlement uses a unidirectional
payment channel. You put your funds on hold, and give your peer signed claims
for more and more of the funds. These signed claims are passed off-ledger, and
your peer submits the highest claim when they want to get their funds.

Claim based settlement has been implemented on ripple with the PayChan
functionality, or on
bitcoin (and
many other blockchains) by signing transactions that pay out of some script
output.

  1. const { makePaymentChannelPlugin } = require('ilp-plugin-virtual')
  2. const { NotAcceptedError } = require('ilp-plugin-shared').Errors
  3. const Network = require('some-example-network')
  4. const BigNumber = require('bignumber.js')
  5. return makePaymentChannelPlugin({
  6. // initialize fields and validate options in the constructor
  7. constructor: function (ctx, opts) {
  8. // we have one maxValueTracker to track the best incoming claim we've
  9. // gotten so far. it starts with a value of '0', and contains no data.
  10. ctx.state.bestClaim = ctx.backend.getMaxValueTracker('incoming_claim')
  11. // the 'maxUnsecured' option is taken from the plugin's constructor.
  12. // it defines how much the best incoming claim can differs from the amount
  13. // of incoming transfers.
  14. ctx.state.maxUnsecured = opts.maxUnsecured
  15. // use some preconfigured secret for authentication
  16. ctx.state.authToken = opts.authToken
  17. },
  18. // This token will be sent as a bearer token with outgoing requests, and used
  19. // to authenticate the incoming requests. If the network you're using has a
  20. // way to construct a shared secret, the authToken can be created
  21. // automatically instead of being pre-shared and configured.
  22. getAuthToken: (ctx) => (ctx.state.authToken),
  23. // the connect function runs when the plugin is connected.
  24. connect: async function (ctx) {
  25. // network initiation should happen here. In a claim-based plugin, this
  26. // would be the place to connect to the network and initiate payment
  27. // channels if they don't exist already.
  28. await Network.connectToNetwork()
  29. // establish metadata during the connection phase
  30. ctx.state.prefix = 'peer.network.' + (await Network.getChannelId()) + '.'
  31. // create ILP addresses for self and peer by appending identifiers from the
  32. // network onto the prefix.
  33. ctx.state.account = ctx.state.prefix + (await Network.getChannelSource())
  34. ctx.state.peer = ctx.state.prefix + (await Network.getChannelDestination())
  35. ctx.state.info = {
  36. prefix: ctx.state.prefix,
  37. currencyScale: 6,
  38. currencyCode: 'X??',
  39. connectors: []
  40. }
  41. },
  42. // Synchronous functions in order to get metadata. They won't be called until
  43. // after the plugin is connected.
  44. getAccount: (ctx) => (ctx.state.account),
  45. getPeerAccount: (ctx) => (ctx.state.peer),
  46. getInfo: (ctx) => (ctx.state.info),
  47. // this function is called every time an incoming transfer has been prepared.
  48. // throwing an error will stop the incoming transfer from being emitted as an
  49. // event.
  50. handleIncomingPrepare: async function (ctx, transfer) {
  51. // we get the incomingFulfilledAndPrepared because it represents the most
  52. // that can be owed to us, if all prepared transfers get fulfilled. The
  53. // 'transfer' has already been applied to this balance.
  54. const incoming = await ctx.transferLog.getIncomingFulfilledAndPrepared()
  55. const bestClaim = await ctx.state.bestClaim.getMax() || { value: '0', data: null }
  56. // make sure that if all incoming transfers are fulfilled (including the
  57. // new one), it won't put us too far from the best incoming claim we've
  58. // gotten. 'incoming - bestClaim.value' is the amount that our peer can
  59. // default on, so it's important we limit it.
  60. const exceeds = new BigNumber(incoming)
  61. .minus(bestClaim.value)
  62. .greaterThan(ctx.state.maxUnsecured)
  63. if (exceeds) {
  64. throw new NotAcceptedError(transfer.id + ' exceeds max unsecured balance')
  65. }
  66. },
  67. // this function is called whenever the outgoingBalance changes by a
  68. // significant amount. Exactly when and how often it will be called may
  69. // become configurable or change in the future, so your code should not rely
  70. // on it being called after every transfer.
  71. createOutgoingClaim: async function (ctx, outgoingBalance) {
  72. // create a claim for the total outgoing balance. This call is idempotent,
  73. // because it's relating to the absolute amount owed, and doesn't modify
  74. // anything.
  75. const claim = Network.createClaim(outgoingBalance)
  76. // return an object with the claim and the amount that the claim is for.
  77. // this will be passed into your peer's handleIncomingClaim function.
  78. return {
  79. balance: outgoingBalance,
  80. claim: claim
  81. }
  82. },
  83. // this function is called right after the peer calls createOutgoingClaim.
  84. handleIncomingClaim: async function (ctx, claimObject) {
  85. const { balance, claim } = claimObject
  86. if (Network.verify(claim, balance)) {
  87. // if the incoming claim is valid and it's better than your previous best
  88. // claim, set the bestClaim to the new one. If you already have a better
  89. // claim this will leave it intact. It's important to use the backend's
  90. // maxValueTracker here, because it will be shared across many processes.
  91. await ctx.state.bestClaim.setIfMax({ value: balance, data: claim })
  92. }
  93. },
  94. // called on plugin disconnect
  95. disconnect: async function (ctx) {
  96. const claim = await ctx.state.bestClaim.getMax()
  97. if (!claim) {
  98. return
  99. }
  100. // submit the best claim before disconnecting. This is the only time we
  101. // have to wait on the underlying ledger.
  102. await Network.submitClaim(claim)
  103. }
  104. })

Example Code with Unconditional Payment-Based Settlement

Unconditional payment settlement secures a trustline balance by sending payments
on a system that doesn’t support conditional transfers. Hashed timelock transfers
go through plugin virtual like a clearing layer, and every so often a settlement
is sent to make sure the amount secured on the ledger doesn’t get too far from
the finalized amount owed.

Unlike creating a claim, sending a payment has side-effects (it alters an
external system). Therefore, the code is slightly more complicated.

  1. const { makePaymentChannelPlugin } = require('ilp-plugin-virtual')
  2. const { NotAcceptedError } = require('ilp-plugin-shared').Errors
  3. const Network = require('some-example-network')
  4. const BigNumber = require('bignumber.js')
  5. return makePaymentChannelPlugin({
  6. // initialize fields and validate options in the constructor
  7. constructor: function (ctx, opts) {
  8. // In this type of payment channel module, we create a log of incoming
  9. // settlements to track all the transfers sent to us on the ledger we're
  10. // using for settlement. We use a transferLog in order to make sure a
  11. // single transfer can't be added twice.
  12. ctx.state.incomingSettlements = ctx.backend.getTransferLog('incoming_settlements')
  13. // The amount settled is used to track how much we've paid out in total.
  14. // We'll go deeper into how it's used in the `createOutgoingClaim`
  15. // function.
  16. ctx.state.amountSettled = ctx.backend.getMaxValueTracker('amount_settled')
  17. // In this type of payment channel backend, the unsecured balance we want
  18. // to limit is the total amount of incoming transfers minus the sum of all
  19. // the settlement transfers we've received.
  20. ctx.state.maxUnsecured = opts.maxUnsecured
  21. // use some preconfigured secret for authentication
  22. ctx.state.authToken = opts.authToken
  23. },
  24. getAuthToken: (ctx) => (ctx.state.authToken),
  25. connect: async function (ctx, opts) {
  26. await Network.connectToNetwork()
  27. // establish metadata during the connection phase
  28. ctx.state.prefix = 'peer.network.' + (await Network.getChannelId())
  29. ctx.state.account = await Network.getChannelSource()
  30. ctx.state.peer = await Network.getChannelDestination()
  31. ctx.state.info = {
  32. prefix: ctx.state.prefix,
  33. currencyScale: 6,
  34. currencyCode: 'X??',
  35. connectors: []
  36. }
  37. },
  38. // we don't need to define a disconnect handler in this case
  39. disconnect: function () => Promise.resolve(),
  40. // Synchronous functions in order to get metadata. They won't be called until
  41. // after the plugin is connected.
  42. getAccount: (ctx) => (ctx.state.prefix + ctx.state.account),
  43. getPeerAccount: (ctx) => (ctx.state.prefix + ctx.state.peer),
  44. getInfo: (ctx) => (ctx.state.info),
  45. handleIncomingPrepare: async function (ctx, transfer) {
  46. const incoming = await ctx.transferLog.getIncomingFulfilledAndPrepared()
  47. // Instead of getting the best claim, we're getting the sum of all our
  48. // incoming settlement transfers. This tells us how much incoming money has
  49. // been secured.
  50. const amountReceived = await ctx.state.incomingSettlements.getIncomingFulfilledAndPrepared()
  51. // The peer can default on 'incoming - amountReceived', so we want to limit
  52. // that amount.
  53. const exceeds = new BigNumber(incoming)
  54. .subtract(amountReceived)
  55. .greaterThan(ctx.state.maxUnsecured)
  56. if (exceeds) {
  57. throw new NotAcceptedError(transfer.id + ' exceeds max unsecured balance')
  58. }
  59. },
  60. // Even though this function is designed for creating a claim, we can
  61. // very easily repurpose it to make a payment for settlement.
  62. createOutgoingClaim: async function (ctx, outgoingBalance) {
  63. // If a new max value is set, the maxValueTracker returns the previous max
  64. // value. We tell the maxValueTracker that we're gonna pay the entire
  65. // outgoingBalance we owe, and then look at the difference between the last
  66. // balance and the outgoingBalance to determine how much to pay.
  67. // If we've already paid out more than outgoingBalance, then it won't be the
  68. // max value. The maxValueTracker will return outgoingBalance as the result,
  69. // and outgoingBalance - outgoingBalance is 0. Therefore, we send no payment.
  70. const lastPaid = await ctx.state.amountSettled.setIfMax({ value: outgoingBalance, data: null })
  71. const diff = new BigNumber(outgoingBalance)
  72. .sub(lastPaid.value)
  73. if (diff.lessThanOrEqualTo('0')) {
  74. return
  75. }
  76. // We take the transaction ID from the payment we send, and give it as an
  77. // identifier so our peer can look it up on the network and verify that we
  78. // paid them. Another approach could be to return nothing from this
  79. // function, and have the peer automatically track all incoming payments
  80. // they're notified of on the settlement ledger.
  81. const txid = await Network.makePaymentToPeer(diff)
  82. return { txid }
  83. },
  84. handleIncomingClaim: async function (ctx, claim) {
  85. const { txid } = claim
  86. const payment = await Network.getPayment(txid)
  87. if (!payment) {
  88. return
  89. }
  90. // It doesn't really matter whether this is fulfilled or not, we just need
  91. // it to affect the incoming balance so we know how much has been received.
  92. // We use the txid as the ID of the incoming payment, so it's impossible to
  93. // apply the same incoming settlement transfer twice.
  94. await ctx.state.incomingSettlements.prepare({
  95. id: txid,
  96. amount: payment.amount
  97. }, true) // isIncoming: true
  98. }
  99. }

Backend API, with Extensions for Payment Channels


getMaxValueTracker (opts)

Get a MaxValueTracker.

Parameters

  • opts.key (String) name of this value tracker. Creating a new value tracker with the same key will load the data from the store. Must use the base64url character set.

Returns

  • return (MaxValueTracker) max value tracker.

getTransferLog (opts)

Get a TransferLog.

Parameters

  • opts.key (String) Name of this transfer log. Creating a new transfer log with the same key will load the data from the store. Must use the base64url character set.
  • opts.maximum (String) The maximum sum of all transfers (including all incoming prepared transfers, but not outgoing prepared transfers) allowed in this transfer log. Default Infinity.
  • opts.minimum (String) The maximum sum of all transfers (including all outgoing prepared transfers, but not incoming prepared transfers) allowed in this transfer log. Default -Infinity.

Returns

  • return (MaxValueTracker) max value tracker.

async TransferLog.setMaximum (max)

Set the TransferLog’s maximum balance.

Parameters

  • max (Integer String) new maximum balance.

async TransferLog.setMinimum (min)

Set the TransferLog’s minimum balance.

Parameters

  • max (Integer String) new minimum balance.

async TransferLog.getMaximum ()

Get the TransferLog’s maximum balance. This is as high as the balance can go,
including all fulfilled transfers and incoming prepared transfers but not
including outgoing prepared transfers.

Returns

  • return (Integer String) maximum balance.

async TransferLog.getMinimum ()

Get the TransferLog’s minimum balance. This is as low as the balance can go,
including all fulfilled transfers and outgoing prepared transfers but not
including incoming prepared transfers.

Returns

  • return (Integer String) minimum balance.

async TransferLog.getBalance ()

Get the TransferLog’s balance, including only fulfilled transfers. This
function is best used for display purposes only. Validation should be done
using getIncomingFulfilled, getIncomingFulfilledAndPrepared,
getOutgoingFulfilled, or getOutgoingFulfilledAndPrepared.

Returns

  • return (Integer String) total fulfilled balance.

async TransferLog.get (id)

Get a transfer with info from the transfer log.

Parameters

  • id (UUID String) transfer ID.

Returns

  • return (TransferWithInfo) transfer with additional info.
  • return.transfer (Transfer) transfer with ID of id.
  • return.isIncoming (Boolean) set to true if the transfer is incoming, and false if it’s outgoing.
  • return.state (String) set to prepared, fulfilled, or cancelled.
  • return.fulfillment (String) transfer’s fulfillment, present only if the state is fulfilled.

async TransferLog.prepare (transfer, isIncoming)

Get a transfer from the transfer log. If the transfer would cause the balance to go over
the transfer log’s maximum or under its minimum, this function will throw an error. If the
transfer cannot be applied for any other reason, an error will be thrown.

If a transfer with the same ID and the same contents as a previous transfer is added, it
will return successfully, without modifying the database. If a transfer with the same ID as
a previous transfer is added with different contents, an error will be thrown.

Parameters

  • transfer (Transfer) the transfer to be prepared.
  • isIncoming (Boolean) set to true if the transfer is incoming, and false if it’s outgoing.

async TransferLog.fulfill (transferId, fulfillment)

Fulfill a transfer currently in the prepared state. If the transfer’s state is
already fullfilled, the function will return without error. If the transfer’s
state is cancelled, the function will throw an error. If the transfer’s state
is prepared, it will be set to fulfilled and the fulfillment will be
stored.

Important: The TransferLog is concerned only with storage, and making sure
that the sum of the transfers does not exceed its given limits. As such, the
fulfillment is not compared against the executionCondition of the transfer.
This allows more flexibility in how the TransferLog is used, but developers
should be careful to perform proper fulfillment validation in their own code.
Remember that SHA256(fulfillment) must equal executionCondition, and the
fulfillment should always be exactly 32 bytes.

Parameters

  • transferId (UUID String) ID of the transfer to fulfill.
  • fulfillment (String) Fulfillment to store with the transfer.

async TransferLog.cancel (transferId)

Cancel a transfer currently in the prepared state. If the transfer’s state is
already cancelled, the function will return without error. If the transfer’s
state is fulfilled, an error will be thrown. If the transfer’s state is
prepared, it will be set to cancelled.

Parameters

  • transferId (UUID String) ID of the transfer to cancel.

async TransferLog.getIncomingFulfilled ()

Get the sum of all incoming payments in the fulfilled state.

Returns

  • return (Integer String) incoming balance.

async TransferLog.getIncomingFulfilledAndPrepared ()

Get the sum of all incoming payments, including those which are in the prepared state.

Returns

  • return (Integer String) highest incoming balance.

async TransferLog.getOutgoingFulfilled ()

Get the sum of all outgoing payments in the fulfilled state.

Returns

  • return (Integer String) outgoing balance.

async TransferLog.getOutgoingFulfilledAndPrepared ()

Get the sum of all outgoing payments, including those which are in the prepared state.

Returns

  • return (Integer String) highest outgoing balance.

async MaxValueTracker.setIfMax (entry)

Put entry into the MaxValueTracker. If entry.value is larger than the
previous entry’s value, then entry becomes the new max entry, and the
previous max entry is returned. If entry.value is not larger than the
previous max entry’s value, then the max entry remains the same and entry is
returned back.

Parameters

  • entry (Object) entry to add to the max value tracker.
  • entry.value (Integer String) value to compare to the current max value.
  • entry.data (Object) data to attach to the entry.

Return

  • return (Object) previous max entry or entry.
  • return.value (Integer String) value of returned entry.
  • return.data (Object) data attached to returned entry.

async MaxValueTracker.getMax ()

Returns the max value tracker’s maximum entry.

Return

  • return (Object) max entry of the max value tracker.
  • return.value (Integer String) value of returned entry.
  • return.data (Object) data attached to returned entry.

Plugin Context API

The PluginContext is a bundle of objects passed into the Payment Channel
Backend methods, in order to access useful plugin state.

Field Type Description
state Object Object to keep Payment Channel Module state. Persists between function calls, but not if the plugin is restarted.
rpc RPC RPC object for this plugin. Can be used to call methods on peer.
backend ExtendedPluginBackend Plugin backend, for creating TransferLogs and MaxValueTrackers.
transferLog TransferLog Plugin’s TransferLog, containing all its ILP transfers.
plugin LedgerPlugin Plugin object. Only LedgerPlugin Interface functions should be accessed.

Payment Channel Module API

Calling makePaymentChannelPlugin with an object containing all of the
functions defined below will return a class. This new class will perform all
the functionality of ILP Plugin Virtual, and additionally use the supplied
callbacks to handle settlement.

Aside from connect and disconnect, the functions below might be called
during the flow of an RPC request, so they should run fast. Any of these calls
SHOULD NOT take longer than 500 ms if await-ed. If a slower operation
is required, it should be run in the background so it doesn’t block the flow of
the function.


pluginName

This optional field defines the type of this plugin. For instance, if it is set
to example, the class name will become PluginExample, and debug statements
will be printed as ilp-plugin-example.

Type

String


constructor (ctx, opts)

Called when the plugin is constructed.

Parameters

  • ctx (PluginContext) current plugin context.
  • opts (Object) options passed into plugin constructor.

async connect (ctx, opts)

Called when plugin.connect() is called.

Parameters

  • ctx (PluginContext) current plugin context.

getInfo (ctx)

Return the
LedgerInfo
of this payment channel. This function will not be called until after the
plugin is connected. The prefix must be deterministic, as the connector
requires plugin prefixes to be preconfigured.

The framework code will automatically create a deep clone of the return value
before returning to the plugin user.

Parameters

  • ctx (PluginContext) current plugin context.

getAccount (ctx)

Return the ILP address of this account on the payment channel. The ILP prefix
must match getInfo().prefix. This function will not be called until
after the plugin is connected.

Parameters

  • ctx (PluginContext) current plugin context.

getPeerAccount (ctx)

Return the ILP address of the peer’s account on the payment channel. The ILP
prefix must match getInfo().prefix. This function will not be called until
after the plugin is connected.

Parameters

  • ctx (PluginContext) current plugin context.

async disconnect (ctx)

Called when plugin.disconnect() is called.

Parameters

  • ctx (PluginContext) current plugin context.

async handleIncomingPrepare (ctx, transfer)

Called when an incoming transfer is being processed, but has not yet been
prepared. If this function throws an error, the transfer will not be prepared
and the error will be passed back to the peer.

Parameters

  • ctx (PluginContext) current plugin context.
  • transfer (Transfer) incoming transfer.

async createOutgoingClaim (ctx, balance)

Called when settlement is triggered. This may occur in the flow of a single payment,
or it may occur only once per several payments. The return value of this function is
passed to the peer, and into their handleIncomingClaim() function. The return value must
be stringifiable to JSON.

Parameters

  • ctx (PluginContext) current plugin context.
  • balance (Integer String) sum of all outgoing fulfilled transfers. This value is strictly increasing.

async handleIncomingClaim (ctx, claim)

Called after peer’s createOutgoingClaim() function is called.

Parameters

  • ctx (PluginContext) current plugin context.
  • claim (Object) return value of peer’s createOutgoingClaim() function.