The Redemptive Greed That Will Drive Decentralization & Generalized State Channels Part 2
An In-Depth Look into the Growing Ethereum Scalability Toolset
Part 2: Continuing a look at why generalized state channels are going to help build the decentralized future and how to build them. Part 1 can be found, here.
Please note this is a thorough but not 100% complete or definitively secure methodology of how to build out generalized state channels. Code is not meant for immediate use. Also, libraries and tools used in this tutorial may have been updated since this post was published, please be mindful.
To continue from Part 1, having opened a state channel between Party A and Party B, we will be going over:
- Off-Chain Interaction Between State Channel Parties & Sub-Channels
- Closing a State Channel
- Handling Disputes
- Why: More thoughts on driving the ecosystem forward through state channels and other scaling solutions.
Off-Chain Interaction Between State Channel Parties & Sub-Channels
Having open a generalized state channel, now Party A and Party B must be able interact with and update the state in this channel. Recall the state that we used in opening the channel:
//32 increments in reference to bytes
bytes32 _state = keccak256(bytes_composition_of:
 bool isClose
 _uint256 sequence
 address partyA
 address partyB
 bytes32 counterfactualMetachannel
 bytes32 subChannelRootHash
 uint256 balancePartyA
 uint256 balancePartyB)
As discussed, state is agreed on through the use of digital signatures. So let’s suppose that Party A wants to update the state and send Party B 1 Ether, our new state would look like:
//32 increments in reference to bytes
bytes32 _state = keccak256(bytes_composition_of:
 bool isClose
 _uint256 sequence + 1
 address partyA
 address partyB
 bytes32 counterfactualMetachannel
 bytes32 subChannelRootHash
 uint256 balancePartyA - 1 ETH
 uint256 balancePartyB + 1 ETH)
Notice how the sequence is incremented by one and the balances of Party A and Party B are adjusted according to the value transfer. As before, Party A and Party B will sign the updated state. The state can be further updated by incrementing the sequence and balances accordingly — other elements should not change (i.e.,
At this point, the state does not require any on-chain interaction per se, unless we want a record of a state update on-chain. There will be a strong reliance on a front-end and back-end architecture to support the building of a state and storage of signature elements (i.e.,
s). As it stands, this is not unfamiliar in relation to how current decentralized applications work as developers are also the custodians in the formation of on-chain transactions for users as well. In the current design, it is only that far more information is stored and transactions executed on-chain than ultimately required.
Now, let’s say we have opened a generalized channel with 100 Ether. Being a widely used crypto-currency, let’s assume that in our model there are several different types of state channels for which a Party A and Party B could enter into: different games, different agreements, etc. Since we have locked away our Ether in the MultiSig contract, we actually have the chance to enter into a variety of sub-channels as well. We would only need to update our main state accordingly.
Prior to Ethereum, supply chain was my world, so to give a potential real world example, imagine your company had entered into a purchasing agreement with a supplier, and you estimate that your company will purchase one unit per day. Your payment terms are 30 days, but you need to have a quality certificate and be aware of the status of each item as it progresses through the supply chain up until delivery to you. The information required for tracking may look similar to the below:
"Purchase Order" SubChannel Example (in 32 byte increments)
 isClose bool
 sequence uint256
 timeout (seconds) uint256
 paymentChannelAddress address (of interpreter)
 ethChannelID bytes32 random hash
 MetaChannelCTFAddress bytes32 (counterfacutal)
 CTFRegistry address
 subchannelTxRootHash bytes32
 partyA address //Buyer in company (could be different departments or segments)
 partyB address //Supplier
 party A balance uint256 //To be paid to supplier upon delivery
 party B balance uint256
 qualityRecord bytes32 //Hash of quality report
 status uint256 // 1 — waiting, 2 — production, 3 — assembly, 4 — shipped, 5 — delivered, 6 — accepted
Instead of creating a purchasing order for each contract, we could open a larger generalized channel that holds a large amount Ether (or ERC20 token). For each purchase order, we could move a small amount of Ether to a subchannel for payment, and then at the end of the month, we could close out the larger channel for payment to the supplier. Granted this is not exactly how 30 day payment terms work, but the inefficiencies in current systems are truly unbelievable. Trust me, this would be a vast improvement — so long as we find solutions around credit as they are certainly needed.
To go through what this particular subchannel would include, we are familiar with the
timeout would refer to how long would we keep this channel open while a dispute is handled and then close it so each party could withdraw their funds. An example of a simple timeout limit on a payment channel can be seen through some tests here, using a contract example that I borrowed from here. Next, we have a
paymentChannelAddress, like the library extensions that we have to open our main state as mentioned previously, we will also have a library to handle the sub-channel as well. This sub-channel library will handle finalizing the sub-channel in case of a dispute. The sub-channel will not be opened on-chain in any manner — after finishing reviewing what’s in the channel, I’ll go over that. Next, we see a
bytes32 ethChannelID, this is a randomly generated identifier for the sub-channel. Again, we know what the
CTFRegistry are now.
subchannelTxRootHash, we have seen this before in our main state as well. What happens in here? In our main state, essentially, this element is used to track the sub-channel that we are talking about. In Spankchain’s version, you can see through the testing that the hash of sub-channel state is used to build a Merkle Tree which is then hashed. I dug deep enough to understand that after the state was hashed, using Node’s Buffer class, the hash was readied for use to build a Merkle Tree as in this respective Merkle Tree builder, which takes in an “array of hashed leaves” in which “each leaf must be a Buffer.” After building the Merkle Tree for the state of the sub-channel, the root is extracted to be stored in the main state again as a hashed bytes32 element. Why does the sub-channel include a
subchannelTxRootHash as well? Well, if we can open one sub-channel from the main channel, why not one day a sub-channel from within a sub-channel? I will not go that far in this example though.
The rest of the elements are fairly straightforward — Party A’s address, Party B’s address and their respective balances. For
bytes32 qualityRecord I imagined using the IPFS or Swarm hash of document for reference in a state channel. Then, imagine using a
uint status to denote the status of a certain order. All these things could be used to update a front-end and provide a user with information. This
sub-channel could ultimately employ some logic in its
finalize() function such as that a certain
status is met with the requirement of an available
qualityRecord. What’s more, recall that all of this state information is maintained off-chain enabling privacy. That is so key. In this way, state channels can achieve off-chain privacy with the benefit of on-chain value transfer and on-chain governance if needed. Also, with the state information being stored off-chain , it is far more easily available than it would be pulling the information from a smart contract.
To summarize, in order to open this sub-channel with 1 ETH from party A, states would need to be signed from both parties as the below:
Notice how the original balance in the main channel is adjusted along with the addition of the sub-channel root hash. The states would be further updated as the states changed.
Closing a State Channel
Under the assumption that there are no disputes to resolve within the current channel, it can actually be closed fairly easily.
Let’s say that we want to close this example state channel that we have been going through, and all sub-channels can be closed without dispute as well. Back to our MultiSig contract, we can use a
closeAgreement() function to close down our state.
Prior to the close, both Party A and Party B must sign the state with the
isClose boolean set to true. Using the
_isClose(_state), we verify that this is true, and we also verify the signatures as expected.
_finalizeAll is an internal function that iterates through the available extensions to ensure that the proper channel is properly closed.
Looking back to our extension library, through the use of
delegateCall() in the
_finalizeAll() internal function, we call
finalize() which sends the respective balance to each party from the MultiSig contract. More logic can be added here if applicable.
In this way, each party is sent their respective funds at the closing of the state channel.
Closing a state channel without a dispute was as simple as executing a transaction. The good news is to close out a state channel with a dispute, it is only a few more transactions; however, the design behind them is a little bit more complex. We know that if a Party A ever tries to submit a state that is not signed by Party B, then this state will not be counted as valid. For example, if Party A signed a state with
isClose as true, attempting to close out the channel and steal funds, without the signature from Party B, they will not be able to close out as the
closeAgreement() function above requires that both parties sign the state.
There are a few instances in which a dispute or issues can arise:
- Party A and Party B disagree on the final state and hence both decline to sign a state with
isCloseset to true, although both parties remain active.
- Party A or Party B “disappears” as in they are not in an active dispute, but for whatever reason become unavailable and thus cannot provide a signature, thus, locking the opposing party’s funds in the channel.
As discussed in Part 1, in the case of a dispute, the Registry contract would be used to deploy the MetaChannel contract via a
deployCTF() type function. Notice either party can deploy the MetaChannel contract — or even a third party.
Subsequently, let’s suppose we have a dispute on the outcome of one of our sub-channels, within the MetaChannel contract, a function as in the
startSettleStateSubchannel() will be called. Further below is a large chunk of code, but it is helpful to see the entirety of where all of these corresponding variables exist.
In looking at the inputs of
startSettleStateSubChannel(), first a
_proof is provided. The proof will be a composed state of the Merkle Root (recall that the Merkle Root will be in a Buffer form, so it needs to be converted back into a
bytes variable) of the sub-channel state that the user is trying to prove was the last true state. Next, we include a
_state, which would be the state of the main channel, and
_subchannel would be the the state of the sub-channel that the user would like to prove as true. The last inputs will be to prove the signatures of the latest
_state of the main channel — recall the main channel state holds the
subchannelroothash of last agreed sub-channel state as well.
So, we first check that the
_state submitted for the main channel has been signed. Then, using the
_subchannel state, we grab the
_channelID using an internal function
_getChannelID() with assembly. Again, using an internal function with assembly, we grab the hashed state of the sub-channel Merkle Root to store as the
stateRoot . Then, there are some basic checks to ensure that this specific sub-channel is not closed yet. By using its unique channel ID, a struct is referenced with the relevant information to ensure a) through
isSubClose that that sub-channel is not closed yet and b) through
isSubInSettlementState that the settlement has not already been initiated.
Next, we ensure that this challenger is actually attempting to settle this dispute with a sub-channel state has been agreed to by using
_isContained() to ensure that the proof provided has been included in a signed state. Next, a few pieces of information regarding the sub-channel and the dispute is saved including: the ‘interpreter’ address or the library address that is used to interpret the sub-channel state as the
CTFaddress, the sub-channel is now in the settlement state and is marked as true through
isSubInSettlementState. Recall, that in the sub-channel a
timeout was identified for the challenge period where both parties agreed to the amount of time that they would have to respond to a proposed challenge. This timeout end period is now recorded as the
subSettlementPeriodEnd. Then, the sub-channel state is saved as
subState, and the sequence of the sub-channel state is extracted and saved as the
subSequence. Lastly, the state of the main channel is saved as
Now that a challenge has been initiated, if the opposing party disagrees on the last signed state, they have the opportunity to submit a challenge through
challengeSettleStateSubchannel() . Why would a party initiate a challenge that could potentially be disproven? Imagine that Party A and Party B have been exchanging Ether back and forth in the sub-channel, say for 15 state changes. If Party A sees that state 10 is a more favorable outcome for them rather than state 15, Party A could refuse to sign a closed state and attempt to submit a challenge with state 10 to withdraw from the channel in their most favorable state. However, Party B would have the challenge period to submit a later state that Party A had signed and then close out the channel at the appropriate state.
A question that may arise is what happens if Party A submits a challenge while Party B is offline? What if the challenge period comes and goes without Party B having a chance to submit the correct state? Slashing conditions are one option that could come into play here; however, there is still the time issue of the challenge. This is also where services, such as Connext, may be employed so that guardians may “watch” channels in order to prevent such instances from happening. Recall, in this specific design discussed, that any party, A, B or other may initiate the MetaChannel, start the challenge and respond to a challenge. As long as Party A and Party B have signed states, a third party may act on their behalf — that is where Connext’s platform comes in with Ingrid, which listens for challenges and responds appropriately. It is a brilliant idea for a service to provide because individuals and organizations will not want to sit around watching their state channels. Furthermore, I would argue that it is not at all centralized as a service like Connext would be bound to the same smart contract logic as Party A or Party B would be bound to as well.
As mentioned above the
challengeSettleStateSubChannel() function would look nearly identical to the
startSettleStateSubChannel() function in which a party could attempt to provide a state signed by Party A and Party B at a later sequence. The challenge function would take in the same inputs and use similar checks. However, it would have the addition of checking whether or not the sub-channel state provided was at a higher sequence or not, as a higher sequence corresponds to a more up to date state.
require(_getSequence(_subchannel) > subChannels[_channelID].subSequence);
Next, since the sub-channel is now settling, the MultiSig contract will need to close out in a different manner as well because
isClose will not be signed as true now. The following type of function as the below
closeSubchannel() would be used in which the
_getInterpreter() function would get the interpreter from the state of the sub-channel saved in the MetaChannel. The
getSubChannel() function returns all elements of the sub-channel struct in MetaChannel, hence, the following syntax
(,,,,,,,,,,,_state) is used to only save the returned state without wasting additional memory to store the other variables.
Next, using the
_finalizeSubChannel() function a
delegatecall() is made to to the interpreter function
finalizeByzantine() function in the interpreter library, using the Registry, we get the deployed MetaChannel address. Following that, from gathering the total amount of value stored in the state, the total amount of value stored in the MultiSig contract is transferred to the MetaChannel contract. Recall that the ultimate value settlement is stored in the MultiSig state, so in order to settle the sub-channel and hence the main channel, we will need to transfer all funds to the MetaChannel for a final closeout.
This final close out will be called in the MetaChannel contract with a function like
closeWithTimeoutSubchannel() after the settlement period ends. Then, the
finalizeState() function is called using the interpreter library with the latest validated state from the challenge. Recall that this is the same
finalizeState() function used during our close out with no disputes as now that Party A and Party B have settled on a final state, then the funds held now in MetaChannel can be reallocated or returned to Party A and B in a similar fashion. The sub-channel struct is closed out by setting
isSubClose to true,
isSubInSettlementState to false, and
settledAt to now.
And there you have it, settling a dispute (and not a single judge or juror in sight).
At this point, we have gone through how to open a generalized state channel, how users would interact with it and with subchannels off-chain, and how the channel would be closed, both in the case of no disputes and in the case of some disputes.
Why Scaling and State Channels? A Few More Thoughts
Coming this far, we have seen how state channels help to significantly reduce gas costs and increase privacy while facilitating similar, if not identical results, for transactions that today are regularly occurring on-chain.
I recently listened to a16z’s podcast on Network Effects with W. Brian Arthur in which he explains that network effects “exist when the “value” of a format or system depends on the number of users.” First off, did you know that Arthur first submitted his paper in 1983, and it was not published until six years later? Amazing conviction in his theory. Anyway, we know that Amazon, Google, Facebook, Instagram and more have grown value through accumulating users first, not value, which came later through scale, data and advertising.
So, once more, the world has been completely flipped on its ends. The Ethereum network holds a tremendous amount of value already in its infancy through the existence of Ether and other tokens, but what it does not have is a tremendous amount of users because, as it stands, it has not scaled to accommodate or entice those users. I know that’s an affront to many working in the Ethereum community — the user base and developer community is growing, but let’s also be realistic, the numbers pale in comparison on a global scale.
For example, user wise Google has over a billion monthly active users, Ethereum has only 35 million unique addresses. For a smaller comparison, Craig’s List, with a market cap of $3 billion, has 60 million active users. These are not a perfect comparisons, and they are not meant to be — Google, Craig’s List and Ethereum are obviously very different. Also, I am not accounting for private chains of Ethereum. What I am trying to emphasize is scale. The Ethereum network holds more value than companies like Google or Craigsist even dreamed of having in the first few years, but the Ethereum network has less users than those companies probably ever imagined having at that value, too. Ethereum is not a business, but its value as a network (not saying Ether here) will ultimately be driven in a similar way: by users. However, in this situation, as solutions have already scaled under a different infrastructure, Ethereum has to scale first as a solution to get the users.
Accordingly, founder of Connext, Arjun Bhuptani, said “scaling is normally an issue during the growth stage, now the issues are at the beginning.” In building on Ethereum, the arrival of scaling challenges is completely flipped on its head from traditional Web 2.0 applications as we know Web 2.0 can scale, albeit with a centralized design.
In Ethereum, users may be favorable to the decentralized design but we need to make it scale. Otherwise, as discussed in the beginning, greed will pull the world towards the ideology, or infrastructure in this case, in which the individual most benefits.
For need of any further proof, read Michael Lewis’ Flash Boys, which chronicles the length to which financial firms will go to for nanoseconds of improved speed of transactions, which in turn translates into transaction volume. Taken from the book, Lewis explains:
“The U.S. stock market was now a class system, rooted in speed, of haves and have-nots. The haves paid for nanoseconds; the have-nots had no idea that a nanosecond had value.”
Michael Lewis, Flash Boys
How great would it be to scale Ethereum and break down barriers to the point in which the above can be rewritten?
“The world was now a equal playing field, rooted in decentralized governance, of those who are aware and those who are not. The awares contribute and participate; the unawares have no idea of their invitation to.”
If you made it this far, thanks for reading. I hope this resource helps you on the road to scaling the decentralized future.
Disclosure & Thank You’s
I work for Token Foundry as a Solidity Developer, where we are actively preparing for the scaled future of Ethereum. Thanks to SpankChain and Connext for allowing me to write about their content in depth. Also, thanks to Alice Henshaw, without whom these articles may have been a flood of terrible grammatical errors — with edits by Steven McKie