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.
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:
- Alice & Bob co-signing an off-chain agreement to start the channel with a specific initial outcome.
- 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+.
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.
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).
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:
- Bob and the Irene are not to be trusted.
- Alice has no control over what happens in
L'
.
Let’s consider how J
is defunded after step 2.
L
andL'
outcomes are recorded on chain.L
andL'
funds are transferred toJ
. A transfer operation shifts tokens from one channel to another.J
now has 20 tokens and is fully funded.- Alice can withdraw 4 tokens, and Bob withdraw 6 tokens from
J
. Irene withdraws 10 tokens fromJ
.
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:
- 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}
. - 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:
- A target, which is a channel.
- An amount.
- 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.
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.
- The outcome of
L
is updated to{J: {amount: 10, priorities: [A, I]}
. Note that we are using a new notation to show thatL
’s outcome has a single item, which is a guarantee with destinationJ
, an amount and a priority list of addresses. - 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:
- 4 tokens are sent to
A
, 6 tokens are sent toI
. - To account for (1),
A
andI
balances are adjusted inJ
’s outcome to{B: 6, I: 4}
. 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:
- Alice, Bob, and Irene agree to a finalized
J
with outcome of{A: 8, B: 2, I: 10}
. - It is now safe for Alice and Irene to update
L
to remove the guarantee that funds J. The outcome ofL
is{A: 8, I: 5}
. - 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!
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
There is another way to think about the claim operation as a composition of:
- transfer from L to J that modified J’s outcome.
- transfer from J to its destinations.
If ledger channel outcome is
{J: {amount: 10, priorities: [A, I]}
andJ
’s outcome is{A: 4, B: 6, I: 10}
, then claim can be described as:- A transfer of 10 from
L
toJ
. J
’s outcome changes to{A: 4, I: 6, B: 6, I: 4}
- Then transfer is called for
A
andI
onJ
.