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

Proposal: Duplex PSK #388

Closed
sharafian opened this issue Feb 11, 2018 · 18 comments
Closed

Proposal: Duplex PSK #388

sharafian opened this issue Feb 11, 2018 · 18 comments
Labels

Comments

@sharafian
Copy link

This is something that @emschwartz and @justmoon have thought about extensively, but I was pretty skeptical until the last conversation that we had about it.

The current version of PSK can be treated as a unidirectional stream. You can send data and money one way (and there's a method of acknowledging these), but the receiver cannot actively tell the sender what to do. So:

The sender can:

  • Send money w/ attached data

The receiver can:

  • Respond to money w/ attached data

If the receiver wants the sender to stop sending money, they need to communicate that in the response of the sender's packet. The sender needs to send continuous "polling" packets in order for the receiver to tell them when to send again.

This is workable in some use cases, but a little clunky. Furthermore, it completely fails to solve some cases like refunds, where the sender actually wants the receiver to start sending money back.

We can solve this issue by making PSK a duplex stream. The sender and receiver have each others' ILP addresses in this model, so packets can be sent actively both ways.

We can get rid of polling now, and say that instead of periodically asking when to send money, the sender and receiver (who are now identical in their capabilities) can actively request money by sending a packet. It goes from the sender being able to "push", to both parties being able to "push" and "pull." So:

Both parties can:

  • Send money w/ attached data
  • Request money w/ attached data

You'll notice that this opens up a couple of different flows. One flow is that someone is requesting money (and the other party doesn't know how much to send). The other flow is that someone wants to send an amount of money (and the other party doesn't know how much they're going to receive).

We can reason about this by saying the PSK stream's balance starts at zero, and each party has a minimum and maximum balance. The rules are as follows:

  • If you send money, your balance goes down.
  • If you receive money, your balance goes up.
  • If the current balance is below your peer's minimum, you'll send money.
  • If the current balance is above your own maximum, you'll send money.
  • Take care of your own min/max before your peer's, i.e. Don't send money if it puts you below your minimum.
  • Either party can change their minimum/maximum at any time by sending a special PSK packet to the other party.

That's a little hard to grasp in all at once, so I'll give a couple of examples:

I want to send 1,000,000 units

I set my minimum to -1,000,000 and my maximum to -1,000,000. The other party's maximum is Infinity (I can pay them as much as I like) and their minimum is 0 (They're not going to pay me).

The balance starts at zero, so I'm above my maximum. I begin sending money. Once the other party gets a payment they increase their minimum by the payment amount, because they're not willing to refund it. Eventually I send 1,000,000 units.

I am paying an invoice for 1,000,000 units

I set my minimum -1,000,000, because that's how much I'm willing to pay. I set my maximum to 0 (They're not going to pay me). The other party sets their minimum to 1,000,000 and their maximum to 1,000,000.

The balance starts at zero so I'm inside my min/max. The other party is below their minimum so I begin sending money. Eventually I send the 1,000,000 units that they request.

I am redeeming a voucher for 1,000,000 units

This is an entirely new scenario, and it's the one that I think is really exciting. You can actually create a PSK connection that is willing to give out money when asked, essentially creating a voucher that you can give to someone like cash. They can pull the money into their own account, or give the voucher to anyone else and let them pull the money. Here's how it works:

I set my maximum to 1,000,000 and my minimum to 1,000,000. The other party (the voucher) sets their minimum to -1,000,000 and their maximum to 0.

The balance starts at zero so I'm below my minimum. The other party (the voucher) begins sending money to me, because doing so doesn't put them below their minimum. Eventually I receive the 1,000,000 units for which the voucher is redeemable.


I imagine that with this new version of PSK, the corresponding version of SPSP would be the protocol which tells each side what they're supposed to set their min/max to.

An SPSP response containing no balance information would mean that it's the SPSP client's decision to send money (the first example, where the receiver has a min/max of 0/infinity)

An SPSP response containing the following would encode an invoice for 1,000,000 units (as in the second example):

balance: {
  current: 0,
  maximum: 1,000,000
}

And an SPSP response containing the following would encode the third scenario, where it represents a voucher that can be pulled from:

balance: {
  current: 0,
  minimum: -1,000,000
}

Right now, with simplex PSK, you can give someone a payment pointer in order to let them send you money. With duplex PSK, you can give someone a payment pointer to send them money. You don't need to know their ILP identity at all, so it's like giving them a gift card.

There are a lot more scenarios that you can envision with this. I didn't cover the refund scenario, but it can be accomplished by having the sender adjust their minimum back to zero. You can also imagine scenarios where you may sometimes pull and sometimes push, like a payment pointer that points to a shared pool of money.

@justmoon
Copy link
Member

justmoon commented Feb 11, 2018

Oops, I've already been working on a write-up on this idea. Here it goes. Same idea, but described slightly differently:

This idea is based on PSKv3 by @emschwartz and conversations with @sharafian.

Basic idea

Conceptually separate server/client and sender/receiver.

Abstract

Currently, PSK assumes that the client is always the sender and the server is always the receiver. It also assumes that the sender is the one deciding the amount to be transmitted. In practice, neither of these assumptions always hold.

Model

There are a client and a server. The client can open connections to the server. Each participant (client and server) is running an application which defines per connection:

  • Minimum required amount: This participant will never let their balance for this connection drop below this amount.
  • Maximum desired amount: The participant will reject any packets which would put their balance (significantly) above this amount.

Both sides also track their view of the balance for the current connection. For each connection, the balance starts at zero for both sides. Note that the balance is always from the perspective of the participant, i.e. if the client sends money to the server, the client's balance will decrease and the server's balance will increase. For either side, the balance and minimum/maximum are denominated in that side's local units.

When the balance is above a participant's maximum balance, the participant will start sending money. When the balance is below a participant's minimum balance, the participant will request the other participant to start sending money. When receiving such a request for money, the other participant will comply up until they reach their minimum balance.

Example Use Cases

SPSP Payment (without refund)

A client "Alice" wants to donate $1 to server "Bob". Both sides use nano-dollar accounts.

Alice sets her minimum and maximum to "-1000000000" and connects. When receiving the connection, Bob sets his minimum to "0" and his maximum to "Infinity". Bob does not offer refunds, so he increases his minimum along with each packet that arrives.

SPSP Payment (with refund)

A client "Alice" wants to pay an invoice for $1 on server "Bob". Both sides use nano-dollar accounts.

Alice sets her minimum and maximum to "-1000000000" and connects. When receiving the connection, Bob sets his minimum to "0" and his maximum to "1000000000". Bob offers refunds, so he does not increase his minimum and keeps it at 0. After sending some money, Alice changes her mind and wants a refund. She sets her minimum and maximum to zero, so the money flows back to her.

Cog (streaming smart contract)

Cogs are contracts where the client provides funds on demand as required by the contract. The idea is that the amount required to pay for a certain API may not always be known ahead of time.

A client "Alice" wants to convert a high-resolution MP4 into a set of lower-resolution, more optimized video files. "Bob" offers a paid API to do just that. Alice connects to Bob setting a maximum of "0" and a minimum of "-100000000" (she wants to pay at most 10¢). Bob, upon receiving the connection, starts his minimum and maximum at "0". As he starts to process the video he progressively increases his minimum and maximum to match the cost of processing. He may himself call other streaming paid APIs and pass on their cost.

Cog (HTTP-ILP variant)

In the previous example, all communication happened over ILP. Perhaps we want to continue using HTTP-ILP as a more familiar options. Previously, HTTP-ILP worked thusly: The client sends a "token" to the server. The server maintains a balance for each token. If the funds on the token are not sufficient for the current request, the server responds with an error and returns an ILP address and shared secret for refilling the token. We can simplify this example a great deal using pull payments.

A user "Alice" wants to convert an image from webp to jpeg. Alice makes an HTTP-ILP call to "https://bob-convert.com/api/webp-to-jpeg". Instead of including a token, she includes an ILP address and shared secret she generated. Bob receives the HTTP request, sees the payment information and connects to Alice, setting his minimum and maximum to the cost of the request. Alice receives Bob's connection and sets her maximum to zero and her minimum to zero minus the maximum she is willing to pay for the request.

This way, we avoid the 402 error being a normal part of the flow. This is similar to @michielbdejong's idea from #307.

Gift code

The neat thing about a duplex transport layer protocol is that the payee may be the client.

"Alice" is very happy with Interledger and wants to get her friend "Marion" to try it out. She wants to give Marion $1 to play around with. Alice uses the "create gift code" option in her wallet provider, which creates the payment pointer $gift.me/OLMCH-NKCI3-OKWCA-SSMAQ. Marion opens a new wallet and enters the payment pointer. Marion's wallet does an SPSP query on this pointer and receives an SPSP result with an ILP address and shared secret and metadata indicating that this payment pointer is a gift code. It creates a connection to this endpoint, setting the minimum to "0" and the maximum to "Infinity". Alice's wallet receives the connection and set its minimum and maximum to "-1000000000". The money is then being transferred into Marion's wallet. Alice's wallet lowers its maximum along with the current balance because it does not want the gift code to be refilled.

Deposit/withdraw

A duplex transport protocol can also allow sending to and receiving from a single payment pointer. This can be used to manage a balance at another service for instance.

"Alice" wants to trade digital assets at a digital assets exchange. She logs in to the exchange and receives a payment pointer for account "$exchange.example/12345" and a corresponding secret "dOczXynvVwoDmxEnPZUKVw". The exchange advises her never to share the secret with anyone since it would give them access to her money. Alice logs in to her normal ILP wallet and enters both credentials. The wallet resolves the payment pointer via SPSP and realizes that it is a deposit/withdraw pointer. So it stores it permanently and Alice can now manage her exchange balance from within her wallet. When depositing $1, Alice's wallet creates a connection to the exchange, setting its minimum and maximum to "-1000000000". The exchange receives the connection, setting its maximum to "Infinity" (or whatever Alice's remaining deposit limit is) and its minimum to "-341000000000" (Zero minus Alice's current balance at the exchange.)

When Alice wants to withdraw $5, her wallet once again connects, but this time sets its maximum and minimum to "5000000000".

Pull payments

Previously, the way we implemented Codius, Unhash and other "rentables" on Interledger is by putting the onus on the uploader to take care of refilling the hosting on time. This leaves a bit to be desired, we don't want all of our websites to go down simply because the script we set up to renew it failed somehow. Even the fact that we have to write a script to renew our hosting is less than ideal.

"Alice" wants to upload a Codius contract. She first connects to her wallet to create a new pull payment authorized payment pointer with a budget of $10/month. She then makes an API call to the Codius host to upload the container and cites her payment pointer as the payment method. The Codius host will once a day connect to the payment pointer, setting its minimum and maximum to $10/30 = "333300000", Alice's wallet receives the connection and sets its maximum to zero and its minimum based on the remaining available funds for this pull payment authorization. As Alice's wallet pays out it lowers its maximum along with the balance, because it does not want to receive money on this connection.

Protocol Outline

The basic protocol is that each side updates the other when their respective minimum, maximum or balance changes. Based on their own min, max and balance as well as their knowledge about the other side's, each participant decides whether they need to send money. When sending money, things work pretty much the same as PSKv3, the sender ramps up the amount until reaching the limit for the current path. Encryption also works the same as PSKv3.

@emschwartz
Copy link
Member

This is what I've been working on for the past week or two and some of the questions that I've been thinking about have been:

  1. Should this more advanced streaming payments protocol also carry data? It would get more complicated, but it seems like we'd want to send data along the same channel if we already have a bidirectional connection open.
  2. Should this be built on top of PSK2 or would it replace it? If we implement this, will we ever want to use PSK2 directly without it?
  3. Do we need any fields in the protocol header aside from how much more each side wants to receive to support proper backpressure or the development of more advanced TCP-style congestion control algorithms in the future?
  4. Are there other lessons we should learn from the decades of TCP development that we should apply now to avoid reinventing the same problems people are still dealing with on the internet today?
  5. Should this protocol support multi-homing or changing addresses?
  6. If we have a streaming payments server, do we still want the setup to be focused on calling a function like getAddressAndSecret to create a client-specific address and secret, or would we want to use a single address with public key crypto and TLS-over-ILP to derive the shared secret instead? The current Diffie-Hellman algorithms are likely to be broken by quantum computers, so we definitely want to make those easy to swap out if we do use them.
  7. What happens if the minimum and maximum amounts on both sides are incompatible? Should the protocol try to discover that as quickly as possible to prevent uselessly sending money?
  8. How are we going to deal with in-protocol DoS attacks on these payment servers? They'll need to keep some state (in memory) for each socket open, and it seems like you could pretty easily overwhelm one of them by opening a number of connections.

And, maybe the most important question: how important is it to figure all of this out and implement this now? Is it necessary to get ILP adopted, or is it a distraction from more important MVP-level goals?

@adrianhopebailie
Copy link
Collaborator

@emschwartz asked (some questions will be answered in time as we experiment, I think it's too early to say):

Should this more advanced streaming payments protocol also carry data? It would get more complicated, but it seems like we'd want to send data along the same channel if we already have a bidirectional connection open.

As you know I think this is a bad idea. A system optimized for routing ILP packets is not optimized for routing data. i.e. The former is high CPU, the latter introduces a need for large buffers and hence memory and storage.

Not impossible but I don't think it is a priority.

Should this be built on top of PSK2 or would it replace it? If we implement this, will we ever want to use PSK2 directly without it?

No. Let's harden PSK2 for it's current use case and incubate this idea separately.

If we have a streaming payments server, do we still want the setup to be focused on calling a function like getAddressAndSecret to create a client-specific address and secret, or would we want to use a single address with public key crypto and TLS-over-ILP to derive the shared secret instead?

These are orthogonal. I think what you are suggesting is that we no longer assume there is a secure connection between client and server outside of the ILP route. For many reasons I think that is a bad idea but primarily because ILP is a value transport protocol and shouldn't be mixed in with the business meta-data exchange of a commercial transaction.

The beauty of ILP is that we have separated these two and that is why it is so powerful. Two entities negotiate a payment via ANY channel (i.e. verbal, bluetooth, SPSP over the Internet, embedded payment request in an eInvoice) and then make the payment via ILP.

And, maybe the most important question: how important is it to figure all of this out and implement this now? Is it necessary to get ILP adopted, or is it a distraction from more important MVP-level goals?

Given that I haven't seen any proposals about how to handle connectors that charge fees in both directions I'd say there is a lot of complexity here that will certainly distract us from solving the most simple use cases first.

i.e. Alice sends packets of 1000 via Chloe to Bob and 999 consistently arrives since Chloe takes a 1 spread. When Alice requests a refund of her payment which has not completed entirely Bob starts sending packets of 1000 back via Chloe until he gets back to a balance of 0. However since Chloe is taking her fee on the return payments too Alice ends up with less than she had before.

Further, Chloe is highly incentivized to start charging higher fees as the payment progresses because this is likely to trigger Alice to abandon the payment and request a refund.

Even if the return route (Bob to Alice) goes via a different connector (Clint) they may still be subject to fees. If Clint and Chloe collude then they can quickly steal money in a way that simply looks like the network has poor liquidity or volatile rates.

TL;DR: This is a very interesting transport layer solution but one that I think has a LOT of complexity we should not be spending too much time trying to solve right now.

@emschwartz
Copy link
Member

A system optimized for routing ILP packets is not optimized for routing data. i.e. The former is high CPU, the latter introduces a need for large buffers and hence memory and storage.

I don't think that's true if you stream the data from the incoming connection to the outgoing one without buffering the whole packet in memory.

I think the stronger argument against this is more along the lines of latency or how closely your ILP connection lines up with the shortest physical path. Previously I would have thought this wouldn't line up at all, but I've started thinking about it a little differently more recently. If you always want your payments to be super fast, you could open a payment channel to a "local" connector wherever you are and then you could have a situation where your ILSP and ISP are the same party. I've sometimes described that as carrier billing on steroids.

No. Let's harden PSK2 for it's current use case and incubate this idea separately.

This can be built on PSK2 without changing its design, and it would make sense to do that if this more advanced protocol will want to use the same type of encryption and condition generation.

@adrianhopebailie
Copy link
Collaborator

I don't think that's true if you stream the data from the incoming connection to the outgoing one without buffering the whole packet in memory.

Except that in doing that you need to perform a number of business functions like routing, calculate exchange rates etc.

The architecture you propose would have to block while these things are being done at each connector making it impossible for this link to be as fast as a dedicated data link.

@sharafian
Copy link
Author

sharafian commented Feb 12, 2018

Should this more advanced streaming payments protocol also carry data? It would get more complicated, but it seems like we'd want to send data along the same channel if we already have a bidirectional connection open.

One thing that makes me think this doesn't belong in this protocol is that the volume of data doesn't really correlate to the volume of payment.

For example, a small amount of data could be sent to help the receiver find a large payment in their database, in which case the number of chunks required to send the data is very unlikely to line up with the number of chunks required to send the money. All the money could be sent and then the data can be carried on a zero-amount packet.

In the case when a large amount of data is sent, for example in unhash, the payment may not be large. If you're sending a gigabyte and paying 20 cents, there will be far more data chunks than money chunks.

So you might want to use the same channel, but I don't think it makes sense to make the protocol worry about sending both at the same time

@emschwartz
Copy link
Member

Implemented a first version of this in: https://github.com/emschwartz/ilp-protocol-paystream

@adrianhopebailie
Copy link
Collaborator

@emschwartz While you're doing some experiments I would really like to see a test where there is a connector that has a takes a fee/spread in both directions between two nodes doing duplex PSK.

I suspect it would be pretty easy for that connector to manipulate its rates to quickly drain both side's accounts as they pay each other back and forth to try and get their balances correct.

@sharafian
Copy link
Author

In any case, the sender must always set the maximum price they're willing to pay. If they theoretically reach that price before the balance stabilizes, they could, in the current implementation, lose that entire amount to fees on the refund path.

If the sender included source amount in their payment, we could theoretically add a tolerance for fees as a parameter. That way the sender determines the expected spread on the way to the receiver, and can tell on the way back whether the fee is significantly worse (by comparing the amount they receive to the amount the receiver sent). If it has gotten significantly worse then the sender would reject payments until the rate returns to a normal rate.

The hope would be that if some connector has vastly inflated their fees, their peers in the network will swiftly remove them. The receiver keeps trying to refund the sender, the sender will continue rejecting their payments while the spread is much worse than the sender saw initially. Once the network recovers from the malicious connector and the spread drops, the connector will accept their refund.

Overall I agree though, If we don't have a robust network where participants will quickly depeer malicious nodes, then someone could potentially hold payments for ransom or make them hang. Even without anyone malicious, this is a risk in a small network because a node could just go down and leave a lot of payments half-completed. When the network becomes better connected and more competitive, though, the depeering should happen fast enough that it's not a viable attack against chunked payments.

In the network's early state the safest thing to do is to use the network for micropayments or streaming payments, where it doesn't matter if you're abruptly cut off or the rate becomes unacceptable.

@adrianhopebailie
Copy link
Collaborator

My concern here is that we no longer have the same incentives that make ILP robust.

To me duplex PSK only works if its only used by a minority of the network so it's hard for a connector to spot it being used and exploit that.

Connectors that can identify duplex psk and exploit that will be very hard to isolate. They could do it very sporadically and still make a lot of money by running a lot of connectors that very occasionally milk a duplex psk connection (especially where they know they are many hops from the sender or receiver).

@emschwartz
Copy link
Member

@adrianhopebailie are you worried specifically about the refund portion of duplex payment streams or just chunked payments in general? Just so you know, refunds will be disabled by default and senders can specify the minimum amount the receiver will accept so they can make payments fail if the exchange rate moves at all or too much.

@adrianhopebailie
Copy link
Collaborator

adrianhopebailie commented Feb 16, 2018

So having re-read this now with the context of @emschwartz implementation and in light of some of the comments on #394 I think I have clearer picture of the problem statement and proposal.

I think, as usual 😄 , we're diving straight into use cases without much architecture or modeling so let me try to restate things as I see them and see if that helps.

Our goal is to establish a "payment socket" between two parties so they can trade ILP packets back and forth to ultimately transfer some amount of value between them.

We can assume that either the receiver knows up front how much they want to receive or that the sender knows up front that they know how much they want to send, or both.

Chunked or streamed payments (i.e. a payment sent as multiple ILP packets) are a great innovation but they do have two major challenges:

  1. The sender and receiver have no idea what the exchange rate is between their two assets until the first packet travels between them.
  2. That rate can change while packets are flowing so it's possible that a sender has sent as much as they are prepared to send and yet not enough has arrived at the receiver. If we try to perform a refund there is a good chance the receiver ends up with zero and the sender with less than they had before (only the connectors win).

Because of this we can't simply define a payment in terms of a send amount and/or receive amount. The payment must defined in terms of a maximum and minimum amount at both ends. But, a payment occurs over time (i.e. the state changes with each packet sent) so we also need to define the state in terms of a current balance.

Ignoring refunds for now, I believe this is what @sharafian has described in the OP: A way to define a payment in terms of:

  • max and min send amounts
  • max and min receive amounts
  • a current balance at the sender
  • a current balance at the receiver

With just that information it is possible to initiate a payment, send multiple ILP packets and eventually get to a point where the balance at either the sender or receiver hits one of their limits. At this point the payment is complete.

It's possible that it hasn't been completed to the satisfaction of both parties, for example if the balance at the receiver is still below the minimum but the balance at the sender has exceeded it's maximum.

But, without changes to either of these values no more packets will be sent be either party.

Where this get's complicated is in @justmoon 's additional uses case requirements where he describes changes to the max and min on both ends. As soon as these change this is no longer a payment because you can't change the terms of a payment halfway through.

I would describe changes to the max and min as something new. My proposal is that each change is also just a new payment. So, let's adjust our definitions slightly.

The ability to exchange ILP packets between two entities is a payment socket. The socket has a current balance, which, when it is first established is 0. The socket also has two current rates (one in each direction). These also change as packets flow and it is revealed how much was received on one end of the socket in relation to how much was sent.

Now, if you look at our definition of a payment above, it also always has a starting balance of 0 on both sides, and we actually don't know the rates until we start paying. So we could redefine what we call a payment in terms of the existence of a payment socket:

An ILP Payment is a change to the max and min values on both ends of an ILP Payment Socket.

Let's walk thorough an example (I've repurposed @justmoon's SPSP Payment with refund):

There is a need to define a protocol here, and for now I propose we define this on top of PSK and call it the Payment Socket Protocol.

A client "Alice" wants to pay an invoice for $1 on server "Bob". Both sides use USD cents accounts (i.e. USD at a scale of 2).

Alice establishes a new Payment Socket to Bob. In practical terms this means Alice sends a probing ILP Packet that Bob does not fulfill but in the response Bob provides the amount he received so Alice now has the rate.

Note: Alice could do this any time. She doesn't need to be ready to send a payment. She is simply establishing a socket. If Bob wants to make it a duplex socket he sends a probing payment to Alice that she also doesn't fulfill so he knows the rate from his side too.

The current state of the socket from Alice's perspective is:

  • Balance: 0
  • Rate: 1:1
  • Min Balance: 0
  • Max Balance: 0

i.e. The socket is "balanced" so no packets are flowing.

Now Alice wants to make a payment. She submits the "ILP Payment" to the socket in the form of a max and min amount which are applied as deltas on the current max and min and also the requested deltas at the other end of the socket.

Alice doesn't have a specific amount she wants Bob to receive so she uses the current rate of the socket to guess (she adds some slippage allowance). The payment she submits to the socket is defined as:

{
  "payment_id": "80ca4c3c0a2d662130cf88aeee8c5842b421bf0e27f53cd609a2403c10b91ca",
  "local_max_delta": -100,
  "local_min_delta": -100,
  "remote_max_delta": 101,
  "remote_min_delta": 0 
}

Her new view of the socket is:

  • Balance: 0
  • Rate: 1:1
  • Min Balance: -100
  • Max Balance: -100

As a result of Alice submitting that payment, the Payment Socket Protocol module starts sending ILP packets across the socket. The module puts a definition of the payment into the data of the ILP Packet (part of PSK envelope? I think this is a higher layer). The module is also continuously updating the current rate and balance of the stream as it gets responses back so Alice can query the state.

The definition of the payment sent to Bob is:

{
  "payment_id": "80ca4c3c0a2d662130cf88aeee8c5842b421bf0e27f53cd609a2403c10b91ca",
  "max_delta": 101,
  "min_delta": 0 
}

When Bob get's the first packet he looks at the definition of the payment from Alice and decides if he accepts it or not.

First he checks if this is the first time he has seen this payment (based on the ID) and if so he looks at the deltas and decides if he accepts those changes to the scoket. If he does, he applies the deltas to his own max and min for the socket. If he has accepted this payment before he can just ignore the rest of the payment definition because he's already receiving this payment.

The payment definition is sent in all packets since it's small and avoids issues around the first packet being lost and Bob not knowing what to do with subsequent packets.

So, assuming the first packet carried 1c and the above payment definition. Bob's view of the socket after getting the first packet is now:

  • Balance: 1
  • Rate: 1:1
  • Min Balance: 0
  • Max Balance: 101

This continues until Alice stops sending ILP Packets, the balance gets to 101 or (as in the example above Alice decides to request a refund).

My suggestion is that we treat a refund as a request for a new payment (i.e. a payment definition sent in a new ILP Packet, probably with a zero amount).

So let's assume Alice has sent 50c and then cancelled. In practical terms she would likely have performed two operations. First she would have called cancelPayment on the socket and the stream controller would have stopped sending packets.

Alice then creates a reversal of her previous payment using a new payment id:

{
  "payment_id": "80ca4c3c0a2d662130cf88aeee8c5842b421bf0e27f53cd609a2403c10b91ca",
  "local_max_delta": 100,
  "local_min_delta": 100,
  "remote_max_delta": -101,
  "remote_min_delta": 0 
}

Alice submits this new payment to the socket and her new view of the state is:

  • Balance: -50 (already sent)
  • Rate: 1:1
  • Min Balance: 0
  • Max Balance: 0

A new ILP Packet is sent to Bob with a zero value and the following new payment:

{
  "payment_id": "80ca4c3c0a2d662130cf88aeee8c5842b421bf0e27f53cd609a2403c10b91ca",
  "max_delta": -101,
  "min_delta": 0 
}

At this point Bob's view of the stream is:

  • Balance: 50
  • Rate: 1:1
  • Min Balance: 0
  • Max Balance: 101

This new payment would result in an outflow for Bob but Bob offers refunds so he accepts it.

He could also perform some other business logic where he reconciles this with the original payment etc? I'd expect accepting of a new payment to be a hook on the Payment Socket Protocol module that allows application layer logic to intercept and respond.

After accepting the payment Bob's adjusted view of the socket is:

  • Balance: 50
  • Rate: 1:1
  • Min Balance: 0
  • Max Balance: 0

So he starts sending to reduce his balance back to 0 (equal to his max).

Notes:

  • I haven't even started to define the protocols required to create a new socket and start making payments on it. Those will likely involve SPSP and payment pointers but they are different to this protocol and should not be tightly coupled to it.
  • A big advantage of this model is that payments are asynchronous. From the perspective of the application there are just payments and from the perspective of the socket, just balances. E.g. An application can start reversing one payment before cancelling it because the reversal is not concerned with the total amount paid to date.

@emschwartz
Copy link
Member

That is pretty close to what I've implemented so far with two exceptions: the payment ID and setting limits for what you want the balance on the other side to be.

What is the payment ID for? Is that necessary? Is there an advantage to thinking of multiple payments going through a socket as discrete things versus you caring just about what the balance is at the end? (Note that packets will not be lost because sockets are defined by specific destination addresses)

(part of PSK envelope? I think this is a higher layer)

This protocol is implemented on top of PSK2 so the data goes into the PSK2 data field.

I haven't even started to define the protocols required to create a new socket and start making payments on it. Those will likely involve SPSP and payment pointers but they are different to this protocol and should not be tightly coupled to it.

Right now it has the same setup parameters as normal PSK2. The server now creates a unique destination address and shared secret and gives them to the client through a higher-level protocol. The client sends a packet to inform the server of its address (and right now secret, because different secrets are being used in both directions but I'd like to change that). That first packet can also be used to determine the exchange rate.

@emschwartz
Copy link
Member

Also, in your explanation it seems like the packets sent from Alice to Bob change Bob's local view of the payment parameters, but I don't think that's correct. You can't affect the other party's local values, only inform them of what you're looking for.

@emschwartz
Copy link
Member

emschwartz commented Feb 16, 2018

Some thoughts and open questions on the over-the-wire protocol:

Protocol

All of these are messages sent through PSK2

Opening socket (client to server):

Field Type Description
destinationAccount ILP Address Client socket address
sharedSecret? Var Octet String Client socket shared secret (unless they use the same secret in both directions: emschwartz/ilp-protocol-paystream#4)
amountExpected UInt64 (or VarInt? emschwartz/ilp-protocol-paystream#6) Client socket minBalance minus currentBalance, or 0
amountWanted UInt64 (or VarInt?) Client socket maxBalance minus currentBalance

This should probably be sent on an unfulfillable payment with an amount greater than 0 so that the client can figure out the rate. (PSK2 modules need to pass data from unfulfillable payments to the receiver in order for this to work)

The server should probably respond with another unfulfillable payment to determine the exchange rate in the other direction and communicate their amountExpected and amountWanted. This would allow both sides to determine whether their limits are likely to be met before starting to send money (emschwartz/ilp-protocol-paystream#11).

Each chunk

Field Type Description
amountExpected UInt64 (or VarInt?) Client socket minBalance minus currentBalance, or 0
amountWanted UInt64 (or VarInt?) Client socket maxBalance minus currentBalance

If we want to support clients or servers changing their address and secret while keeping a socket open, we could make each chunk use the same message as when the connection is established but just set the destinationAccount and sharedSecret to the empty string (that's what's currently implemented).

Note this protocol does not support sending additional application data over the stream.

Socket close

Not sure about this one. Do we need a specific message that says "I'm closing the socket"? Or can you figure that out from the fact that the other party isn't requesting or paying you any more? If we need a specific message for this, is there anything else you'd want to communicate (like whether the party closing the socket is satisfied with the outcome)?

Note having a socket close message would also be useful if you try to connect to a socket that's closed or doesn't exist on the server side (see emschwartz/ilp-protocol-paystream/.../index.ts#L439-L456).

Open Questions

  • Should the client and server use the same sharedSecret? Update: Yes. Implemented in emschwartz/ilp-protocol-paystream@dda724f
  • Should the amounts be UInt64s, VarUInts or VarInts? Should you be able to communicate a negative amount if you want to pay?
  • Should the two sides communicate the exchange rate they see to one another? That would help them determine what the rate will be if they request a refund (but that may be unnecessary).
  • Do we need specific messages for socket close and/or errors?
  • Should senders allow the exchange rate to fluctuate at all from the rate they determine initially, or should that cause an error? (Track exchange rate and Maximum Payment Size emschwartz/ilp-protocol-paystream#3 (comment))

@emschwartz
Copy link
Member

@justmoon brought up a good question about whether this should be implemented as a protocol on top of PSK2 or if the two should be combined into PSK3.

Reasons Not to Combine PSK2 and Payment Sockets:

  • A spec and implementation for PSK2 already exists and it's better to keep building on top rather than breaking and refactoring things
  • If PSK2 never got used outside of payment sockets, it wouldn't cause any substantive problems for them to be separate
  • PSK2 could be useful on its own as a more UDP-like protocol that would also provide encryption and condition generation

Reasons to Combine Them:

  • Clearer failure if you try to connect a payment socket client to a different type of receiver (the data wouldn't be decrypted -- otherwise there's a chance that the payment socket data could be confused for another protocol built on top of PSK2)
  • If payment sockets work well, we're probably going to integrate them into all of our current use cases and higher-level protocols, opening a question about why PSK2 still exists as a separate thing
  • We don't need a name for another layer in the Interledger stack

Thoughts?

@emschwartz
Copy link
Member

After working on them separately, I think it makes sense to combine the stream idea and PSK. PSK2 doesn't provide enough functionality on its own to be considered a proper layer, and it's much more useful to have a protocol that handles splitting up value into packets automatically.

Implemented in #417

@stale
Copy link

stale bot commented May 10, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. \n\n If this issue is important, please feel free to bring it up on the next Interledger Community Group Call or in the Gitter chat.

@stale stale bot added the wontfix label May 10, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants