Building Long-Lived Payment Channels

[EDIT 2018-03-13] This post has been updated to use Solidity 0.4.21 event syntax.

In Writing a Simple Payment Channel, I introduced payment channels as a way to reduce the number of Ethereum transactions required for repeated payments between the same two parties. This post will improve upon that post’s SimplePaymentChannel contract to make it suitable for long-lived payment channels, such as might be used to pay an employee an hourly wage over the course of their career.

Introduction

The SimplePaymentChannel from Writing a Simple Payment Channel works well for payments made over a short period of time, but it has three limitations in the context of long-lived channels:

The sender must escrow all ether up front.

The recipient can make only a single withdrawal.

The timing of the channel closure is fixed when the channel is created.

I’ll make three changes to the SimplePaymentChannel contract to address these shortcomings:

Allow the sender to escrow a minimal amount of ether up front and add more funds as needed.

Allow the recipient to withdraw ether as needed before closing the channel.

Allow the sender to initiate channel closure so they can recover unspent funds in a reasonable timeframe.

Minimizing the Escrowed Funds

Payment channels hold escrowed funds to guarantee that a valid, signed payment message will be honored. Recipients will accept a message saying “I owe you a total of n ether,” if and only if the channel contract has escrowed n ether.

Because it’s trivial to check the channel’s current balance, it’s reasonable to allow multiple deposits to the channel:

functiondeposit()publicpayable{}

With this addition, the sender no longer needs to escrow all funds up front. However, each deposit requires an Ethereum transaction—and thus a transaction fee—so the sender must make a tradeoff between smaller, more frequent deposits and larger, less frequent ones.

Allowing Early Withdrawals

In the SimplePaymentChannel, the recipient can only make a single withdrawal, which closes the channel. A little bookkeeping enables multiple withdrawals without closing the channel. Recall that a withdrawal is done by presenting a signed IOU from the sender to the channel contract:

uint256publicwithdrawn;// How much the recipient has already withdrawn.functionwithdraw(uint256amountAuthorized,bytessignature)public{require(msg.sender==recipient);require(isValidSignature(amountAuthorized,signature));// Make sure there's something to withdraw (guards against underflow)require(amountAuthorized>withdrawn);uint256amountToWithdraw=amountAuthorized-withdrawn;withdrawn+=amountToWithdraw;msg.sender.transfer(amountToWithdraw);}

A brief explanation of the above code:

The withdrawn state variable tracks how much ether the recipient has already withdrawn.

The recipient withdraws the total amount that’s been authorized so far minus the amount already withdrawn.

Finally, I need to make a small change to the close function to take into account the amount already withdrawn:

As in the previous section, this new ability introduces a tradeoff. The recipient can now access funds early by using the withdraw function, but each withdrawal requires an Ethereum transaction.

Allowing the Sender to Close the Channel

The SimplePaymentChannel introduced in Writing a Simple Payment Channel has an expiration time built in, and only the recipient can close the channel earlier than that. This is a problem for long-lived payment channels because it means the sender has no way to recover escrowed, unspent funds without the recipient’s cooperation.

To support long-lived payment channels, I’ll allow the sender to initiate channel closure. The recipient will then have some time to claim any funds they’re owed, after which the sender can access whatever’s left. With this new mechanism, there’s no need to have a fixed expiration at all.

// How much time the recipient has to respond when the sender initiates// channel closure.uint256publiccloseDuration;// When the payment channel closes. Initially effectively infinite.uint256publicexpiration=2**256-1;functionLongLivedPaymentChannel(address_recipient,uint256_closeDuration)publicpayable{sender=msg.sender;recipient=_recipient;closeDuration=_closeDuration;}

The preceding code sets up the state variables that are used for sender-initiated channel closure:

The closeDuration specifies, in seconds, how long the recipient will have to claim their funds after the sender initiates a close. It is set in the contract’s constructor.

The expiration is when the sender is allowed to close the channel. Initially, there’s effectively no expiration.

The recipient can watch for the StartSenderClose event so they know when it’s time to collect what they’re owed by closing the channel.

If the timeout is reached before the recipient closes the channel, the sender can close it and claim all remaining funds:

// If the timeout is reached without the recipient closing the channel, then// the ether is released back to the sender.functionclaimTimeout()public{require(now>=expiration);selfdestruct(sender);}

Summary

For long-lived payment channels, it’s desirable to maximize availability of funds for both the sender and the recipient.

At any given time, the sender only needs to have enough funds escrowed to cover the amount already committed to the recipient.

At any given time, the recipient can withdraw up to the amount they’re owed.

Sender-initiated channel closure ensures that the sender can recover unpaid funds in a reasonable timeframe.

Full Source Code

longLivedPaymentChannel.sol

pragma solidity^0.4.21;contractLongLivedPaymentChannel{addresspublicsender;// The account sending payments.addresspublicrecipient;// The account receiving the payments.uint256publicwithdrawn;// How much the recipient has already withdrawn.// How much time the recipient has to respond when the sender initiates// channel closure.uint256publiccloseDuration;// When the payment channel closes. Initially effectively infinite.uint256publicexpiration=2**256-1;functionLongLivedPaymentChannel(address_recipient,uint256_closeDuration)publicpayable{sender=msg.sender;recipient=_recipient;closeDuration=_closeDuration;}functionisValidSignature(uint256amount,bytessignature)internalviewreturns(bool){bytes32message=prefixed(keccak256(this,amount));// Check that the signature is from the payment sender.returnrecoverSigner(message,signature)==sender;}// The recipient can close the channel at any time by presenting a signed// amount from the sender. The recipient will be sent that amount, and the// remainder will go back to the sender.functionclose(uint256amount,bytessignature)public{require(msg.sender==recipient);require(isValidSignature(amount,signature));require(amount>=withdrawn);recipient.transfer(amount-withdrawn);selfdestruct(sender);}eventStartSenderClose();functionstartSenderClose()public{require(msg.sender==sender);emitStartSenderClose();expiration=now+closeDuration;}// If the timeout is reached without the recipient closing the channel, then// the ether is released back to the sender.functionclaimTimeout()public{require(now>=expiration);selfdestruct(sender);}functiondeposit()publicpayable{require(msg.sender==sender);}functionwithdraw(uint256amountAuthorized,bytessignature)public{require(msg.sender==recipient);require(isValidSignature(amountAuthorized,signature));// Make sure there's something to withdraw (guards against underflow)require(amountAuthorized>withdrawn);uint256amountToWithdraw=amountAuthorized-withdrawn;withdrawn+=amountToWithdraw;msg.sender.transfer(amountToWithdraw);}functionsplitSignature(bytessig)internalpurereturns(uint8,bytes32,bytes32){require(sig.length==65);bytes32r;bytes32s;uint8v;assembly{// first 32 bytes, after the length prefixr:=mload(add(sig,32))// second 32 bytess:=mload(add(sig,64))// final byte (first byte of the next 32 bytes)v:=byte(0,mload(add(sig,96)))}return(v,r,s);}functionrecoverSigner(bytes32message,bytessig)internalpurereturns(address){uint8v;bytes32r;bytes32s;(v,r,s)=splitSignature(sig);returnecrecover(message,v,r,s);}// Builds a prefixed hash to mimic the behavior of eth_sign.functionprefixed(bytes32hash)internalpurereturns(bytes32){returnkeccak256("\x19Ethereum Signed Message:\n32",hash);}}