A couple of months ago, we announced the alpha launch of the thunder.network. As we explained at the time, the vast majority of thunder payments are processed without touching the bitcoin blockchain, but that can only work if the few that eventually make it into the blockchain are handled correctly. In layman’s terms: a contract is useless unless it can be enforced in court. The same is true of thunder payments – users need to feel confident that the payments that make it to “court” (in this case, the bitcoin blockchain) will be valid.
Initially, we introduced the idea of a two-layer approach for payments: dual-tx. There are many advantages to this approach but it turns out that implementing that approach was far more difficult than anticipated.
How dual-tx makes thunder more user friendly
Refunds, in existing implementations, can take over a month and you can’t stay offline for more than a day. Want to go on a two-week vacation? That could mean refund times of over a year. You can find details on that here. We want to make refund windows as short as possible and to simultaneously prevent the payment recipient from dragging it out.
Dual-tx adds a second layer between broadcasting the settlement and claiming a payment which allows us to clearly separate the process of claiming a payment from waiting out the revocation delay. If the receiver of a payment wants to redeem a payment, he has to broadcast the second transaction which will allow them to claim the output of the second-tx after the revocation delay. If the user cheated, however, the counterparty can claim the funds even from the second layer. The second transaction acts as a safety net. Note: going through the two layers is only necessary if you want to claim the outputs of a channel transaction you broadcasted. The other party can always redeem or refund a payment directly from your channel.
This approach allows for a 1-day payment window with a 2-week revocation delay, which meaningfully improves upon the existing implementations.
Below, Blockchain engineer Mats Jerratsch goes into detail on our solution for this problem, which allows for an application that is much more convenient. For those interested in geeking out, read on.
Handling a payment settlement
In order to make it easier to assess the correctness of the approach, I’ll split the process into 3 steps.
First, we monitor incoming blocks for transactions that spend the anchor of the channel. When we discover such a transaction, we call ChainSettlementHelper.onChannelTransaction(..) which analyzes the transaction, determines its origin, and confirms that the transaction matches the version we have on file (if it doesn’t, someone cheated). It then creates a ChannelSettlement object for each payment and for the normal output to keep track of the individual states. When done, all ChannelSettlements will be saved in the database.
Second, ChannelSettlements are updated with the information found in each block from there on. We save counterparty refunds and payment claims and updates the next time we need to act upon this state. You can see the different cases in ChainSettlementHelper.onBlockSave(..)
Third, bitcoin transactions are created in ChainSettlementHelper.onBlockAction(..) and broadcast based on one of the following flags:
- ourTx — did we broadcasted the channel to the blockchain or is it the other party that wished to settle on-chain?
- cheated — is the channel transaction the most recent one, or did someone try to cheat?
- sending — have we initiated this payment or are we on the receiving end?
- secondTx — have we seen the second transaction for that payment in a block yet?
Here’s what an ordinary case would look like: I receive a payment from Bob and am ready to redeem it but Bob is currently offline. As we approach the refund time that Bob and I negotiated when making the payment, I need to make a decision. Do I want to forget about the payment, even though I am eligible to redeem it, or do I want to keep the payment channel open. For the sake of this argument, let’s say we want to close the channel.
- We broadcast the latest channel transaction.
- Once we discover it in a block onChannelTransaction(..) gets triggered and saves a ChannelSettlement object into the database.
- onBlockAction(..) will create the dual-tx containing the payment secret, which is necessary, such that the payment can’t time out.
- The transaction from (3) will make it into the next block, such that onBlockSave(..) will update the ChannelSettlement object. It also saves that it will need to wait out the revocation delay, in this case one week.
- Wait 1008 blocks (= one week)
- ChainSettlementHelper.onBlockAction(..) creates the transaction necessary to finally claim the funds into our wallet.
So you tried to rip me off?!
When the counterparty cheated (!ourTx|cheated) we generally can claim all outputs of the channel transaction. To do this, we have to retrieve the revocation hash used in that transaction. Unless we happened to lose data, we can always retrieve the revocation hash either from our database because we still have it stored, or by computation if we go with more advanced mechanisms like shachain. However, the counterparty could also waste some money if he decides to broadcast the second tx for that payment. While we are still able to claim the output of the second tx, there are obviously some losses due to transactions fees.
For the cases where we cheated (ourTx|cheated), the current implementation does not follow up with any action. Even though it could try to claim the payments anyways, this would work very rarely and I don’t want to encourage this behaviour in any way. Feel free to add it though.
The perfect code — to never be executed
Though I hope this code will never need to be used because we won’t have to worry about cheating counterparties, we’ve done rigorous unit testing in thunder to cover all the cases of payments, and to also make sure the bitcoin scripts actually work. That said, despite our best efforts, a code that isn’t battle-tested is hard to perfect so I welcome your exploration of the code and your thoughts and reviews!