An Approach for Turing-Complete State Channels – Part 1
State Channels have been intriguing the crypto-community since their initial conception, and have been an active topic of discussion for many years.
The process of two or more parties committing an initial state on-chain, transacting off-chain using a sequence of signed messages, and committing the final result back to the chain using a second on-chain transaction sounds like a great solution to the inherent scaling issues of today’s blockchain implementations.
Much of the discussion so far, though, has been around the simple process of transferring tokens between parties, but this concept can do so much more, and so let’s explore an approach to the implementation of Turing-powerful logic in a State Channel.
For the purposes of this note, I’ve narrowed the scope to a 1-to-1 channel in which each party takes turns to act, but the concepts can be extended to a broader range of use cases.
First up, let’s recap on what a State Machine is. A State Machine is a very simple concept that underpins a vast amount of the world’s software in its various forms, and can be summarised thus:
f(state, action) => state’
ie it is a function that takes an existing state and an action (a way it should be modified), then applies the action to the state and returns a new state. This is one of the core concepts that underpins most traditional video games (my personal background prior to crypto) – and the action can be anything from the user pressing a button, a network packet arriving, or (most commonly in games) time passing (hopefully at a smooth 60 frames per second!).
For the rest of this note I’m going to use a worked example: Chess. Chess is a 1-1 game, players take turns, and the rules are clearly defined so it’s a natural fit. A State Machine that implemented chess would look like this:
The State: The current positions of the pieces on the board, whose turn it is, and whether the board is in check or checkmate (I’m going to ignore Resignation and Stalemate for this note for simplicity).
Actions: A move that specifies a piece and a target square to move it to.
The State Machine needs to do two things when it’s called:
- Validate that the action is valid:
- Is it the turn of the player who’s posted the action?
- Is the piece the player is trying to move legally allowed to move to the target square (including the restrictions of being in Check etc)
- Is the game not yet finished by a prior Checkmate
- Update the state
- Move the piece to the new square
- Remove any taken pieces from the board
- Check for Check and Checkmate
- Update whose turn it is
And with that you have the ability to play chess, by calling the function repeatedly by alternating players and passing in the previous state each time.
Can we write this function in (say) Solidity? Certainly we can, but keeping the state on the chain and calling the contract code directly to update the state for each turn will result in a slow and expensive user experience, and this is where State Channels can help!
Let’s look at a first-pass attempt at implementing this in real world – and let’s give it a reason for existing on the chain in the first place – competition between two players for something of value. We can open a channel simply enough by writing a contract that takes funds from both players and locks them until the channel is closed. But what else do we need to do?
First, we need to create an application for each player to use. Let’s assume that it’s web-based, uses web3 to interact with the chain, uses some unspecified point-to-point mechanism to communicate directly between the two player applications, and has some nice UI for the players to interact with it.
At this point, if we implement the State Machine function within the browser app, then two players could play a game of chess by each player in turn choosing a move, updating the state using the State Machine, and passing the state over to the other player. Without any checking, though, each player could trivially cheat. So, progressively, here’s what is added to make the game fair:
- Instead of passing the new state, the acting player passes both the action and the new state. The passive player also runs the State Machine against their local copy of the previous state, and sees if the result is the same as the new state. If not, the player has tried to cheat
- The active player signs the message containing their action and the new state with their private key. Now if the second player detects that the state transition is invalid, they can prove that the active player was trying to cheat
- The active player co-signs the previous message from the second player (which they’ve signed in step 2 above) and sends it along with the new move state. This is really important, as it sets a base-line for where both players agree.
- Each time a signed message is sent, the signatures are checked by the other party. An invalid signature is rejected as an ill-formed message rather than an attempt to cheat (as we need a valid signature to prove this)
At the point at which the game is over the final state is signed by both parties, and we have a game of chess where every move has been validated and agreed by both players.
So – how do we put this on-chain in a State Channel? From an engineering point-of-view it’s relatively simple – we create an on-chain CloseChannel() function in the State Channel contract which takes a signed state from both players, verifies that the signatures are valid, verifies that the signed state of the game is in Checkmate, and transfers the locked assets to the winner. Great!
But… have we created a provably-fair, distributed, autonomous game at this point? No! For two reasons:
- We haven’t worked out what to do when one player tries to cheat, and
- The actual rules of the game are embedded in the client application, which, depending on how it’s hosted, could be changed/rigged maliciously.
- Additionally – how can we guarantee that both players are actually using the same State Machine?
And so we finally get to the point of the note, and the solution it describes!
As discussed above, we can encode the rules of Chess on the blockchain, but we don’t really want to play a game using on-chain transactions for each move. However, there’s no reason that the chain itself can’t be an authority on the rules of the game – and, much more importantly, an authority on the code that implements the rules of the game. If we create the State Machine function in Solidity and publish it on-chain, it can be independently verified as being correct in advance, and referenced by anyone.
More importantly, though, it can be executed off-chain, which is orders of magnitude faster and cheaper than doing it on chain. (A note: the State Machine function itself in this form has no side effects, so it is a constant function in effect)
So – if, in our worked example above, we replace the local, client-side State Machine implementation with an off-chain execution of the on-chain code, both parties can feel a lot more confident about the code that’s being run, and that what they’re signing is correct.
But the best bit is that we now have a solution for a player who tries to cheat. If they send a signed message that includes a state transition which is invalid – as in it doesn’t follow the rules of the on-chain code – we can do something about it! The receiving player can call an on-chain dispute resolution function, passing in the signed but invalid state transition, this function can call the *same* State Machine code on-chain and demonstrate that it’s invalid, and the cheating player can be penalised.
So – by this approach, we can construct a wide variety of state-machines for all sorts of purposes, which can be advanced off-chain in a provably verifiable way using committed on-chain logic, and is simply verifiable on-chain in the case of dispute.
This methodology imposes no restriction on the complexity of the State Machine, the state or the actions which transition it, other than what is capable of being executed on the chain – so it is as Turing-Complete as the chain on which it is implemented.
In Part two, I plan to discuss:
- How, at FunFair, we’ve actually implemented a real-world solution based on this concept, and some of the challenges we’ve faced
- How we’ve injected randomness into the process
- How to deal with some more malicious cases of attempting to cheat (a trivial example in the Chess example is a player who realises he’s about to lose simply doing nothing)
- A discussion on fixed-odds gambling games, where the rules are different for each participant and the differences between inter- and intra- round state for a sequence of games being played in the same State Channel
- How to scale this approach for hundreds of different games with different rules
Jeremy Longley, CTO of FunFair – 15 December 2017