A Generic Payment Channel Construction and Its Composability

Channel networks are probably the most fascinating players in the layer-2 arena. A widely deployed channel network can maximize the throughput (unlimited!) and minimize the latency (as fast as network connection between parties) of transaction processing, enhance transaction privacy, and can even provide certain interoperability for blockchains. In short, channel network is the silver bullet if done right.

Unlike protocols that based on zero-knowledge proof or layer 2 chains, the basic idea of channel network is fairly simple: use direct network connections between two parties to exchange off-chain transactions and make sure the effect of those transactions are enforceable on blockchain, then fabricate those peer-to-peer connections into a network so there is always one or multiple routes between any two parties in the network. Although channel networks have their own headaches such as liquidity efficiency and routing problem, those are either plausibly solvable or have approximate optimal solutions.

Compare to layer 1, layer 2 protocols are relatively easy to upgrade once deployed, so the best way to make a channel network happen is to kick it off and evolve it piece by piece. Lightning network is an exemplary project walking in that way. This post demostrates a generic payment channel construction on CKB that is simple and excels at composability, with the hope to attract more research attention, spawn more discussions on channel networks and facilitate the building of channel networks on CKB.

Introduction

Payment channel network (PCN) is a decentralized payment network to support global payments. The design of a channel network is mainly about 1. channel protocol which specifies the off-chain messaging, on-chain contracts and on-chain off-chain interactions in a direct channel between two users, and 2. networking protocol which specifies how to combine multiple direct channels into a longer channel, how to find a route between two users etc… This post discusses a generic payment channel (GPC for short) construction on CKB. It’s generic because it works with basically any assets on CKB.

The main question in channel construction is how to ensure that only the latest channel state is considered valid on-chain. The Poon-Dryja construction used in Lightning network achieves that with punishment-based revocation, which incurs complex key and state management to both parties, make it harder to implement the protocol correctly and increases the risk in node operation. The Decker-Wattenhofer construction gives a timelock-based state update mechanism, with the drawback that only a limited number of state updates is allowed, which means higher costs for both layer 1 and users because channels are forced to close and open periodically. The Decker-Russell-Osuntokun construction ‘eltoo’ is an elegant version-based (‘Replace by Version’) solution however it requires the introduction of a new opcode SIGHASH_NOINPUT into Bitcoin. Ethereum provides a stateful turing-complete smart contract model which makes building version-based scheme an easier task, as in Sprites, ForceMove and Perun.

Construction

The GPC construction described here is basically an eltoo port on CKB. However, we’ll see the almost identical idea is not only easier to implement on CKB, but also displays merits such as composability that don’t exist in Bitcoin/Ethereum payment channels, with just a little bit discretion on design. Channel composability is channel construction’s ability to work with other assets and dapps on layer 1. With GPC’s composability, a generic payment channel network (GPCN) could be built for all layer-1 assets.

The GPC construction consists of a GPC lock and 3 types of transactions.

GPC lock

GPC lock is the key ingredient of a GPC channel, it is the lock script used for cells in channel. It uses 5 arguments:

  1. state: lock state, one of OPEN and CLOSING,
  2. timeout: closing period length,
  3. pubkey_a: Alice’s public key,
  4. pubkey_b: Bob’s public key,
  5. nonce: a monotonous increasing unsigned integer.

A GPC channel can be modeled as a simple finite state machine with 3 states: OPEN, CLOSING and SETTLED. OPEN is the initial state. OPEN and CLOSING is represented by GPC lock with OPEN and CLOSING state respectively, while SETTLED is represented by the absence of GPC lock.

A state transition happens when the GPC lock is unlocked. A GPC lock in state OPEN can be unlocked in one of two conditions, bilateral-close and unilateral-close: (feel free to skip these conditions and come back when you read the ‘Close a channel’ section):

  • condition 0 bilateral-close, unlock if its consuming transaction is a settlement transaction satisfying:
    • has at least 1 input:
      • input 0: refers to the output with GPC lock, and its lock state must be OPEN,
      • witness 0: must be in the form 0 || sig_1 || sig_2 where:
        • 0 represents condition 0,
        • sig_1 is Alice’s (or Bob’s) signature of the consuming transaction hash,
        • sig_2 is Bob’s (or Alice’s) signature of the consuming transaction hash,
    • has exactly 2 outputs:
      • output 0: Alice’s output, must use the same type script as input 0,
      • output 1: Bob’s output, must use the same type script as input 0.
  • condition 1 unilateral-close, unlock if its consuming transaction is a closing transaction satisfying:
    • has at least 1 input:
      • input 0: refers to the output with GPC lock,
      • witness 0: 1 || sig_1 || sig_2 where:
        • 1 represents condition 1,
        • sig_1 is Alice’s (or Bob’s) signature of the closing transaction’s no-input hash (more on this later) ,
        • sig_2 is Bob’s (or Alice’s) signature of the closing transaction’s no-input hash,
    • has exactly 1 output:
      • output 0: the same as consumed output (refers to by input 0) except:
        • its GPC lock nonce must be larger than the one in the GPC lock of input 0,
        • its GPC lock state must be CLOSING.

A GPC lock in state CLOSING can be unlocked in one of two conditions, timeout and unilateral-close:

  • condition 0 timeout, unlock if the consuming transaction is a settlement transaction satisfying:
    • has at least 1 input:
      • input 0: refers to the output with GPC lock, and its lock state must be CLOSING,
      • witness 0: 0 || sig_1 || sig_2 where:
        • 0 represents condition 0,
        • sig_1 is Alice’s (or Bob’s) signature of the consuming transaction hash,
        • sig_2 is Bob’s (or Alice’s) signature of the consuming transaction hash,
    • has exactly 2 outputs:
      • output 0: Alice’s output,
        • must use the same type script as input 0,
        • must use the default secp256k1_blake160 lock using pubkey_a in input 0,
      • output 1: Bob’s output.
        • must use the same type script as input 0,
        • must use the default secp256k1_blake160 lock using pubkey_b in input 0,
  • condition 1 unilateral-close , the same as unilateral-close condition for OPEN state.

A relative timelock is set on input 0 in condition 0 to make sure the settlement transaction can only be included in blockchain after timeout blocks.

Transaction Types

Funding transaction - A funding transaction opens a new GPC channel. It has one funding output, whose lock is a GPC lock in state OPEN.

Closing transaction - A closing transaction consumes the funding output or the output in another closing transaction. It has one closing output, whose lock is a GPC lock in state CLOSING. It always has a matching settlement transaction.

The unique feature of closing transactions is they are not fixed to a single previous outpoint. When a closing transaction is signed, its inputs is excluded from hashing so the signature covers only outputs. This special hashing is called no-input hash. E.g. when a user wants to sign a closing transaction, he/she serializes outputs and hash them, then sign on the result hash. This way inputs in closing transaction doesn’t affect the exchanged signature. Because of this a closing transaction can be attached to a funding transaction or another closing transaction, and the link can be decided at a later time after the closing transaction is signed. This behavior can be implemented on CKB without adding a new opcode because the hashing and signing of a CKB transaction is completely customizable.

The GPC lock condition 1 (unilateral-close), which requires the output 0 of the closing transaction must be the same as its input 0 except nonce and state in their lock, implies that its input 0 can only attach to a previous output using a similar GPC lock as its own output 0 (with the same timeout, pubkey_a and pubkey_b).

Settlement transaction - A settlement transaction consumes a closing output and closes a GPC channel. It has two outputs that distributes funds to Alice and Bob. It always has a matching closing transaction.

Example Flow

Here I give an example interaction flow. It’s only a sketch protocol for the purpose of explaination, not the exact steps of a real protocol. A key difference between the sketch and a real protocol is how to reach agreement on each state transition, i.e. each closing/settlement transaction pair. Two-party agreement problem is trickier than it may sound, considering issues like concurrency and fair exchange. Solutions do exist but they are out of the scope of this post, and it’s orthogonal to the GPC construction. I’ll just assume such an appropriate secure protocol is used when necessary, e.g. in signature exchange.

Open a channel

Let’s say Alice wants to open a new GPC channel with Bob:

  1. Alice initiates a request to Bob to negotiate channel parameters including funding inputs, timeout, pubkeys etc.
  2. Alice and Bob builds a funding transaction locally using pre-negotiated parameters, with inputs from both party and a funding output:
    1. output 0: the funding output locked by a GPC lock:
      1. state: OPEN,
      2. timeout: a negotiated value T,
      3. pubkey_a: Alice’s public key,
      4. pubkey_b: Bob’s public key,
      5. nonce: 0.
  3. Alice and Bob builds the first pair of closing transaction and settlement transaction:
    1. closing transaction ctx_0 which consumes the output 0 of funding transaction by condition 1 unilateral-close.
    2. settlement transaction stx_0 which consumes the output 0 of closing transaction ctx_0 by condition 0 timeout. The outputs of stx_0 represent initial balances of Alice and Bob.
  4. Alice and Bob exchange signatures of stx_0 and ctx_0.
  5. Bob signs funding transaction and sends the signature to Alice.
  6. Alice broadcasts the signed funding transaction, once it’s included in blockchain Alice and Bob consider the new channel opened.

The channel id can be defined as H(funding_transaction_hash || 0). If the funding output has its type script set, the channel is considered to be an UDT channel, otherwise a CKByte channel.

The first pair of closing and settlement transaction is created and signed before funding transaction to ensure the initial funds won’t lock in channel forever. If either party become unresponsive after channel open the other can always claim his/her funds back by close the channel unilaterally.

Make a payment

To make a payment Alice/Bob simply exchange signatures on a new pair of closing and settlement transactions. Payments are bi-directional.

Let’s say the latest agreed pair is ctx_i and stx_i. ctx_i is a closing transaction whose GPC lock nonce is i. stx_i is a settlement transaction consuming the output 0 of ctx_i. The two outputs of stx_i are A UDTs and B UDTs, which are the latest balances of Alice and Bob respectively. Assume Alice wants to pay Bob X UDTs:

  1. Alice builds a new pair ctx_j and stx_j where:
    1. j = i + 1,
    2. closing transaction ctx_j consumes the output 0 of funding transaction by condition 1 unilateral-close.
    3. settlement transaction stx_j consumes the output 0 of closing transaction ctx_j by condition 0 timeout. The outputs of stx_j are A-X and B+X UDTs respectively.
  2. Alice sents ctx_j and stx_j to Bob, Bob verifies their validity.
  3. Alice and Bob exchange signatures of ctx_j and stx_j.

Bob verifies the validity of the new transaction pair by 1. make sure they’re valid CKB transactions, e.g. lock/type script can successfully evaluate, and 2. the outputs of stx_j reflects new balances correctly. The same verification rules in type scripts are reused here to ensure each state transition is allowed, and the examination on settlement transaction outputs ensures the state transition is intended.

Each pair of closing and settlement transaction represents a new channel state. Alice and Bob reach agreement on a new state by exchanging signatures of them, as in step 3. A new pair can only be considered valid when he/she knows both parties have all signatures.

Close a channel

A GPC channel can be closed in 3 ways.

The Good

Alice and Bob are both online and they collaboratively close the channel by signing a settlement transaction. Assume the lastest agreed closing and settlement transaction pair is ctx_i and stx_i, and Alice initiates a bilateral close:

  1. Alice build a settlement transaction stx_b (‘b’ for bilateral) as specified in the condition 0 bilateral-close of OPEN lock. The outputs of stx_b use the same data and type as the outputs of stx_i.
  2. Alice sets the lock of output 0 to her choice and sends stx_b to Bob.
  3. Bob verifies the outputs of stx_b have the same data and type as the outputs of stx_i. If they match Bob sets the lock of output 1 to his choice, signs the transaction and send the signature sig_b to Alice.
  4. Alice verifies sig_b, signs the transaction to get sig_a and set witness 0 to 0 || sig_b || sig_a.
  5. Alice broadcast the settlement transaction.

Once the settlement transaction is included in blockchain both parties deem the channel closed. In this optimal path parties can receive their funds immediately at any addresses they specified.

The Bad

Alice wants to close the channel but Bob is unresponsive (maybe he has unpaid VPS bills and the service provider is not very tolerant). Alice can simply use the latest agreed closing transaction ctx_i to initiate a unilateral-close and close the channel after T blocks (as specified by timeout):

  1. Alice sets the input 0 of ctx_i to funding output.
  2. Alice broadcasts the closing transaction ctx_i (which is the closing transaction with largest nonce).
  3. Wait for T blocks.
  4. Alice broadcast the settlement transaction stx_i.

Once the the closing transaction is on chain the channel (as well as its GPC lock) is in CLOSING state. The CLOSING period will last T blocks (as specified by timeout in GPC lock) within which the other party can respond by submitting any ctx_j in his/her hand where j > i. In this case we assume Alice is honest and ctx_i is indeed the latest closing transaction, so no one is able to submit a closing transaction with a larger nonce to consume the closing output. After T blocks Alice broadcast stx_i to close the channel. Both parties will receive their funds at pre-defined addresses.

The Ugly

Suppose the latest closing transaction is ctx_i but Bob intentionally initiates a unilateral-close by sending an obsolete closing transaction ctx_j where j < i. Once Alice observed the closing transaction ctx_j she must respond with another closing transaction whose nonce is larger (in this case ctx_i) to consume the output of ctx_j before closing period timeout T. Once ctx_i is on chain, the closing period is extended another T blocks. If there’s a ctx_k with k > i it can be attached to ctx_i to update the on-chain channel state again, otherwise Alice could broadcast stx_i after timeout T. Assume Alice is honest and ctx_i is indeed the latest agreed state, then the whole process looks like:

  1. Bob broadcasts ctx_j and it gets included.
  2. Alice sees ctx_j, she immediately sets the input 0 in ctx_i to the output 0 of ctx_j and broadcasts ctx_i.
  3. Alice waits T blocks.
  4. Alice broadcasts stx_i.

The process could be longer if Bob broadcasts more obsolete closing transactions, e.g. ctx_{j+1}, ctx_{j+2} etc., on chain to prevent Alice’s attempt of closing with ctx_i / stx_i. However since ctx_i is the one with largest nonce it will be accepted eventually, after Bob run out of all obsolete transactions. In the worst case Alice needs to wait O(i*T) blocks to get her funds back, however the transaction fee would still be O(1) for Alice but O(i) for Bob, make such an attack uneconomical.

Transaction Fee

More inputs can be added to closing and settlement transactions which allows the participants to determine transaction fee at broadcasting time.

Rationale

Before the funding transaction is signed, either party can simply stop without any loss if he/she doesn’t want to proceed. If the funding transaction is signed but the initiator of channel openning doesn’t broadcast it, the other party can always send another transaction consuming the same inputs in the pending funding transaction to invalidate it.

Once the funding transaction is on chain and the channel is open, either party of the channel can always initiate a unilaterl-close to close it all by himself/herself as specified in “Closing a channel: The Bad”. If an adversary initiates a unilateral close, the party can either wait closing timeout or update the GPC lock if he/she has a balance object with larger nonce. The closing period provides a window for either party to submit his/her version of latest state, and at the end the channel will be settled on the state with the largest nonce.

Composability

The GPC construction can work with any tokens on CKB, no matter it’s the native CKByte or an UDT. The construction touches cell’s lock only, left their data and type fields at smart contract developer’s disposal. The small interface makes it compatible with any contracts using only data and type. sUDT is such a category of contracts and it’s plausible future UDT standards will be too. An UDT developer doesn’t need to consider how to channelize UDTs, because it can always be channelized without any modification. That means UDTs are born to be channelizable and the development cost of channelizing and scaling an UDT is ZERO. So UDTs are not only first-class assets on layer 1, they’re also first-class assets on layer 2, because they can flow in and out between layers without any modification. That gives a PCN maximum liquidity because all assets on layer 1 can join the layer 2 pool without any friction.

This is quite different from payment channels on Bitcoin and Ethereum. Payment channels on Bitcoin only works with Bitcoin because it is the only asset and the blockchain’s ability is limited. On Ethereum there’re many assets however the composability between payment channels and ERC tokens is restricted. Payment channels have to interact with token through pre-defined interfaces which could be different for different tokens. For example, Celer just hard coded support for ETH and ERC20 tokens and relies on its approve/transferFrom mechanism. Even the ERC token follows a standard with compliant interface it’s still insecure to channelize it because the interface semantic may be different, as shown in the recent Uniswap + ERC777 incidents. In contrast the GPC lock doesn’t care about how the UDT is implemented at all.

The GPC construction also works with anyone-can-pay lock as the closing transaction can use arbitrary lock for its outputs. The composability between GPC and UDTs makes channels more convenient for users to use, e.g. they don’t need to ‘approve’ an allowance before open a channel. Their funds can just securely sit there in wallets as usual and be sent into channels whenever they want with a single transaction.

The construction’s composability could enable interesting use cases on CKB, e.g. channelized stable coins. No matter it’s a algorithmic or trust-based stable coin, it is a first-class citizen of the payment channel network from day one. A payment channel network of stable coins could bring users an experience beyond Visa, Paypal and Alipay, because it’s fast, scalable, permissionless and future proof - I’m talking about streaming payment for APIs and IoT!

Dual funding

Ethereum payment/state channels also face an atomicity problem on channel openning because Ethereum transaction has only one sender but channel funds can come from both parties. To allow dual funding Ethereum channels can either use two separate funding transactions which would bring in counterparty risk because one party can abort after the other committed funds, or mimic atomic dual funding by using more on-chain transactions which increases the cost to open a new channel and again depends on specific interface of ERC20. The funding transaction of GPC can have multiple inputs so dual funding is supported naturally.

Stateful lock pattern

The construction depends on a lock script solely. The lock script is stateful, it models a simple state machine and use args in lock script (instead of cell data) to keep the internal states. The pattern can be applied elsewhere when a moderately complex stateful lock is needed.

For locks with more complidated state machine, two alternative approaches could be considered. The first is to make data a shared field between lock and type scripts, that requires predefined data format so each script can read/write its own data without interering with others. The second is using a separate cell to keep lock state and transform the locked cell and lock state cell together. These two are the most obvious but definitely not an exhausted list of all possible patterns.

Future Work

GPC is a simple construction inspired by eltoo, however it’s not just easier to implement on CKB but also get extra nice properties for free as discussed above. The “state object” essense of cells fits channels and 2nd layer naturally, and I hope channel network research can attract more attetions from our community. Here’re some interesting problems:

  • State channel - State channels are a generalization of payment channel to support off-chain execution of smart contract. The GPC lock contains no payment specific structs and it’s a decent start for state channel constructions.
  • Channel network - How to connect GPC channels so anyone can pay anyone in the channel network either directly or indirectly through some intermediary nodes? Do we use HTLC as in Lightning or there’s better solution?
  • Watchtower - How to delegate dispute to thrid party in a trustless way so channel participants can be off-line and go vacation?
  • Routing - How to find the best path between two nodes in the channel network? As the open/close and capacity variation of channels, the network is in constant changing, thus nodes have only partial information about the network topology. The definition of “best” also varies for different users in different scenarios. Due to these limitations developing several different routing strategies and leave the choice to users may be more reasonable. There’re many interesting papers in this area.
  • Multipart payment - The larger a payment is the less likely a path with enough capacity can be found to deliver it. The idea of multipart payment is to divide the large payment into several small payments and route them in separate paths to the same destination, and make sure the small payments are either all accepeted or rejected.
16 Likes

That’s unique! Actually, all assets and states described by type script on CKB will automatically get the ability to access the channel network as soon as this job has finished. It will bring brand new applications to blockchain world, with unlimited throughput and instant response.

One question about composability. Is it possible to execute some state generators in the channel? For example, Alice and Bob has created a channel with some cells constrained by specific type script, and they exchanges their signature on state transition (changes of cells’ data). The state transition logics (generator) are defined by the channel application, and will be verifed by on-chain type script. Payment is one of the rules, we could also deploy other rules off-chain. If this process is feasible, I think when we build a “generalized Bitcoin” with cell model, we have also built a generalized Channel.

3 Likes

Absolutely! That’s what state channels are about - execute smart contracts in an off-chain manner. If you’re interested, ForceMove is a good start. I also recommend the Sprites and General State Channel Network paper.

1 Like

此处的细节应该存在差异吧? input 0 : refers to the output with GPC lock, and its lock state must be CLOSING ,

Yes, thanks for pointing this out. Fixed by removing the OPEN state requirement in condition 1.

Debug: input 0 应该为 ctx_j ,output 0 为 ctx_i ,此处写反了。

It seems not a bug.ctxi is latest tx,so its output can’t be the input for the past tx.

对,是我英文水平有限,给理解错了 :rofl: ctx_i的上链交易构造:
input 0 设定为ctx_j的output 0,即链上的最新gpc_lock_cell状态, output 0还是原来链下通道里签署好的ctx_i的output 0,然后把ctx_i广播上链。 多谢williams指正,又加深理解了。

1 Like

客气了 我也刚好在看这篇文章,刚好再烧一点脑来思考一下你的留言