State Channels

close button

Part II

Bidirectional State Channel

In the previous section, we saw how to use a unidirectional state channel to enable a simple payment redemption system. It is also possible to support more complex off-chain interactions with bidirectional state channels, where all parties can both send and receive messages. Like before, the state channel is less concerned with how agreements are arrived at off-chain, and more concerned with ensuring that the final state is agreed upon by all relevant parties.

Counting Game

In the mines, the owner devises a simple two-player game to incentivize her workers to fill their carts faster. The rules of the game are as follows:

  1. Two workers take turns loading ores into a minecart.

  2. Each turn, they can load between 1 to 4 ores.

  3. If the total ores loaded onto the cart is a multiple of 5, the cart spills a little, and two ores fall out.

  4. The first player to get the total ore count to above 20 wins themselves a bonus pay.

Say we wish to implement this game as a contract. A naive implementation will require 1 transaction for each turn. This is expensive and slow.

A cheaper alternative will be to implement the game as a bidirectional state channel. Like the payment channel in Part 1, the channel is opened with a deposit of funds by the channel's creator (e.g., the mine owner). The parties then send signed messages to each other via an off-chain channel, describing the amount of ores in the mine cart.

At any time, either party can submit any of these signed messages to the contract to update the on-chain state, so long as the message describes a future state of the contract. Once there is a winner, the channel is closed and the winner receives the contract's funds.


You will be implementing the functions of this contract, known as MineCart.

Exchanged Messages

The messages exchanged between the parties should describe the total amount of ores already loaded onto the cart. Additionally, it should also include a turn counter (messageNum) to ensure that no party can exploit the system by submitting older, yet still valid, states to the contract.

Like before, the message format follows the EIP-191 standard, and signatures are signed using ECDSA, under the secp256k1 curve.

bytes memory message = abi.encodePacked( "\x19Ethereum Signed Message:\n", "84", address(mineCart), totalOres, messageNum )

Each time either party wishes to update the on-chain state, they provide the game contract with a message that corresponds to the new totalOres and messageNum, with a corresponding signature signed by the other party. By verifying the signature, the contract can be certain that the new state has been agreed upon by both parties. If so, the contract doesn't need to know the details of how the parties arrived at this state to safely accept the state as true.

Withholding Messages

It is possible that one party may withhold messages from the contract, thereby preventing the game from advancing. To prevent this, the contract should allow:

  1. The active worker to make their next immediate move without the other party's signature, so long as the move is valid.

  2. The game to time out if the active worker does not make a move within a certain time frame.

These will be achieved with the load and timeOut functions.

The active worker is the worker whose turn it is to load the mine cart.

ABI of MineCart



constructor(address payable _worker1, address payable _worker2, uint256 _timePerMove) payable

Creates a MineCart. The ETH sent to the contract during creation is the pool of funds from which payments are made when the game is over.

View Function


worker1() → address payable

Address of worker 1, the worker who loads the cart every odd turn.

worker2() → address payable

Address of worker 2, the worker who loads the cart every even turn.

totalOres() → uint256

Total amount of ores already loaded onto the mine cart.

messageNum() → uint256

Current turn number. Starts at 1 (i.e., worker 1 starts the game).

isActive() → bool

Represents whether the channel is open or closed.



update(uint256 _totalOres, uint256_messageNum, bytes memory _signature)

Update totalOres and messageNum. msg.sender must be either worker and _signature must be signed by the other worker. If totalOres >= 21, then the channel is closed and the contract's funds are transferred to the victor.

load(uint256 _newOres)

Load _newOres into the cart. msg.sender must be the worker whose turn it is to load the cart and the move must follow the game's rules. If totalOres >= 21, then the channel is closed and the contract's funds are transferred to the victor.


If the current worker does not update the state in time, transfer all funds to the other worker and close the channel.

Your Task

Complete the functions in MineCart.sol as described in the ABI table above.

Run tests in Questplay

Submit work in Questplay

The miners are working again, but perhaps a bonus would encourage them to work harder…