A simple Ethereum payment channel implementation

Alex Miller
7 min readJun 12, 2017

Last week this article surfaced, informing (or perhaps reminding) Ethereum developers that simple payment channels are feasible today and are not terribly difficult to implement. This was perfectly timed for me, as I was just about ready to start hacking away at a basic payment channel for Grid+. While I appreciated the article and its inspirational effect, I found it lacking in implementation details and upon diving deeper I found a few non-trivial technicalities that will throw you through a loop if you’re not careful.

I thought the community would benefit from a fully fledged guide containing everything you need to add a simple payment channel to your app. So here it is. To follow along, check out my eth-dev-101 repo, which contains a contract file, a truffle test file, and a mocha test file.

Opening a channel

The first thing I did was rewrite the contract. I suspect there are several ways to make a functioning payment channel — here is mine. To open a channel, a sender would call this function:

function OpenChannel(address to, uint timeout) payable {
// Sanity checks
if (msg.value == 0) { throw; }
if (to == msg.sender) { throw; }
if (active_ids[msg.sender][to] != bytes32(0)) { throw; }
// Create a channel
bytes32 id = sha3(msg.sender, to, now+timeout);
// Initialize the channel
Channel memory _channel;
_channel.startDate = now;
_channel.timeout = now+timeout;
_channel.deposit = msg.value;
_channel.sender = msg.sender;
_channel.recipient = to;
channels[id] = _channel;
// Add it to the lookup table
active_ids[msg.sender][to] = id;
}

This instantiates a new Channel struct with the relevant data including the sender, recipient, duration, and deposit amount. The function is payable and msg.value must be non-zero. This message amount will function as the deposit and all messages in the channel must be less than or equal to that deposit amount.

Get the channel id

Once the channel is open, we can get the channel id out of the active_ids mapping. Note that this means only one channel may be open for any two participants at any one time. I think this restriction is okay for most applications.

function GetChannelId(address from, address to) public constant returns (bytes32) {
return active_ids[from][to];
}

This function is called on this line of our mocha file.

Sending messages

Once the channel is open and we have its id, the sender can pass signed messages to the recipient. One such message is formed in this test:

var sha3 = require('solidity-sha3').default;var _value = 0.01*Math.pow(10, 18)    
var value = _value.toString(16)
let _msg_hash = sha3(`0x${channel_id}`, _value);
let msg_hash = Buffer.from(_msg_hash.substr(2, 64), 'hex');
let sig = util.ecsign(msg_hash, keys.test.privateKey);
let parsed_sig = {
v: sig.v.toString(16),
r: sig.r.toString('hex'),
s: sig.s.toString('hex')
};
latest_value = value;
latest_sig = parsed_sig;
latest_msg_hash = msg_hash.toString('hex')

Verifying a message

When a recipient is given a signed message by the sender, he needs to decode it and verify that it could be used to close the channel. This can be hacked out in javascript, but I opted to instead write a constant contract function. This is basically a dry run for closing the channel — it returns true if the channel can be closed with the message and false otherwise.

function VerifyMsg(bytes32[4] h, uint8 v, uint256 value) public constant returns (bool) {
// h[0] Channel id
// h[1] Hash of (id, value)
// h[2] r of signature
// h[3] s of signature
// Grab the channel in question
if (channels[h[0]].deposit == 0) { return false; }
Channel memory _channel;
_channel = channels[h[0]];
address signer = ecrecover(h[1], v, h[2], h[3]);
if (signer != _channel.sender) { return false; }
// Proof that the value was hashed into the message
bytes32 proof = sha3(h[0], value);

// Ensure the proof matches
if (proof != h[1]) { return false; }
else if (value > _channel.deposit) { return false; }
return true;
}

This function validates that the sender is indeed the one who signed the message and that the provided value is both correct and acceptable for the channel.

Closing the channel

Once either participant is happy with the state of the channel and wants to close it, he or she may do so using this function:

function CloseChannel(bytes32[4] h, uint8 v, uint256 value) {
// h[0] Channel id
// h[1] Hash of (id, value)
// h[2] r of signature
// h[3] s of signature
// Grab the channel in question
if (channels[h[0]].deposit == 0) { throw; }
Channel memory _channel;
_channel = channels[h[0]];
if (msg.sender != _channel.sender && msg.sender != _channel.recipient) { throw; } address signer = ecrecover(h[1], v, h[2], h[3]);
if (signer != _channel.sender) { throw; }

bytes32 proof = sha3(h[0], value);
if (proof != h[1]) { throw; }
else if (value > _channel.deposit) { throw; }
// Pay out recipient and refund sender the remainder
if (!_channel.recipient.send(value)) { throw; }
else if (!_channel.sender.send(_channel.deposit-value)) { throw; }
// Close the channel
delete channels[h[0]];
delete active_ids[_channel.sender][_channel.recipient];
}

Note that this is very similar to VerifyMsg, but for two differences:

  1. It may only be called by the sender or recipient of the channel
  2. It pays the recipient, refunds the sender the remainder, and deletes the channel

An example of closing a channel is shown in this test.

Channel timeouts

A careful reader might have noticed that these functions do not check that the channel is expired. This was by design, as there is another function that will delete a channel when the time is expired.

function ChannelTimeout(bytes32 id){
Channel memory _channel;
_channel = channels[id];
// Make sure it's not already closed and is actually expired
if (_channel.deposit == 0) { throw; }
else if (_channel.timeout > now) { throw; }
else if (!_channel.sender.send(_channel.deposit)) { throw; }
// Close the channel
delete channels[id];
delete active_ids[_channel.sender][_channel.recipient];
}

Note that this refunds the entire deposit to the sender if the channel is expired, so the recipient should be diligent about that expiration time! You could probably implement payment channels without timeouts, but I like having them for reasons I won’t get into.

Token Channels!

The payment channel idea is easily extendible into channels that use ERC20 tokens as payment. Consider this slightly tweaked OpenChannel function:

function OpenChannel(address token, address to, uint amount, uint timeout) payable {
// Sanity checks
if (amount == 0) { throw; }
if (to == msg.sender) { throw; }
if (active_ids[msg.sender][to] != bytes32(0)) { throw; }
// Create a channel
bytes32 id = sha3(msg.sender, to, now+timeout);
// Initialize the channel
Channel memory _channel;
_channel.startDate = now;
_channel.timeout = now+timeout;
_channel.deposit = amount;
_channel.sender = msg.sender;
_channel.recipient = to;
_channel.token = token;
// Make the deposit
ERC20 t = ERC20(token);
if (!t.transferFrom(msg.sender, address(this), amount)) { throw; }
channels[id] = _channel; // Add it to the lookup table
active_ids[msg.sender][to] = id;
}

Note that this calls transferFrom, which means the sender needs to approve the channel to move tokens ahead of time. Everything else is exactly the same from the user’s perspective— see the contract here and the mocha test here.

A note about gas

It should be noted that payment channels are not scalability mechanisms for the Ethereum network itself — we need on-chain scaling solutions such as Proof of Stake and sharding to expand the overall transactional throughput. However, payment channels can alleviate a lot of network traffic save a lot of gas for individual applications if used properly. Consider that an ether transfer costs around 20,000 gas, while a token transfer is somewhere around 50,000. Opening a channel is about 150,000 and 250,000 gas, respectively (I didn’t clock these precisely, but the numbers are correct within +/- 50,000). Closing a channel costs roughly the same amount (also not precise measurements).

So if we want to break even, we would need roughly:

(150,000+150,000)/20,000 ~ 15 ether transactions
(250,000+250,000)/50,000 ~ 10 token transactions

These are very low for many applications, but can’t be justified for everything. Just keep this in mind when determining if your application needs a payment channel.

Miscellaneous points

  • In my tests, you’ll notice things like 0.01*Math.pow(10, 8). This is at odds with an article I wrote previously about usingBigNumber. I can write my number in plain javascript because I’m not operating on it. If I were dividing my number (and certainly if it had 18 decimals instead of 8), I would need to use the BigNumber representation.
  • You may have noticed that a smart receiver will always submit the highest amount when he or she is done with the channel. This is fine for applications where the message amount grows monotonically, but many implementations of state channels will use nonces and dispute resolution. Roughly, this means that the sender keeps all messages and, if the receiver cheats and tries to close the channel with an earlier message, the sender can dispute that. If the sender disputes with a message that has a higher nonce, that message will be used to close the channel instead.

Update 6/19/17 Version 2:

Upon integration into Grid+, I didn’t like a few things in the above implementation. I wrote a second version of the above payment channel, which can be found here. The improvements are as follows:

  1. The messages now include nonces, which are meant to be incremented by the sender.
  2. Instantiation of the channel includes an optional “challenge period” whereby either party can submit a message with a higher nonce to replace the message currently queued to close the channel.
  3. The ChanelTimeout function has been deleted.
  4. The channel can only be closed by the receiver (though either party can submit challenged).

That’s all folks

That’s all there is to my simple payment channel implementation. It’s still a fresh idea and I welcome any feedback and suggestions. Huge thanks to Matthew Di Ferrante for writing that initial article, which got my mental gears spinning.

Hopefully this tutorial is enough to seed some simple implementations across the community. Contracts are getting quite expensive and I think payment channels could help lighten the load if used properly.

Grid+ is a perfect place for payment channels and if you aren’t familiar with us already, check out https://gridplus.io to join our mailing list. You can also follow us on twitter. Thanks for reading!

--

--

Alex Miller

Developer/writer/thinker living in the cryptoverse. Co-founder of GridPlus