Virtual channels: creating a state channel network

May 28, 2021

In this post, we describe a novel state channel construction called virtual channels that uniquely enables use cases like paid file torrenting (check out the demo!). Virtual channels can streamline decentralized Graph query payments, Filecoin content retrieval, incentivized state provider networks, and many other interesting use cases.

🥕 Motivation

Let’s design a trustless payment system for paid file torrenting. There are seeders and leechers. A leecher downloads small pieces of a file for a fee from many seeders. Using Ethereum mainnet transactions for payments is a non-starter. The mainnet Ethereum throughput is less than 50 transactions per second and the cost (at the time of publishing) is over $2 for the cheapest transfer. Optimistic and ZK Rollups increase throughput and decrease transaction costs. StarkEx ZK Rollup achieves a throughput of 3,000 transactions per second with a cost of $0.03 per transaction. If we assume that a leecher is willing to pay $1 for a 1 GB file and that a leecher downloads the file in 256 kB pieces, then the leecher makes around 5,000 payments. With an internet connection of 20 MB per second, the user makes 80 payments per second and each payment is $0.0002. Rollup throughput is insufficient for many leachers, and transaction costs are still much too high.

To meet these transaction throughput and cost demands, basic state channels are a good start. After a state channel is setup, individual incremental payments are free to make and throughput is only limited by the bandwidth of the communication channel between a leecher and a seeder and the hardware each uses. The challenge with a basic state channel is that a leecher needs to submit a mainnet transaction to set up a channel with each seeder it downloads from. A leecher has many short lived interactions with many seeders, so the channel setup cost is prohibitively expensive.

Virtual channels are a perfect solution to our design problem. Virtual channels are especially useful for hub-and-spoke topographies. Many participants connect to an untrusted intermediary. After that, any two (or more) participants can establish a private, staked channel. The intermediary is not aware of what application is run in the private channel. Moreover, any participant can recover their funds through on-chain challenges, even if all other participants and the intermediary hub in the channel go offline or act maliciously.

hub
Connecting parties with virtual channels.

In general, virtual channels enable Alice to open a channel with Bob given any network topography where there is a path through intermediaries between Alice and Bob.

📚 A bit of background

In the various blockchain ecosystems, state channels are known as a way for a few participants to:

  • Establish a relationship (aka channel) on-chain and deposit some funds.
  • Privately exchange messages that conditionally move funds between participants.
  • Close out the channel on-chain and pay out to each party.

At statechannels.org, we have previously described the fundamentals of how to construct a channel, and how to fund one channel via another channel with a construction called ledger channels. These constructions have the limitation that channel participants must establish a direct relationship on-chain. In other words, if Alice and Bob have never interacted before and now would like to open a channel, Alice and Bob must sign an (off-chain) agreement and deposit funds into a contract.

Enter virtual channels. Virtual state channels allow the following scenario:

  • Alice establishes a state channel with an untrusted intermediary. Let’s call that intermediary Irene.
  • Bob does the same.
  • With the help of Irene and using the channels created above, Alice and Bob can create a new private, funded channel. No on-chain interactions are needed to establish this channel. We call the channel between Alice and Bob a virtual channel.

For the paid torrenting scenario, Alice makes one mainnet transaction to establish a channel with intermediary Irene. She can then connect with any number of peers, “virtually”, as long as they also have a on-chain relationship with Irene. Once connected, she can stream payments in exchange for data.

Our protocol, called Nitro, enables the following use case:

  • The intermediary, Irene, that assists Alice and Bob in establishing a virtual channel, is not actually in the virtual channel. Irene simply helps to set the channel up and to settle the funds when the channel closes. This allows for a completely private exchange between Alice and Bob and removes Irene from the critical path of the application. But perhaps more importantly, Alice and Bob can run many different applications in the channel without requiring Irene to implement any new, application-specific logic. Virtual channels are thus generally programmable multi-hop state channels — similar to how Ethereum makes blockchains programmable — as each state channel can be created with its own “rules.” Alice can pay Bob funds in return for a some file content. Or Bob can make a paid a Graph query to Alice.
  • All Nitro channels are composable, or put another way, channels can recursively fund more channels, no matter how they’re funded. Once a channel is funded (directly via a chain contract, using a ledger channel, or via virtual funding), the channel can be used to run any application or fund any other channels.

🎨 Alternatives to Nitro

Other protocols (such as Raiden) allow two participants to establish a channel via an intermediary with no new on-chain deposits. A well-established pattern is to use hashed timelocks to route a payment from Alice to Bob through the intermediary. The clear downside of this setup is that all payments must be routed through an intermediary.

Some of the newer constructs, such as Scalar, allow for payments to be peer-to-peer after Alice and Bob set up the channel through an intermediary. However, these constructs still require the intermediary to understand the application that Alice and Bob are running as the final channel balances need to be redeemed through the intermediary. With Nitro virtual channels, the intermediary is not involved in the application channel between Alice and Bob in any way.

💰 Channel funding

This document is going to get deep into the weeds of how Nitro channels are funded. It has to to show you how trustless and secure virtual channels are possible!

A direct Nitro channel between Alice & Bob is funded by:

  1. Alice & Bob co-signing an off-chain agreement to start the channel with a specific initial outcome.
  2. Alice & Bob depositing in the order specified by the initial outcome, increasing the holdings of the channel recorded in the on-chain Adjudicator contract.

Outcomes are a structure that instruct the Adjudicator how to distribute funds when the channel is finalized (check out our latest update!). In Nitro, they are a prioritized list of (destination, amount) pairs:

{A: 7, B: 3} instructs the Adjudicator for a channel C to first pay out 7 to A (Alice), then pay 3 to B (Bob), and zero out the outcome of C.

This is a slight abuse of notation: we are assigning a priority to destinations based on the order of the keys in a dictionary. So, {B: 3, A: 7} would pay out to Bob first, then Alice. Think of this as an ordered dict a la Python 3.7+.

defund channel
The diagram shows how Alice and Bob can withdraw tokens from a channel C with an outcome {A: 7, B: 3}. The on-chain adjudicator records that the channel has 10 tokens deposited. Alice or Bob register the channel outcome on-chain. With this outcome recorded, they can then withdraw their tokens, leading to Alice's EOA increasing by 7 tokens, and Bob's increasing by 3.

The destinations in the above outcomes are implicitly EOAs. But in Nitro, channels themselves can be destinations. This allows for one channel, L, to serve as a “private ledger”, as it enables peers to fund many sub-channels by depositing once into a single ledger channel. We avoid the need, delay, and cost of going back to layer 1! An example:

If C2 is a Nitro channel, then {A: 4, B: 1, C2: 5} instructs the adjudicator to pay 4 to Alice, 1 to Bob, and increase the holdings of C2 by 5.

ledger channel
C1 is a ledger channel and C2 is a ledger-funded channel.

This is a very brief overview. For a more in depth discussion, please check out our blog post on the topic!

🔒 Guarantees: a new construct

Below, we demonstrate a safe construction for creating a 3-party channel when 2 participants have no relationship on-chain. This construction, in addition to ledger funding, enables virtual channels. Our participants are Alice (A), Bob (B) and Irene the intermediary (I).

naive guarantee
Our first attempt at connecting A, B, and I in a channel. Spoiler alert, not a trustless construction!

Initial setup: Three independent channels.

We start with a pair of ledger channels L (between Alice & Irene) and L' (between Bob & Irene). Typically, L & L' already exist. This is because Irene’s purpose is to connect folks virtually — Alice can use L to simultaneously connect to Bob, Cheryl, David, Eve. If L and L' do not exist, L is set up with the outcome of {A: 4, I: 6}, and L' is set up with the outcome of {B: 6, I: 4}. Each of L and L' have 10 tokens deposited on-chain.

Independently, channel J is set up with the outcome of {A: 4, B: 6, I: 10}. Note that participants must agree to the outcome before the channel is funded. The channel is funded later, in steps 1 and 2. Once J is funded, it can be used to fund an arbitrary, private application channel between Alice & Bob.

Steps 1 and 2: Ledger channels are switched to fund J.

Alice and Irene switch the outcome of L to {J: 10} . Bob and Irene switch the outcome of L' to {J: 10}.

Good enough?

Alice must take the following into consideration:

  1. Bob and the Irene are not to be trusted.
  2. Alice has no control over what happens in L'.

Let’s consider how J is defunded after step 2.

  1. L and L' outcomes are recorded on chain.
  2. L and L' funds are transferred to J. A transfer operation shifts tokens from one channel to another. J now has 20 tokens and is fully funded.
  3. Alice can withdraw 4 tokens, and Bob withdraw 6 tokens from J. Irene withdraws 10 tokens from J.

Nice! Everyone got what they deserved. But, let’s consider a scenario where Alice and Bob collude against Irene. Suppose step 1 takes place, but Bob refuses to participate in step 2. Then this may happen:

  1. The outcome of L is recorded on-chain. Now, J is funded with 10 tokens, with the recorded outcome {A: 4, B: 6, I: 10}.
  2. Alice and Bob withdraw 4 and 6 tokens respectively.

Notice that Bob received 6 tokens from J, even though none of his tokens are ever transferred to J. The net result is: Alice gets 4, Bob gets 6, and Irene gets nothing. Irene has been stiffed!

You might think that, if the order of the destinations in the outcome {A: 4, B: 6, I: 10} is switched around, then we can create a safe construction. Play around with the order, though, and you’ll see that there’s always someone who could lose out!

Guarantees to the rescue

In the above scenario, we used the transfer operation to move funds between channels. With a transfer operation, funds from one channel are simply moved to the target channel. In this section, we introduce a claim operation that allows for funds to flow through the target channel to specific destinations.1 To enable claims, we need guarantees.

Let’s introduce a new data structure. A guarantee is a type of outcome that specifies:

  1. A target, which is a channel.
  2. An amount.
  3. A priority, a prioritized list of destinations. The priorities serve to instruct the adjudicator on how to reprioritize the outcome items of the target channel.
guarantee
Our second attempt at connecting A, B, and I in a channel. This time, we have a trustless construction!

Let’s walk through how J is funded:

Initial Setup: Three independent channels are set up.

  • Channel J is set up with the outcome of {A: 4, B: 6, I: 10}.
  • L is set up with the outcome of {A: 4, I: 6}.
  • L' is set up with the outcome of {B: 6, I:4}.

Steps 1 and 2: Ledger channels are switched to fund J. These can happen in any order.

  1. The outcome of L is updated to {J: {amount: 10, priorities: [A, I]}. Note that we are using a new notation to show that L’s outcome has a single item, which is a guarantee with destination J, an amount and a priority list of addresses.
  2. The outcome of L' is {J: {amount: 10, priorities: [B, I]}.

The outcomes of L & L' now each contain a single guarantee. A transfer operation will not work with these guarantees. Instead, let’s introduce the claim operation. A claim operation accepts as input a guarantee and the channel the guarantee targets.

Let’s describe how a claim works. Suppose

  • J’s outcome, {A: 4, B: 6, I: 10} is registered on-chain.
  • L’s outcome, {J: {amount: 10, priorities: [A, I]}} is registered on-chain.

Then claiming the guarantee in L’s outcome has three effects:

  1. 4 tokens are sent to A, 6 tokens are sent to I.
  2. To account for (1), A and I balances are adjusted in J’s outcome to {B: 6, I: 4}.
  3. L’s outcome is adjusted to {} — the guarantee is removed.

The purpose of the priorities, in this operation, is to tell the adjudicator to skip over the outcome item with destination B, so that Irene can get her portion of the funds originating from L. So, the adjudicator looks at the highest priority destination in the guarantee, which is A, and transfers 4 tokens to A, adjusting J’s outcome accordingly. Then, it looks at the next highest priority destination, which is I. In J’s outcome, Irene should get 10 tokens, but there are only 6 tokens left to be claimed. Thus, 6 tokens are sent to Irene, and J’s outcome is again updated accordingly.

Note that it’s important for the priorities to be [A, I] and not [I, A]. If they were [I, A], then Irene would get 10 tokens out of the claim operation, leaving Alice no way to get the 4 tokens that she deserves!

With the claim operation, Alice and Bob can no longer collude to cheat the Irene out of funds.

🌴 Best case scenario

You might have noticed that for Alice to pull their funds out, 3 on-chain operations are required: outcomes of the joint channel and funding are registered on chain and claim is called. It is important to remember that this is a worst case scenario. Let’s say that Alice and Bob would like to close J after Bob has transferred 4 tokens to Alice. If Bob and Irene are cooperating, then:

  1. Alice, Bob, and Irene agree to a finalized J with outcome of {A: 8, B: 2, I: 10}.
  2. It is now safe for Alice and Irene to update L to remove the guarantee that funds J. The outcome of L is {A: 8, I: 5}.
  3. Alice can now use tokens in L to fund other channels. Or Alice can chose to withdraw the tokens.

The takeaway is that, in the collaborative case, Alice can use funds deposited to L to fund and defund many different application channels without any on-chain transaction.

🥅 Finally, virtual channels

In the previous section, we have established how to create a 3-party channel when two of the participants have no relationship on chain. A careful reader will note that any updates to J require a sign-off by the intermediary. At the beginning of this post, we set out to create a channel between A and B that is private. Fortunately, Nitro composability allows us to create a private application channel X funded by J. Below diagram shows the construction. You are armed with all the necessary concepts to understand this construction, but we welcome any and all questions!

virtual channel
X is a virtual channel.

⛵ Road ahead

The virtual channel construction described in this blog post is an evolved version of the construction described in the Nitro Paper. Most notably, this construction eliminates the need for dedicated guarantor channels.

Another exciting, in-progress update to Nitro virtual channels is eliminating the need for joint channels. With this update, the ledger channels L and L' fund the application channel X.

Written by Mike Kerzhner and Andrew Stewart, thanks to Robert Drost, Joseph Chow, George Knee, and Colin Kennedy for feedback. This post is based on the Nitro Paper by Tom Close.

Footnotes


  1. There is another way to think about the claim operation as a composition of:

    1. transfer from L to J that modified J’s outcome.
    2. transfer from J to its destinations.

    If ledger channel outcome is {J: {amount: 10, priorities: [A, I]} and J’s outcome is {A: 4, B: 6, I: 10}, then claim can be described as:

    1. A transfer of 10 from L to J.
    2. J’s outcome changes to {A: 4, I: 6, B: 6, I: 4}
    3. Then transfer is called for A and I on J.


Thanks for reading! If you have comments or questions, please join our discussion forum. We're also on Twitter @statechannels! All of our code is MIT licensed and available on GitHub.