2023.07 Vol.1

Bit of proxy invoices

We Did Something Weird with CLTV

Mash would like to be in the game of coordinating transactions between users. For example, Alice is selling cat gifs and Bob wants to buy one. Mash tech helps both Alice and Bob by making it easy to setup the transaction, be it by supplying drop in widgets for Alice to price and sell their gifs or a browser wallet for Bob to make lighting transactions. But when it comes to the movement of actual funds, they go straight from Bob to Alice with no middle man responsibilities for Mash. We don’t wanna go back to the fiat world of “Bob pays paypal, paypal pays Alice, everyone trusts and gives paypal a cut”. If you are aware of lightning tech, you already know we shouldn’t have too, but we have run into a few tricky scenarios.

In the easiest scenario, Bob is viewing Alice’s website and would like to purchase a cat gif. Alice is using Mash to monetize and has a little widget on her site that helps collect lightning payments before giving a cat gif to the buyers. Bob clicks the widget which goes and fetches a lighting invoice from Alice. Bob pays the invoice with a lightning wallet in his browser (maybe something like Mutiny). Bob then passes the preimage proof-of-payment back to the widget which can verify it and give access to the cat gif. Alice and Bob have exchanged a bit of privacy and security for the ease of setting up the transaction. Mash is aware of the invoice and the preimage. It doesn’t have an incentive to do anything with them, but it is still the trade being made and some users might not want to make it. In any case though, this easy scenario is pretty easy!

So what is a non-easy scenario? Well it turns out not very many users have lighting wallets in their browser at the moment. While the easy scenario is great, Mash doesn’t want to require Bob to get a browser wallet in order to pay Alice. If Bob wants to pay with the lighting wallet which is instead on his phone, that should be fine too. So let’s step through it again. Bob is on Alice’s site and triggers the widget to fetch an invoice. The invoice is displayed as a QR code and Bob pays for it with his phone. Very cool. But now the preimage proof-of-payment is sitting on Bob’s phone, not in his desktop’s browser like it is in the easy scenario. How is he able to get the preimage from his phone to his desktop? There are technically tons of ways to do this, but they are all kinda clunky and defeat the point of making the transaction simple for both parties.

How can we grease these wheels? Well we took some inspiration from the lnproxy project. The goal of lnproxy is to hide either the payer or the payee in a lightning payment in trustless fashion. Let’s say the payer is using a custodial wallet and doesn’t want the custodians to know who they just sent a payment too. They can take the invoice and ask the lnproxy service to proxy it. The custodial wallet pays the proxy invoice which allows the proxy service to pay the original invoice, but the original is not exposed to the custodial service. Where lnproxy is focused on the privacy aspect, Mash saw it as a way to solve the “how to get the preimage from the mobile device” challenge.

Back to Alice and Bob, Bob gets the invoice for the cat gif, but tells the Mash widget he actually wants to pay it from a mobile device and not within the browser. So the Mash widget wraps the invoice with its proxy service and exposes the new proxy invoice as a QR code. The mobile device pays the proxy invoice, but since the Mash service is now in the payment path it can get the preimage and hang onto it for Bob. Bob was going to give the preimage to Mash anyways as proof-of-payment, so he doesn’t mind that they are automatically grabbing it. And now he doesn’t have to deal with getting the preimage from his phone to his desktop!

So how is this trustless? The proxy service uses HODL invoices under the hood so that the two invoice are paid atomically.

The HODL proxy flow:

  1. The client passes an invoice to the proxy service.
  2. The proxy service creates a new HODL invoice with the same payment hash and amount as the original invoice, it tosses this new HODL proxy invoice back to the client.
  3. The client pays the proxy invoice.
  4. Since it is a HODL invoice, the proxy service holds the payment instead of immediately returning the preimage (the key here is that the proxy service doesn’t have the preimage yet!).
  5. The proxy service now goes and pays the original invoice in order to get the preimage.
  6. The proxy service settles the HODL invoice with the newly acquired preimage.
  7. [Mash Only] Save the preimage to the user’s account as proof-of-payment.

Since the invoices are using the same payment hash, it is pretty low risk for the end-user. Back to Bob in the Mash scenario. Bob can verify the two invoice match before making a payment and he knows that the proxy service needs to pay the original invoice before settling the proxy invoice in order to get the preimage.

This is all great, are there downsides?

Imagine Bob is a bad actor and decides to attack a proxy invoice service. Bob generates a HODL invoice on his own node and passes it to the proxy service. The proxy service has no way of knowing that the invoice is actually going to Bob, and generally, doesn’t care. But Bob now pays the proxy invoice and waits for the proxy service to attempt to pay the original HODL invoice he created. Once the payment comes through, he just holds it in flight. For the attack to work, Bob needs to hold this payment long enough for his payment to the proxy invoice service to time out. As soon as it times out he can release the preimage for the proxy payment to the proxy service and collect their funds. So the proxy service ends up paying Bob for the preimage, but isn’t able to collect Bob’s payment for the proxy invoice. Bob wins!

This is pretty bad and forced Mash to get a little weird with CLTV settings in order to protect its proxy service from losing funds.

Some of the key security settings for HTLCs are the Check LockTime Verify deltas (described in blocks) which are used to help protect a user’s funds as a payment is routed through the network. The proxy service is interested in two CLTV settings. The first is the MaxCltv of a payment, which sets how long a wallet is willing to tie down funds for a payment. Every hop on a lightning payment route adds a bit to the aggregate CLTV of the payment. The second CLTV setting is the MinFinalCLTVExpiry of an invoice. This allows an invoice to set the minimum CLTV setting for the last hop of a lightning payment route (the hop to itself). Something to note is that the CLTV settings are all deltas, not absolute block heights, but the scripts in the HTLC payment outputs use absolute block heights.

So thinking back to the proxy flow, as long as the MinFinalCLTVExpiry of the proxy invoice is greater than the MaxCltv of the payment to the original invoice, Bob isn’t able to out-wait the proxy service. This is kinda tricky to think through. But it is following the same principles used to protect a routing node in a normal HTLC payment, the proxy service is just performing things at a “higher” level because it is attempting to connect two HTLC payments.

Within a normal HTLC payment all CLTV block heights are based on each other by calculations determined by the payer. The payer sets the initial block height to use for its hop and each hop in the route takes a chunk (its delta) of the blocks for its own protection. The proxy service is attempting to connect two HTLC payments, but in this case the payer (a.k.a. Bob’s wallet) doesn’t know about the second payment’s CLTV cost up front. So the CLTV cost needs to be completely baked into the last hop of the proxy invoice. That is how we arrive at MinFinalCLTVExpiry > MaxCltv. In other words, the last hop of the proxy invoice payment needs to cover the entire payment from the proxy service to the original invoice.

So what does the proxy service use for MaxCltv of the payment to the original invoice? The proxy service could just set the MaxCltv to something super high, like 2000 blocks, to protect it from anything. But this puts a large burden on the wallet paying the proxy invoice because it forces a high MinFinalCLTVExpiry, and thus a high total CLTV for the payment. The wallet might not be willing to risk funds being tied down for that long. Ideally, the proxy would know the exact total CLTV for a payment before it made the proxy invoice. This is sort of impossible to know for sure since the lightning network graph could change right before a payment, so the best we could do is an estimate of the total CLTV. While a fees estimate is readily available in the lnd router interface, a CLTV estimate isn’t (as far as I know). So for now, Mash’s proxy service just sets a MaxCltv value around 350 blocks and we will see how it works out in practice. The hypothesis is that 350 is high enough to reach almost any node on the network, while still being low enough for most wallets to be ok with paying.

The proxy service will also want to add a bit of CLTV delta for the same fund protection reasons a routing node adds a delta in a normal HTLC payment. Similar to the proxy service, the worst case scenario for a routing node is that it sends funds without receiving any. The CLTV delta is the time, in blocks, that a routing node has to pull back their funds before the upstream sender can pull back theirs. The key is that the router node needs to be “online” to pull back funds. So if they only give themselves 1 block of time and their ISP happens to have an outage where two block get mined, then the router’s funds are put at risk. The proxy service will want a similar window in order to pull back funds before the proxy payment does in order to protect their funds.

There are a few node settings which complicate things just a tad in the proxy service scenario. For example, lnd’s invoices.holdexpirydelta is a setting that cancels accepted HODL payments if they are getting too close to timing out. This is to avoid getting the channel force closed by the other party attempting to pull back funds. In the proxy service scenario though, this exposes the proxy service to losing funds. So the delta chosen by the proxy service should be greater than their invoices.holdexpirydelta setting.

So the algorithm becomes MinFinalCLTVExpiry > (MaxCltv + Delta). For reference, the default CLTV delta in lnd is 80 blocks and the invoices.holdexpirydelta is 15 blocks. I think an LSP operator could be comfortable having a proxy CLTV delta somewhere in between those two.

As with all things lighting, its a trade off, and if the payments are for small things (so not to lock down a lot of funds for a long time) I imagine most wallets would be cool with this approach.

UPDATE – Looks like lnd does in fact expose a CLTV estimate for a payment, so should be able to set a better MaxCltv.