Giter Site home page Giter Site logo

interledgerjs / ilp-protocol-stream Goto Github PK

View Code? Open in Web Editor NEW
32.0 16.0 15.0 2.73 MB

Moved to monorepo in interledgerjs/interledgerjs

Home Page: https://github.com/interledger/rfcs/blob/master/0029-stream/0029-stream.md

JavaScript 2.28% TypeScript 97.72%
interledger ilp streaming-payments micropayments transport chunked-payments stream

ilp-protocol-stream's Introduction

Overview

This document describes the JavaScript implementation of Interledger's STREAM transport protocol.

NPM Package CircleCI codecov JavaScript Style Guide Known Vulnerabilities Greenkeeper badge FOSSA Status

References

Table of Contents

STREAM protocol overview

STREAM stands for Streaming Transport for the Real-time Exchange of Assets and Messages. It’s a transport-layer protocol that's used to send packets of money and data through the Interledger network, rather than over the internet.

STREAM in protocol suite

STREAM is designed for use by applications that stream micropayments and those that deliver larger, discrete payments. It's responsible for:

  • Defining the conditions and fulfillments used in the Interledger protocol layer
  • Grouping and retrying packets to achieve a desired outcome
  • Determining the effective exchange rate of a payment
  • Adapting to the speed at which money can be sent, and for what amounts (congestion and flow control)
  • Encrypting and decrypting data

STREAM connections

A STREAM connection is a session established between two endpoints (a client and a server). Since STREAM is an Interledger protocol, the connection occurs over the Interledger network, not the internet.

A single connection can support multiple streams, which are logical, bidirectional channels over which money and data are sent and received.

Open a STREAM connection

Before establishing a STREAM connection, the client must request and receive a destination ILP address and shared secret from the server. This exchange is not handled by the STREAM protocol. It must be handled by a higher-level protocol and/or out-of-band authenticated communication channel. The Simple Payment Setup Protocol (SPSP) is an Interledger application-layer protocol that can be used for this purpose.

When the client receives the destination address and shared secret, it passes the credentials to its STREAM client module. The module then initiates a STREAM connection to the server over the Interledger network.

STREAM and SPSP servers

STREAM and SPSP are separate entities though they often run in the same process. This is because the STREAM server that handles incoming funds must use the shared secret that was generated by the SPSP server's generateAddressAndSecret call.

While SPSP uses HTTPS to communicate the destination address and shared secret to the client, STREAM does not create the connection over the internet. The connection is created over the Interledger network.

Creating one STREAM server object through the protocol-stream library and using the object for both the STREAM and SPSP servers simply saves you some work. They are often put together in flow diagrams because that's how they are most commonly implemented.

Streams

A stream is a logical channel on which money and data is sent and received over a STREAM connection. A single connection, using a single shared secret, can support multiple streams.

Either endpoint (client or server) can open a new stream. Multiple streams can deliver smaller payments on an ongoing basis or a single stream can deliver one large, discreet payment.

Install the STREAM module

Install the ilp-protocol-stream module into your Node.js project. You must be running Node 10 or greater. You need the module if you intend to develop a client or run a server that supports Interledger.

$ npm install ilp-protocol-stream

Also, ilp-protocol-stream will run in a browser via webpack, but only as a client.

Dependencies

The ilp-protocol-stream module is bundled with a set of dependencies.

  • Bundle dependencies
    • ilp-logger - Debug logging utility for Interledger modules
    • ilp-packet - Codecs for ILP packets and messages
    • ilp-protocol-ildcp - Transfers node and ledger information from a parent node to a child node
  • Development dependencies
    • ilp-plugin - Gets ILP credentials
    • ilp-plugin-btp - Acts as a building block for plugins that don't have an underlying ledger

Usage examples

Exchange destination address and shared secret

Server

The server must generate a destination account address and shared secret for each client, then pass the information to the client through an authenticated communication channel.

const { createServer } = require('ilp-protocol-stream')
const Plugin = require('ilp-plugin-btp')


// Connects to the given plugin and waits for streams.
async function run () {
  const server = await createServer({
    plugin: new Plugin({ server: process.env.BTP_SERVER })
  })

  const { destinationAccount, sharedSecret } = server.generateAddressAndSecret()
}

Client

After the server provides the destination account and shared secret, the client can create a STREAM connection to the server.

const { createConnection } = require('ilp-protocol-stream')
const getPlugin = require('ilp-plugin-btp')

const { destinationAccount, sharedSecret } =
  providedByTheServerSomehow()

async function run () {
  const connection = await createConnection({
    plugin: new Plugin({ server: process.env.BTP_SERVER}),
    destinationAccount,
    sharedSecret
  })
}

Create a STREAM connection

Server

The server listens for an incoming STREAM connection from the client.

server.on('connection', connection => { ...

Client

The client creates a STREAM connection to the server.

const connection = await createConnection({
  plugin: new Plugin({ server: process.env.BTP_SERVER}),
  destinationAccount,
  sharedSecret
})

After the connection is opened between the client and server, the connection is ready to support streams. A single connection, using a single shared secret, can support multiple streams.

Send and receive on streams

Streams are bidirectional, meaning both endpoints can send and receive on a stream. Each stream is its own object that emits events when money comes in or goes out.

Streams returned by connection.createStream, or produced by the stream event on a STREAM server, expose the Node.js Stream API for sending data. They also expose functions for sending and receiving money.

Payments are sent and received using setSendMax and setReceiveMax. setSendMax and setReceiveMax set the maximum amount an individual stream can send and receive, thereby enforcing sending and receiving limits. These calls occur instantly, before any funds are sent or received.

The amounts given for both functions are denominated in the connection plugin’s units. This amount is absolute, not relative. For example, calling setSendMax(100) twice on a single stream only allows the stream to send 100 units.

When used with event listeners, these calls can supply you with a high level of detail by providing events for each individual micropayment.

Set a send max and send a stream

setSendMax sets the maximum amount an individual stream can send, thereby enforcing a sending limit. This call occurs instantly, before any funds are sent.

A payment is automatically triggered when the setSendMax value is more than the amount already sent. As an example, micropayments in Web Monetization would be a single stream. A setSendMax would be called on some time interval where each time it’s called, it’s called with a higher amount.

The stream will always attempt to send up to its defined max, but ultimately will only send as much as the receiver is willing to receive. If the receiver has set a receiveMax, then the amount is adjusted by the exchange rate.

sendMax example

stream.on('outgoing_money', amount => {
  console.log('sent', amount)
})

stream.setSendMax(100)

In the following scenario, the receiver isn’t willing to receive the whole amount, but some money still moves. The sender will send as much as they can.

Sender

stream.on('outgoing_money', amount => {
  console.log('sent', amount)
})
stream.setSendMax(100)

Receiver

server.on('connection', connection => {
  connection.on('stream', stream => {
    stream.setReceiveMax(75)
    stream.on('money', amount => {
      console.log(`got money: ${amount} on stream ${stream.id}`)
    })
    stream.on('end', () => {
      console.log('stream closed')
    })
  })
})

Set a receive max and receive a stream

setReceiveMax sets the maximum amount an individual stream can receive, thereby enforcing a receiving limit. This call occurs instantly, before any funds are received.

Payments are automatically received when the setReceiveMax value is more than the amount already received.

The stream will always attempt to receive up to its defined max. If the sender has set a sendMax, then the amount is adjusted by the exchange rate.

receiveMax example

To receive a stream, the server listens for a connection event.

Since each connection can include many separate streams of money and data, the connection then listens for stream events.

server.on('connection', connection => {
  connection.on('stream', stream => {
    stream.setReceiveMax(10000)
    stream.on('money', amount => {
      console.log(`got money: ${amount} on stream ${stream.id}`)
    })
  })
})

The stream.id is handled by the STREAM library in the background. Server-initiated streams are assigned an even number. Client-initiated streams are assigned an odd number.

Stream multiple payments on a single connection

Micropayments are sent and received the same way as a single payment, except that multiple streams are created and closed.

Client

const stream = connection.createStream()
stream.setSendMax(10)

const stream2 = connection.createStream()
stream2.setSendMax(25)

Server

server.on('connection', connection => {
  connection.on('stream', stream => {

    stream.setReceiveMax(10000)

    stream.on('money', amount => {
      console.log(`got money: ${amount} on stream ${stream.id}`)
    })

    stream.on('end', () => {
      console.log('stream closed')
    })
  })
})

Stream data

You can stream data using the same interface as the Node.js Stream API.

Send data

const stream = connection.createStream()
stream.write('hello\n')
stream.write('here is some more data')
stream.end()

Receive data

stream.on('data', chunk => {
  console.log(`got data on stream ${stream.id}: ${chunk.toString('utf8')}`)
})

The stream.id is handled by the STREAM library in the background. Server-initiated streams are assigned an even number. Client-initiated streams are assigned an odd number.

Stream expiry

The sender calls the getExpiry function for each packet or sets the expiry to the current date/time plus 30 seconds if getExpiry is not provided.

For example, if a packet is created on May 26, 2020 at 8:15:00 am, US Eastern Standard Time, then the expiry is set to 2020-05-26T13:30:300Z by default.

The intermediaries between the sender and receiver forward the packet as long as the packet is not about to expire in <1s.

Close a stream

Either endpoint can close a stream. Streams can close due to errors, expirations, and completions.

stream.end()

To log the end event:

stream.on('end', () => {
  console.log('stream closed')
})

To force a stream to close immediately without waiting for pending money and data to be sent, use the destroy method.

stream.destroy()

Close a connection

Either endpoint can close a connection. When a connection is closed, all streams are closed as well. Connections can close due to errors and completions.

connection.end()

A closed connection can’t be reused. A new connection must be created with a new shared secret and destination address.

To log the end event:

connection.on('end', () => {
  console.log('1: The connection closed normally')
})

To force a connection to close immediately without waiting for pending money and data to be sent, use the destroy method.

connection.destroy()

How a receiving wallet can use the STREAM server

This section describes how a wallet that only receives incoming payments (it doesn’t send outbound payments) could use the STREAM server and the security and development considerations related to that.

At a high level, the STREAM server:

  1. Binds the connection event.
  2. Might perform some non-async setup.

A common pitfall is for an asynchronous operation to happen between the connection event and binding the stream (in the next step), causing the server to miss when the stream opens.

  1. Binds the stream event.
  2. Might perform more non-async setup.
  3. Sets the setReceiveMax to allow incoming funds.
  4. Binds the money event.
  5. Dispatches something to the database or some kind of store whenever a money event triggers.
streamServer.on('connection', connection => {
  const metadata = JSON.parse(this.connectionTag.decode(connection.connectionTag))
  connection.on('stream', stream => {
    stream.setReceiveMax(Infinity)
    stream.on('money', amount => {
      console.log('received money', amount)
    })
  })
})

Configure the STREAM library as a sender for Web Monetization

This section provides some best practices for configuring the STREAM library as a sender for Web Monetization. In this scenario, bidirectional communication isn’t necessary. The sender repeatedly sends money to the receiver, but does not expect money or data in return.

This section assumes you have an ILP uplink, meaning you have a peer and an arrangement for settlement, or you connect to a system that performs settlements for you and acts as the source of truth.

All STREAM payments for a Web Monetized page visit must happen over a single stream.

Best practices

Expiration due to clock skew

When initializing the sender connection (createConnection), pass in a getExpiry function that returns a far-future expiry (FAR_FUTURE_EXPIRY). This ensures that packets won’t unexpectedly expire if the user’s OS clock is skewed. The next connector will reduce the expiry before passing it on, preventing packets from hanging forever.

Slippage

Senders should set slippage: 1.0.

Exchange rates are irrelevant to Web Monetization because there’s no target amount that the sender is trying to get to the receiver.

If you do need to consider exchange rates, you can verify that the connection’s rate is acceptable by calling connection.minimumAcceptableExchangeRate before sending any money.

Web Monetization lifecycle

  1. The sender opens a connection using create.Connection.
  2. The sender opens a stream using connection.createStream.
  3. The sender begins paying using stream.setSendMax(cumulativeAmountToSend).

The sender’s stream.on(‘outgoing_money’) event emits every time money is successfully delivered to the recipient.

  1. After some time passes, the sender can increase the setSendMax according to their application’s requirements.

If any step fails, or the connection dies, terminate whatever is left of the connection and plugin and start over. Keep track of how much was sent on the old connection with connection.totalSent or connection.totalDelivered.

Example

The following source file is part of the Coil Extension. Lines 303 - 493 can be used as a starting point for configuring the STREAM library as a sender: https://github.com/coilhq/web-monetization-projects/blob/master/packages/coil-extension/src/background/services/Stream.ts#L303-L493

How STREAM handles assets

An asset is comprised three parts:

  • Amount - The quantity of the asset expressed as an integer greater than or equal to zero
  • Asset code - A code, typically three characters, that identifies the amount's unit (e.g., USD, XRP, EUR, BTC)
  • Asset scale - The number of places past the decimal for the amount

For example:

amount: "7567",
assetCode: "USD",
assetScale: 2

We have an amount of 7567 that's denominated in US dollars. Since the asset scale is 2, it means that we're working with $75.67. If the asset scale was 3, then we'd have $7.567 (~$7.57), and so on.

In the next example, the amount is 1,000,000,000. Since the scale is 9, we're working with 1 XRP.

amount: "1000000000",
assetCode: "XRP",
assetScale: 9

The sender can send a currency that’s different from what the receiver wants to receive. As the Stream API communicates sender and receiver currency details, one or more intermediaries on the Interledger network convert the currency automatically. Connectors are one type of intermediary that can perform currency conversions.

Connectors

One example of an intermediary is a connector. A connector is a node between a sender and receiver that receives streams, performs currency conversions, then forwards the stream with the revised amount to the next node. Each connector has a direct peering relationship with other nodes. Each peering relationship has a single currency associated with it. The next node to receive a stream could be another connector, if additional conversions are needed, or the destination. Connectors themselves might generate revenue from spreads on currency conversion, through subscription fees, or other means.

Determine an exchange rate (optional)

A stream’s exchange rate can be determined experimentally by sending test packets to the destination. The destination responds with the amount they received in the stream. That rate can be used as a baseline for how much the sender can expect to be delivered.

STREAM receipts

STREAM receipts can be generated by the receiver to serve as proof of the total amount received on streams. In Web Monetization, for example, receipts allow you to verify payments before securely serving exclusive content.

Standardized receipt functionality at the STREAM layer enables the payment to be verified without needing to access the corresponding Interledger packets. This verification is typically handled by a third-party verifier.

At a high-level, STREAM receipts work as follows:

  1. A receipt secret and receipt nonce are communicated to the receiver by a verifier over some channel, such as SPSP. These parameters are used to generate connection details, which are required before the STREAM connection can be established.
  2. After a STREAM connection is established and the sender is sending, the receiver returns a STREAM receipt to the sender each time a packet is sent, indicating the total amount of money received in each stream.
  3. The receipts returned by the receiver are exposed by the STREAM client on stream.receipt. The receipts may be submitted directly or indirectly to the verifier.
  4. Each receipt on a stream contains a progressively higher amount, representing the total amount received on the stream. This means the latest receipt on a stream replaces any previous receipts on that stream.

For more information, see the STREAM Receipts RFC.

ilp-protocol-stream's People

Contributors

adrianhopebailie avatar dependabot[bot] avatar emschwartz avatar fossabot avatar greenkeeper[bot] avatar khoslaventures avatar kincaidoneil avatar melissahenderson avatar njlie avatar sentientwaffle avatar sublimator avatar traviscrist avatar wilsonianb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ilp-protocol-stream's Issues

Make it harder to identify STREAM packets on the wire

Ideas for obscuring which transport protocol is being used:

  • Pad packets to the maximum data size by default
  • Randomize the expiry time
  • Use a different connection tag for every packet
  • Use amount tranches (send only increments of 1, 10, 100, etc instead of whatever the connector says the maximum packet amount is)

It's an interesting question whether any of this is worth doing, because connectors can probably figure out which transport protocols (or even which implementations of transport protocols) are being used just by looking at the timing of packets and behavior in response to certain kinds of errors.

Update RFC

The spec should be updated to reflect the latest protocol

Inactivity Timeout

  • Add an inactivityTimeout option to the stream connection.
  • If money is being transferred the timeout is reset.
  • If no money is being transferred then the connection times out.
  • Leave timeout option as it.

Send money before determining the exchange rate?

If the user sets the sendMax amount before knowing the exchange rate, should the packets sent to determine the exchange rate carry money (and be fulfillable)? This would potentially reduce the latency for the first bit of money to get through, because it wouldn't be waiting for multiple round-trips with test packets before sending real money.

On the one hand, this opens things up for connectors to take a larger share of that money, because the sender will set a minimum destination amount of 0. On the other hand, though, connectors that want to do that could just take the same amount of the test packet to influence the minimum exchange rate the sender will set, so it might not make much of a difference.

If a user doesn't want to send money with a minimum acceptable amount of 0, they could just wait until the exchange rate is known before setting the sendMax.

Proper data handling

  • Only send data in Prepare packets
  • Resend data
  • Make sure data received out of order is properly handled
  • Resend control frames

An in-range update of @types/node is breaking the build 🚨

The dependency @types/node was updated from 10.11.0 to 10.11.1.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/node is a direct dependency of this project, and it is very likely causing it to break. If other packages depend on yours, this update is probably also breaking those in turn.

Status Details
  • ci/circleci: build: Your tests failed on CircleCI (Details).
  • ci/circleci: build_node_v8: Your tests failed on CircleCI (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Multiple packets in flight

Currently, this module waits for the response to each Prepare packet before sending the next one. If the path can support sending multiple packets at the same time, the module should fire off as many as possible so that the money and data is sent as quickly as possible.

To do this, STREAM would need to keep track of:

  • Total amount of money in flight and the path limit
  • Total number of bytes in flight and the path limit
  • Total number of packets in flight (and the path limit, if there is one)

Update docs

  • Auto-publish typedoc
  • Remove todo list from readme
  • Include how to use in readme
  • Link to typedoc from readme
  • Include badges from circleci, npm, codecov, standard

Connection Timeout

  • Add connection timeout which kills the connection if nothing is happening for a long time. Sorta like the http timeout.

An in-range update of @types/webpack is breaking the build 🚨


☝️ Important announcement: Greenkeeper will be saying goodbye 👋 and passing the torch to Snyk on June 3rd, 2020! Find out how to migrate to Snyk and more at greenkeeper.io


The devDependency @types/webpack was updated from 4.41.10 to 4.41.11.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/webpack is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ci/circleci: build_node_v10: Your tests passed on CircleCI! (Details).
  • ci/circleci: build_node_v12: Your tests failed on CircleCI (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Stream Specific Errors

  • Add Error Codes for specific stream errors
  • Pin point who is at fault by matching ilp address by prefixes

One-sided money connections

STREAM should work even if only one side is capable of sending money. Right now the connection will error out, however, if it is unable to send the first test packet. We should make it work under these conditions by going into some kind of "receive-only" mode.

Connection Timeout

  • Add setInactivityTimeout function to the stream connection.
  • If enabled then the connection times out after that set period if no data is being transferred on the connection.

What should ending a stream do if it still has money to send?

If you call end on a Node TCP socket, it will:

  1. Keep sending data until all the data written to it has been sent
  2. Emit 'finish' when it has finished sending all of the data
  3. Emit 'end' when the other side has indicated it is finished sending data
  4. Emit 'close' when the underlying socket is closed

If the other side isn't reading data from the socket and the reading side's buffer is full, the sending side will stay open until the consumer is finished. (See this gist if you want to play around with different possibilities)

The question is what the behavior should be for a DataAndMoneyStream if you call end or destroy when it's still trying to send money? Should it close the stream immediately, wait until the money is sent, or close the stream only if the other side is currently blocking it from sending more money?

Probing/exchange rates are not a STREAM concern

Proposal: Allow logic enforcing minimum exchange rates to be completely disabled, including:

  1. Sending test packets before the stream is established
  2. Requiring packets to meet the minimum exchange rate that STREAM calculates

  1. Supporting receive-only mode.
    • Case 1: credit with connector is 0, which currently causes all test packets to fail and stream to be closed, even if the endpoint won't be sending money.
    • Case 2: there exists credit with the connector, but the probing temporarily exhausts it, which is unnecessary and can cause disruptions for other applications.
  2. Insecure. Exchange rates from probing can trivially be gamed unless there was a TON of liquidity and many connectors, so for the foreseeable future, most clients necessitate their own exchange rate oracle to ensure the rate is reasonable.
    • In practice, the current behavior only really ensures the exchange rate stays consistent/within the slippage margin throughout the stream, which isn't desirable since it should respond to exchange rate fluctuations. (By contrast, the price oracle approach can respond to fluctuations, if necessary).
    • Instead, a callback function could be passed to STREAM clients & servers, that given a source amount and a destination amount, could return true/false if that particular packet should be fulfilled. (If it returned false, STREAM would reject that packet). This would allow consumers of the module to integrate with their preferred price oracle or use their own method to enforce a minimum exchange rate on a per-packet basis.
  3. Longer connection establishment. Probing wastes time while establishing the connection (#44), when alternatively it could be used to start sending money immediately, since the test packets can trivially be identified and gamed.
  4. No support for large asset scale differences. Probing with no knowledge of the relative exchange rate can introduce problems with vastly different exchange rate scales. (And requires sending massive test packets, which as noted earlier, could temporarily exhaust bandwidth that other applications are using, if they're forwarded).

To determine the initial packet size to send, STREAM could send a single very large packet (e.g., maybe the maximum uint64 an ILP packet allows the total amount remaining to send). F08s (which connectors should be configured to trigger before T04s) should quickly reduce it to a reasonable amount. (Alternatively, STREAM could be more aware of settlement/how much credit exists with the upstream peer, but I'm guessing that'd be more contentious).

An in-range update of @types/chai-as-promised is breaking the build 🚨

The devDependency @types/chai-as-promised was updated from 7.1.1 to 7.1.2.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/chai-as-promised is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ci/circleci: build: Your tests failed on CircleCI (Details).
  • ci/circleci: build_node_v8: Your tests failed on CircleCI (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Server connection can't send money on 'connection' event

Sending money from the server doesn't work well right now. The connection event is emitted before connection.handlePrepare is called, so the connection hasn't processed the request frames (that include the other party's ILP address) yet. As a result, if you call setSendMax synchronously, it will stop because it doesn't know the destination address.

I think we could address the sending money issue by making sure to restart the send loop once the connect event is emitted. However, it's still strange that the connection.destinationAccount is undefined on the server side, so it feels like we should solve this in a better way.

I'm not sure about the best way to do this right now so open to suggestions.

Unable to establish connection, no packets meeting the minimum exchange precision of 3 digits made it through the path.

When I split example.js into server and client I get the error:
Error: Error connecting: Unable to establish connection, no packets meeting the minimum exchange precision of 3 digits made it through the path.

I run the server the server and wait for connections and from a different client.js file I put the part to connect and stream. The error is generated here:

const clientConn = await IlpStream.createConnection({
    plugin: clientPlugin,
    destinationAccount,
    sharedSecret
  })

Reduce packet amount on temporary errors

The proper behavior for determining the packet amount is to have stream figure out how much money it can have in flight at a given time. This is a little complicated to implement so for the time being we should just make the default behavior for temporary errors be to reduce the packet amount (especially on T04 errors).

Related to interledgerjs/ilp-plugin-xrp-asym-server#39

Proper exchange rate handling

Right now the test section titled "Exchange Rate Handling" is empty and it would be good to add some tests to make sure it is behaving properly.

The minimum acceptable rate is determined when the stream connects by using a test (unfulfillable) packet (see here). When each subsequent packet is sent, the sender indicates the minimum amount the receiver should accept (which they check here) in the STREAM packet.

A couple of things that would be worth testing:

  • Packets should be rejected if the exchange rate gets worse than the minimum acceptable amount
  • The user should be able to configure the amount of slippage they'll accept (used here)
  • The connection object should provide methods to query both the current exchange rate (total received / total sent) as well as the minimum acceptable rate (this needs to be implemented and tested)

Exponential Backoff hits 32-bit signed int boundary

If there are connection problems and temporary errors, the exponential backoff continues doubling (here:

this.retryDelay = this.retryDelay * 2
) until it hits the 32-bit signed int boundary and explodes with this error:

TimeoutOverflowWarning: 1.915619426082361e+55 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.

And this continues until the 64-bit double is overflowed and the connection logic likely stops.

In this situation we should either throw an error or simply cap the timeout length

Determining the exchange rate precision

As of right now we are sending out packet amounts of 1, 1000, 1000000, and 1000000000 in an attempt to determine an exchange rate with an amount of precision as quickly as possible.

At the high level it seems that if we send the following amounts that we have a precision to at least 3 digits and a determined exchange rate:

Amount Sent Prepare Amount Exchange Rate
1000 1000 1
10000 1100 .11

However this does not take into account what is actually happening at the connector level. There could be multiple connectors which are changing the rate but since we only know the amount sent and the prepare amount that comes back we cannot see that they have a very different exchange rate so the 1 and .1 are not valid.

Suppose we have the connectors, where their exchange rate is represented in the ():

Conn A <---> (.001) Conn B <---> (1.6) Conn C <---> (1000) Conn D
1000 ----------------> 1 --------------> 1 ---------------> 1000 == 1 

In reality the exchange should have been 1000 * .001 * 1.6 * 1000 = 1600 with a rate of 1.6 but on this path the rate calculated was 1. A similar issue could occur with larger amounts as well.

Conn A <---> (.001) Conn B <---> (1.14) Conn C <---> (1000) Conn D
10000 ----------------> 10 --------------> 11 -------------> 1100 == .11

This should have been 10000 * .001 * 1.14 * 1000 = 11400 with a rate of 1.14 but was calculated as 1.11.

The issue is that connector C is using a 1.6/1.14 exchange rate but connector B is using a very large exchange rate which effectively takes the value down to 1 and connector D is ramping that back up.

How can stream determine without having knowledge about the Conn C exchange rate that in these scenarios the calculated exchange rate is incorrect and needs further clarification?

Consistent Error Handling

On the connection and the stream we should match how node works with write() and only throw errors if there is not an error emitter present. Right now we throw errors regardless of the emitter being present and we do not emit errors if one is present.

if(this._events.error){
    this.safeEmit('error', streamClosedError)
}

Could be used to check if the error listener is present.

verifyReceipt function signature

https://github.com/coilhq/receipt-verifier/blob/eeccf81420c10dda77c50721eda67a1c9645025e/src/services/Balances.ts#L37-L38

      let receipt: Receipt
      try {
        const receiptBytes = Buffer.from(body.toString(), 'base64')
        receipt = decodeReceipt(receiptBytes)
        verifyReceipt(receiptBytes, generateReceiptSecret(this.config.receiptSeed, receipt.nonce))
      } catch (error) {
        ctx.throw(400, error.message)
      }

verifyReceipt takes a secret to verify the hmac, but in practice that secret is generated from the receipt nonce

as can be seen above, that can lead to inefficient double decodes

could the verifyReceipt function optionally take a function with the decoded receipt as an argument and return the secret to be used ?

An in-range update of @types/sinon is breaking the build 🚨

The devDependency @types/sinon was updated from 5.0.5 to 5.0.6.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

@types/sinon is a devDependency of this project. It might not break your production code or affect downstream projects, but probably breaks your build or test tools, which may prevent deploying or publishing.

Status Details
  • ci/circleci: build: Your tests failed on CircleCI (Details).
  • ci/circleci: build_node_v8: Your tests failed on CircleCI (Details).

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Don't serialize JSON for logging unless debug is on

All the debug statements in the stream library use string interpolation with backticks. This means that the objects are serialized for logging before they're passed to debug, and may not be printed if debug is turned off. We should make sure that these unnecessary string operations don't happen unless debug is actually enabled.

Send-only mode

In some use cases on top of STREAM, bidirectional communication isn't necessary. In Web Monetization, for instance, the sender repeatedly sends money to the receiver but does not expect data or money in return.

If we were able to turn on a feature in the STREAM client that stops the sender from sending their ILP address to the other side and also enable a feature that would allow the server to function in a half-open mode like this, we could simplify the infrastructure required for WM sending.

Instead of using a bidirectional communication like BTP, we could use HTTP/HTTP2, simplifying WM sending infrastructure and the WM sending code.

CC @kincaidoneil @wilsonianb @sentientwaffle if one of you is interested in looking into this, feel free to assign yourself on this issue.

@kincaidoneil I noticed you worked on the code for receive-only mode on the server, are there any considerations with this feature that I missed?

Default flow control values

Like QUIC, STREAM is going to have connection- and stream-level flow control (so both sides will be able to control both the amount of data they're getting for each stream and for the connection as a whole).

  1. What should the default buffer size be for each stream? Node streams default to 16kb. However, STREAM packets can carry up to ~32kb so using the Node default would make the receiver reject a full STREAM packet.
  2. What should the default maximum number of open streams be? 10 per side?
  3. What should the default buffer size be for an entire connection (the total of the buffered data in all of the open streams)?

Connection-level inactivity timeout

  • Close the connection if nothing has happened for a while
  • Time out packets if they keep getting rejected for a long time. ex. StreamCloseFrame

An in-range update of oer-utils is breaking the build 🚨

The dependency oer-utils was updated from 3.0.1 to 3.1.0.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

oer-utils is a direct dependency of this project, and it is very likely causing it to break. If other packages depend on yours, this update is probably also breaking those in turn.

Status Details
  • ci/circleci: build_node_v8: Your tests failed on CircleCI (Details).
  • ci/circleci: build: Your tests failed on CircleCI (Details).

Commits

The new version differs by 5 commits.

  • 4a0fd30 3.1.0
  • e0813c4 fix: reject variable integers of length zero
  • 3cdb8ab chore: re-enable code coverage reporting
  • c072342 chore: update dependencies
  • f3dc629 feat: allow using JS numbers instead of BigNumbers

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Clean up closed streams

  • Reject packets with money or data frames for closed streams
  • Delete stream record to avoid memory leak
  • Make sure stream numbers for closed streams aren't reused
  • Delete everything when the connection is closed
  • Make sure that end allows all the data & money to be sent from side that called end
  • Make sure that destroy closed down the connection immediately from both ends and does not allow more data to be sent or received
  • Calling destroy on a stream does not close the connection
  • the server doesn’t stop accepting packets for a given connection when the connection closes. On the close event it should remove it from the connection array and also keep track of connections that were previously open to avoid them being opened again

Should there be ACKs for data or should data only be sent in Prepare packets?

To figure out if the other side got a data frame, we can either:

  • Send data frames only in Prepare packets and use the Fulfill/Reject as an acknowledgement (if the data is authenticated and we know it came from the receiver)
    • If we do this, should we assume that a Reject is a NACK for the data, or should we assume they've gotten and processed the data even if the packet is rejected?
  • Have ACK frames for data so that data can be sent in any type of ILP packet

Note that QUIC uses ACK frames to acknowledge receipt of whole packets rather than specific frames or blocks of data.

Stateless STREAM Server

If ilp-protocol-stream could keep connection state in a data store like redis, we could run sharded STREAM servers. This would make it operationally simpler to scale STREAM servers, allowing us to do away with stateful sets and the two-layered architecture for scaling STREAM servers (a statefulset of connectors + STREAM servers behind a deployment of connectors).

An in-range update of ilp-logger is breaking the build 🚨

The dependency ilp-logger was updated from 1.0.2 to 1.1.0.

🚨 View failing branch.

This version is covered by your current version range and after updating it in your project the build failed.

ilp-logger is a direct dependency of this project, and it is very likely causing it to break. If other packages depend on yours, this update is probably also breaking those in turn.

Status Details
  • ci/circleci: build: Your tests failed on CircleCI (Details).
  • ci/circleci: build_node_v8: Your tests failed on CircleCI (Details).

Commits

The new version differs by 5 commits.

  • 5253753 1.1.0
  • c2d9bb0 feat: upgrade to TypeScript (#8)
  • 65d0941 Merge pull request #5 from interledgerjs/greenkeeper/initial
  • fdd0711 chore: add greenkeeper-lockfile to CI
  • d27e32f docs(readme): add Greenkeeper badge

See the full diff

FAQ and help

There is a collection of frequently asked questions. If those don’t help, you can always ask the humans behind Greenkeeper.


Your Greenkeeper Bot 🌴

Unique id per money event

A question was raised by an implementer

Is there any way native to the Stream protocol or ILP to allow us to individually identify a micropayment?

This can be achieved by raising a unique id within each money event. @justmoon pointed out this id could be the SHA256 hash of the ILP packet. It will be a breaking change on the money event though as currently the value returned is an amount value as a string. The newer structure would be along the lines of

{
 "id": "a6f7b874ea69329372ad75353314d7bcacd8c0be365023dab195bcac015d6009",
 "amount": "123"
}

Another question is should we amend the spec to mandate this type of functionality for STREAM implementations?

CC @adrianhopebailie @sharafian @sentientwaffle @wilsonianb @justmoon @kincaidoneil

Support paths with large exchange rates (e.g. big differences in currency scales)

Right now STREAM sends a test packet of 1000 source units to determine the exchange rate. If the amount that arrives is 0, the sender will assume that the path exchange rate is 0 and won't try to send anything through it. We should probably support paths in which the sender and receiver have very different scales.

I think the way to handle this would be:

  1. Start with a test packet of 1000
  2. If that gets to the receiver, increase the test packet size
  3. If the sender gets an F08: Amount Too Large error before any money gets to the receiver, then the sender can conclude that no money will be able to be sent through the path

Dedup dependencies

There are a lot of different versions of bignumber.js and oer-utils floating around our modules. It would be good to clean those up and get them all using the same versions so we don't need to install all of them.

ilp-protocol-stream git:es-docs ❯ npm ls --production
[email protected] .../ilp-protocol-stream
├── @types/[email protected]
├── [email protected]
├─┬ [email protected]
│ └── [email protected]
├─┬ [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ ├── [email protected]
│ └── [email protected]
├─┬ [email protected]
│ └─┬ [email protected]
│   ├── [email protected]
│   ├─┬ [email protected]
│   │ ├── [email protected] deduped
│   │ ├── [email protected]
│   │ ├── [email protected]
│   │ ├── [email protected]
│   │ └── [email protected]
│   ├── [email protected] deduped
│   ├── [email protected]
│   └─┬ [email protected]
│     ├── [email protected]
│     ├── [email protected]
│     └── [email protected]
├─┬ [email protected]
│ ├── [email protected] deduped
│ └── [email protected]
├─┬ [email protected]
│ └── [email protected] deduped
└─┬ [email protected]
  └── [email protected]

receiveTotal resolves before fulfillment is sent

I was testing a pull payment script that had:

await stream.receiveTotal(amount)
//exit

This resulted in receiveTotal resolving but the outgoing_money event never happening on the sender's side. According to the logs, the fulfillment (nor a rejection) was never sent by the receiver.
I think this is due to the fact that the money event is emitted (inside _addToIncoming) before doing the fulfillment, leaving a window of opportunity to fail to fulfill the payment despite having indicated that the amount was received.
https://github.com/interledgerjs/ilp-protocol-stream/blob/master/src/connection.ts#L556-L595

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.