Skip to content

On-chain

Operating a state channel requires, an initial on-chain setup via a channel_create_tx transaction, that locks up a configurable amount of coins from each involved party in the channel. This requires a consent, in the form of either signatures or generalized account authentication call data, from both participants. More information regarding authentication can be found here

In the ideal case, all on-chain transactions get authenticated by both participants, implying that there are no disagreements. These operations can be committed immediately. There are also some unilateral transactions that need to be authenticated just by one of the participants. Those might have a block height timer attached to them for the other party to dispute. The mutually agreed ones will override all conflicting unilateral actions.

Transactions not authenticated by everyone can be disputed via channel_slash_tx, channel_snapshot_solo_tx and channel_force_progress_tx transactions. During normal operation, these solo transactions can be disputed indefinitely. If the channel is in the closing state, disputes have to happen within the pre-negotiated lock_period, given that closure requires a bounded finality.

Each channel transaction updating on-chain state provides two fields essential for future conflict resolution: round and state_hash. The state hash is the root hash of the channel's state tree after the on-chain has been applied to the local state tree. The round is the state channel's internal round. Since the round is incrementing on every off-chain state change, the channel transaction both represents the off-chain state of the channel and defines the point of time when it was valid. This allows us to make educated decisions on whether or not to update the on-chain persisted channel object. This way we can solo close a channel according to the last provided on-chain state. All we have to do is to provide a proof of inclusion having the same state_hash.

Establishing a channel

All of the on-chain operations could be submitted by any peer but we assume that the initiating peer pays for the setup of the channel. Therefore the initiator MUST pay the channel_create_tx transaction fees.

channel_create

The channel_create_tx transaction is used to register a channel on-chain and its inclusion on-chain causes the specified amounts to be locked up as a guarantee that participants have a vested interest in the channel. Although it is not a strict requirement, it is strongly suggested to do so.

e.g.

Account(initiator).balance := Account(initiator).balance - initiator_amount

Account(responder).balance := Account(responder).balance - responder_amount

Channel(cid).amount := initiator_amount + responder_amount

Serialization defined here

  • initiator_id: account id of the initiating peer
  • responder_id: account id of the responding peer
  • initiator_amount: unsigned amount of coins the initiator commits to the channel
  • responder_amount: unsigned amount of coins the responder commits to the channel
  • lock_period: period for disputes after solo operations
  • delegate_ids, initiator_delegate_ids and responder_delegate_ids: lists of delegate account ids. The delegates play a role in the solo closing sequence: except for the participants of the channel, they are the only ones that can provide a snapshot, slash or forced progress transactions. Pre iris there is only a single list of delegates and from iris on those are fine grained on a participant level
  • state_hash: the root hash of the channel state tree; This is not validated, just kept in the channel's object as initial value. This come into play if participants enter a dispute right after opening the channel on-chain.
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: initiator's account nonce or 0 if one is a generalized account

The length of the lock_period is a trade-off between responsiveness, e.g. how fast solo operations can be committed, and security. Choosing a lock_period that gives participants enough time to react to potential malicious solo operations is crucial.

The ttl is in absolute chain height. The involved parties will want to set the ttl to a value quite a bit larger than the present chain height, to avoid uncertainty. If the fees included are low and transaction pressure is high, then the transaction might end up being stuck in the mempool for an extended period. ttl is optional, no ttl means a transaction that is valid "forever".

The fee and nonce refer to the initiator account, i.e. the fee MUST be taken from their balance and the nonce of their account MUST be incremented.

Generalized accounts

If either of the participants is a Generalized account the channel create will also create an extra entry in the contract state tree, containing a frozen authentication state that will be used for off-chain authentication and verification in off-chain updates (potentially eventually enforced on-chain) of this particular channel. A more detailed explanation can be found here

Requirements

Account(initiator).balance >= initiator_amount + fee

Account(responder).balance >= responder_amount

initiator_amount >= channel_reserve

responder_amount >= channel_reserve

Updating a channel

An update to an open channel requires the authentication of all participants and a channel identification (channel_id).

Both channel_deposit_tx and channel_withdraw_tx MUST be authenticated by all involved parties, since changing channel balances might change the dynamics of code running in a channel.

The channel_id is computed from the public key of the initiator, the nonce of the create transaction and the public key of the responder using Blake2b (256 bits digest).

channel_id = Blake2b(initiator || channel_create_tx_nonce || responder)
                        32                  32                  32

channel_deposit

Depositing funds into a channel after its creation provides the means for a participant to move coins from the participant's balance to the channel's one. This should allow channels to be more long-lived due to the increased ease of balancing them out. The amount of coins sent along with this transaction will get locked up just like the initial deposit.

While it could be desirable to allow anyone to deposit into a channel, we are going to restrict deposits to the peers of a channel. That means, the from_id field MUST be an address of one of the participants of the targeted channel and the standard transaction fee MUST be paid by the from_id account.

This operation is not mandatory for normal channel operation.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: sender of the deposit
  • amount: amount of coins deposited
  • state_hash: the root hash of the channel state tree after the deposit has been applied to it; This is not validated on-chain, just kept in the channel's object
  • round: the channel's internal round that applies the deposit
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: from_id's account nonce

Note that the round SHOULD be incremented on each off-chain update. This means, in order for an on-chain transaction, referring to off-chain state, to be considered valid, it MUST include a round higher than the currently recorded Channel(channel_id).round.

If this transaction is valid then it sets:

  • Channel(channel_id).round := round
  • Channel(channel_id).solo_round := 0
  • Channel(channel_id).state_hash := state_hash
  • Channel(channel_id).total_amount := Channel(channel_id).total_amount + amount

channel_withdraw

Channels should generally not be used to hold significant amounts of coins but being able to withdraw locked coins might still be of use.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • to: receiver of the withdraw
  • amount: amount of coins withdrawn
  • state_hash: the root hash of the channel state tree after the withdraw had been applied; This is not validated, just kept in the channel's object
  • round: the channel's internal round that applies the withdraw
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: to's account nonce

The to account MUST be a participant in the target channel. The amount MUST be less than or equal to the sum of the channel balance with regard to the channel_reserve amounts, i.e. channels cannot create coins out of thin air but also a minimum of channel_reserve must remain locked in the channel. The fee is paid by the to account and that account should hold enough coins to pay the fee, i.e., the fee is subtracted before the withdrawn coins arrive.

Note that the round SHOULD be incremented on each off-chain update. This means, in order for an on-chain transaction, referring to off-chain state, to be considered valid, it MUST include a round higher than or equal to the currently recorded Channel(channel_id).round.

If this transaction is valid then it sets:

  • Channel(channel_id).round := round
  • Channel(channel_id).solo_round := 0
  • Channel(channel_id).state_hash := state_hash
  • Channel(channel_id).total_amount := Channel(channel_id).total_amount - amount

channel_snapshot_solo

In order to make channels both secure and trustless even when one party goes offline, we provide the functionality of snapshots. Snapshots provide a recent off-chain state to be recorded on-chain. This state is represented by a round and a state_hash. Those are proven to be correct at least at some point of time in the past by providing a co-authenticated off-chain update as a payload. Note that the payload can not be an on-chain transaction. After channel_snapshot_solo_tx inclusion the channel can not be closed using an older state—as indicated by the round—than the one provided in the snapshot.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: the account that posts the transaction
  • payload: an off-chain transaction of the same channel authenticated by both parties
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: the from_id account nonce

The from_id account MUST be a participant or a delegate in the target channel. The payload MUST be an off-chain state. It MUST provide correct authentication methods for both parties. It MUST be part of the same channel (containing same channel id) and it MUST have a round higher than the one currently recorded on-chain.

This transaction MUST NOT trigger the lock_period and MUST NOT be used when the channel is in the closing state. It can be used to overwrite a state produced by a channel_force_progress_tx while the channel is in the open state.

If this transaction is valid then it sets:

  • Channel(channel_id).round := payload.round
  • Channel(channel_id).solo_round := 0
  • Channel(channel_id).state_hash := payload.state_hash

channel_set_delegates

In order to make channels both secure and trustless even when one party goes offline, a participant can delegate the right to produce certain transactions to other third parties. Delegates are set initially in the channel_create_tx and, since the iris hardfork, can be updated with channel_set_delegates_tx. It acts similarly to channel_snapshot_solo_tx with three notable differences:

  • While channel_snapshot_solo_tx can be based only on off-chain state, channel_set_delegates_tx can be based both on off-chain state or latest on-chain state. In the latter case the payload provided is empty.

  • channel_set_delegates_tx is mutually agreed upon. If the other participant does not want to approve such a transaction, this could be a clear sign that the assumption of cooperation is broken.

  • channel_set_delegates_tx not only updates the on-chain channel object but also sets the list of delegate ids for each participant. The old lists of delegates are deleted and the new ones provided by the transaction replace them. There is no option for setting the list just for one participant.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: the account that posts the transaction
  • initiator_delegate_ids: the list of delegates that can provide transactions on behalf of the initiator
  • responder_delegate_ids: the list of delegates that can provide transactions on behalf of the responder
  • payload: an off-chain transaction of the same channel authenticated by both parties. It could be empty
  • state_hash: the hash of the payload, if provided - and if not, the latest provided on-chain state_hash
  • round: the hash of the payload, if provided - and if not, the latest provided on-chain round
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: the from_id account nonce

The from_id account MUST be a participant in the target channel. The payload MUST be an off-chain state or empty. It MUST provide correct authentication methods for both parties. It MUST be part of the same channel (containing same channel id). If provided, it MUST have a round higher than the one currently recorded on-chain. The state_hash and round must match the ones in payload or the on-chain stored data if the payload is empty.

This transaction MUST NOT trigger the lock_period and MUST NOT be used when the channel is in the closing state. It can be used to overwrite a state produced by a channel_force_progress_tx while the channel is in the open state.

If this transaction is valid then it sets:

  • Channel(channel_id).round := round
  • Channel(channel_id).solo_round := 0
  • Channel(channel_id).state_hash := state_hash
  • Channel(channel_id).initiator_delegate_ids := initiator_delegate_ids
  • Channel(channel_id).responder_delegate_ids := responder_delegate_ids

Closing a channel

We expect channels to be long running but they could be closed. This shall happen when participants have completed their interaction but this includes also the cases of non-cooperation or malicious behaviour. If both parties decide to close the channel, closing is just a matter of issuing one on-chain transaction, authenticated by everyone involved.

In the case of a solo closing, operations are subject to the lock_period, during which the closing state can be disputed via a channel_slash_tx or even progressed further on-chain via channel_force_progress_tx transactions.

channel_close_mutual

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: the account that posts the transaction
  • initiator_amount_final: final balance for the initiator
  • responder_amount_final: final balance for the responder
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: the from_id account nonce

initiator_amount_final and responder_amount_final are the agreed upon distribution of coins out of the channel total balance of coins. The initiator's and responder's account balances are incremented by initiator_amount_final and responder_amount_final respectively. The channel MUST have enough total coins to pay for the fee as well as the agreed upon amounts. The total closing amount of a channel is computed by adding the amounts of the initiator and responder (before the close) and the fee. If this total closing amount is lower than the total amount coins already dedicated to the channel, the excess of coins is locked.

Requirements

This transaction MUST have valid authentications of both parties.

This transaction MUST NOT be disputed and any ongoing dispute MUST be considered resolved by this transaction.

After this transaction has been included in a block, the channel MUST be considered closed and allow no further modifications. The on-chain persisted channel object is removed from the on-chain state trees.

channel total >= transaction initiator_amount_final + responder_amount_final + fee

channel_close_solo

In order to close a channel unilaterally, a participant has to send a channel_close_solo transaction. This is only necessary if one peer stops responding or cooperating but can also be used by an malicious peer trying to close a channel with a state that hasn't been agreed on by all participants.

At any point a channel participant can initiate the solo closing sequence. After the channel_close_solo is posted and included in the chain a lock_period block height timer is started. This lock period is required to give the other party an opportunity to dispute the final state, that the closing sequence is based on. This can be done via the channel_slash_tx and channel_force_progress_tx transactions.

With the inclusion of this transaction on-chain, the channel enters the locked state, during which the channel_close_solo_tx can be disputed.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: participant of the channel that posts the closing transaction
  • payload: empty or an authenticated off-chain state proving that the proof of inclusion is part of the channel
  • poi: proof of inclusion
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: according to the from_id's account

Proof of inclusion represents the channel's internal state. At the bare minimum it has to include all accounts and their balances. It MUST provide enough information to close the channel. Miners are to check balances in it and use this data to update the channel's on-chain representation. This is how the poster initiates the solo closing sequence. If there are any contracts in the channel and those have balances of their own, they are not provided in the proof of inclusion but they are rather could be force-pushed in subsequent transactions. It is up to participants to decide if they want to post them at all. Thus the accumulative balances of the accounts in the solo-close transaction can be lower than the channel balance persisted on-chain.

The payload can be either empty or an authentication off-chain state transaction.

Empty payload

If the payload is empty, the last on-chain persisted state_hash and solo_round are used. In this case the proof of inclusion root hash MUST be equal to the one persisted for the channel on-chain. If that state was produced unilaterally, i.e. via a channel_force_progress_tx, making Channel(channel_id).round != Channel(channel_id).solo_round, the solo close is based on the solo_round and thus can still be disputed.

  • Channel(channel_id).locked_until := Block.height + Channel(channel_id).lock_period

Off-chain transaction payload

If the payload is a transaction it MUST be a channel_offchain_tx. It MUST be authenticated by both participants.

Payload is a valid transaction that has:

  • state_hash equal to the proof of inclusion's root hash. This is a proof that the PoI is correct
  • channel_id being the same as the transaction channel_id
  • round greater than Channel(channel_id).round. If Channel(channel_id).solo_round > Channel(channel_id).round, then this close will invalidate progress produced on-chain

If true, the following changes will be made:

  • Channel(channel_id).round := payload.round
  • Channel(channel_id).solo_round := payload.round
  • Channel(channel_id).state_hash := payload.state_hash
  • Channel(channel_id).locked_until := Block.height + Channel(channel_id).lock_period

channel_settle

The settlement transaction is the last one in the lifecycle of a channel, but only required if the parties involved did not manage to cooperate when trying to close the channel. It has to be issued after all possible disputes are resolved to then redistribute the locked coins.

The channel_settle_tx CAN only be included in a block if:

  • a channel_close_solo_tx transaction was published and the lock_period has expired, i.e. blockheight(top) - blockheight(channel_close_solo_tx) >= lock_period
  • there are no open disputes, which means that the channel is not currently locked from a prior solo action

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: participant of the channel that posts the settling transaction
  • initiator_amount_final: unsigned amount of coins the initiator gets from the channel
  • responder_amount_final: unsigned amount of coins the responder gets from the channel
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: according to the from_id's account

Requirements

After this transaction has been included in a block, the channel MUST be considered closed and allow no further modifications. After the transaction is included, the channel object is removed from on-chain trees.

The transaction MUST be authenticated using the method corresponding to the account behind from_id.

The amounts must correspond to the ones on-chain, provided by the last channel_close_solo_tx, channel_slash_tx or a channel_force_progress_tx. The sum of those final amounts form the total closing amount of the channel. If this total closing amount is lower than the total amount of coins already dedicated to the channel, the excess of coins is locked.

Forcing progress

Forcing progress is the mechanism to be used when a dispute arises between parties and one of them wants to use the blockchain as an arbiter in a smart contract. The poster provides the off-chain state so that an off-chain contract can be executed on-chain and thus producing the channel's next off-chain state.

This can happen both while a channel is closing or while it is still active. If the channel is not closing, participants can continue using it from the on-chain produced channel state or initiate a closing based on it. If the channel is already closing, the force-progress updates what are the currently expected closing amounts for each participant (according to the contract's execution).

The force progress is based on what is considered to be the latest off-chain channel state. We have no way of proving that this state is actually the last one so any progress on chain can always be invalidated by providing an off-chain channel state, authenticated by both participants, with a higher round number than the round number that the to-be-disputed channel_force_progress_tx transaction used.

If the channel is in the closing state, issuing a channel_force_progress_tx locks it for lock_period blocks, during which the update can be disputed. This lock is necessary to prevent the channel from being closed immediately after a new round has been produced on chain.

The forcer can be either a participant or, after iris, a delegate. The latter can only force from behalf of the participant that had specified her to do so, ex: initiator's delegates can only force progress calls made from the initiator's account. Delegated forced progress is allowed only when the channel is already in a closing state.

It is worth mentioning that what is to be disputed is the off-chain state that the force progress had been based on and not the forcing of progress itself. If an older state had been provided by the forcing party, the other party can post a newer authenticated off-chain state (via a snapshot for example). The authenticated by both participants off-chain state with the same or greater round will replace the on-chain produced one.

channel_force_progress_tx

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: a participant or a delegate of the channel that posts the force progress transaction
  • payload: empty or an off-chain state transaction proving that the state trees represent a mutually agreed-upon channel state
  • round: channel's next round
  • update: channel off-chain update that contains the contract call with gas limit and gas prices to be consumed for the on-chain execution of the off-chain contract. If it is a delegate that provides the force progress, the the caller of the off-chain contract MUST be a participant that had authorized this delegate
  • state_hash: channel's expected new root hash of off-chain state trees
  • offchain_trees: the full state channel's state trees
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: according to the from_id's account

The offchain_trees is the full off-chain state trees set: all accounts and all contracts. It MUST include both participants' off-chain accounts. It MUST include the contract to be called. Based on the offchain_trees, the next state is going to be computed and its root hash will become the new state's state_hash. The newly produced state trees will be different than the provided ones at least with the newly added call object. Thus even if the contract call fails a new state_hash is produced.

If this transaction is sent while the channel is in the closing state, it will transition the channel into the locked state for the next lock_period blocks. Although while this lasts, the channel can not be settled, new force- progressed transactions can still be accepted before the timer expires. Those MUST be based on the previous on-chain produced states but each next one resets the dispute timer.

The payload can be either empty or an authenticated off-chain state transaction.

Empty Payload

If the payload is empty, the last on-chain persisted state and solo_round are used. In this case the state trees root hash MUST be equal to the one persisted for the channel on-chain.

If the contract execution is successful, the channel will be updated with:

  • Channel(channel_id).solo_round := Channel(channel_id).solo_round + 1
  • Channel(channel_id).state_hash := state_hash

Additionally, if the channel is in the closing state:

  • Channel(channel_id).locked_until := Block.height + Channel(channel_id).lock_period

Off-chain transaction Payload

If the payload is a transaction it MUST be a channel_offchain_tx. It MUST be authenticated by both participants.

An off-chain transaction payload is a valid transaction if it has:

  • state_hash equal to the state trees' root hash. This is a proof that the offchain_trees represent a state both participants had agreed upon at some point of time
  • channel_id being the same as the transaction channel_id
  • round greater than Channel(channel_id).round

The update MUST be a call to a contract. The caller of this update MUST be the poster of the force progress transaction. amount, gas and gas_price are specified in the update as well. The gas fees are going to be paid by the poster of the transaction. The gas_price MUST match protocol expectations for minimum gas price. The state_hash will be the root hash of the updated channel's state trees. After applying the contract call to the provided offchain_trees and updating accounts accordingly, a new channel's state tree is produced. It MUST have the same value of root hash as the state hash. If those do not match the force progress fails but since this can only be determined after the call has been executed, a call object is added on-chain and gas is consumed.

If the contract call succeeded, the channel state will be updated:

  • Channel(channel_id).round := payload.round
  • Channel(channel_id).solo_round := payload.round + 1
  • Channel(channel_id).state_hash := state_hash

Additionally, if the channel is in the closing state:

  • Channel(channel_id).locked_until := Block.height + Channel(channel_id).lock_period

Force progress side effects

Updating channel object

Channel state trees are recreated according to the offchain_trees being provided. The update is an off-chain contract call. It is applied on the channel's state trees and modifies them. The modified trees have a root hash. It might be:

  • equal to the state_hash provided in the force progress transaction. This hash indeed is the expected result of the contract call and the blockchain has confirmed it. The on-chain channel object is updated accordingly:
  • channel's state hash is updated to be the newly computed one
  • channel's round is the one in the force progress transaction
  • if the channel had been in a closing state, closing balances of participants are updated according to the ones in the modified channel state trees
  • not equal to the state_hash provided in the force progress transaction. The hash provided was not confirmed to be the expected one. In this case the force progress fails and no new state channel state is created. The on-chain channel object is NOT modified and thus - the round and state_hash as stored on-chain remain unchanged. Gas is still consumed and a call object is created on-chain.

A special case would be the forcer providing an invalid update call to be forced. Examples for an invalid update calls would be:

  • A remote call to a missing contract
  • Spending too much coins in the call so a participants's off-chain balance goes bellow the channel_reserve threshold
  • Contract call being terminated due to a out_of_gas exception

In this case the contract can not be executed and the forcing of progress fails to produce a new state. The end result is exactly the same as if there had been a mismatch of the produced state_hash and the expected one. It is worth mentioning that in this case the transaction is still a valid one, the poster of the transaction is charged for the consumed gas.

Call object

If the channel_force_progress_tx is valid, the contract call in the update is executed upon the MPT that had been produced by the offchain_trees. The output is a new MPT that will represent the new off-chain channel state. Participants are to either continue using the channel or close it. If there is no later off-chain update, they are expected to use this produced MPT in both cases.

The contract execution consumes gas. The update itself defines both the gas limit and the gas price. After the contract call has been executed and the real gas consumption has been calculated, the balance of the account posting the transaction is updated to pay the gas fee. Since this is not a mutual transaction but rather a unilateral one, the initiator of the progress enforcement pays the fees.

The contract call produces on-chain a new call object in the on-chain state trees for contract calls. Usually calls have a key that is composed by the contract's address, the caller's address and the caller's nonce. Since the off- chain contract is not persisted on-chain, it does not have an address that can be used in that manner. The calls produced by forcing progress use the channel_force_progress_tx's hash instead.

Since the miner is expending resources for the contract's execution, the gas fees are paid and the call object is created for every force progress, no matter if it was successful to update the on-chain channel object or not.

Disputing updates

Disputes should be considered anomalies, which only happen whenever one party tries to unilaterally publish an outdated state while:

  • closing a channel unilaterally
  • forcing progress
  • slashing
  • snapshoting

and can be disputed via channel_slash_tx, channel_force_progress_tx, channel_snapshot_solo_tx transactions.

Since disputes can themselves be challenged, we could end up in situations, where a malicious party progresses rounds rapidly via channel_force_progress_tx transactions, depriving the other party of the ability to dispute. To prevent this situation we can either:

  1. enforce each dispute to always have to wait for the lock_period to expire and have only one dispute per period
  2. or not restrict the number consecutive disputes but always have the option of challenging the first element of any chain of disputes, invalidating the full chain.

We choose to use the second strategy because it allows faster progress in the case of a peer that disappeared while still guaranteeing safety. This makes keeping track of the proper states more complex but we assume a peer disappearing has a higher likelihood than them being actively malicious. Therefore we try to optimise for that case.

Force progress dispute with closing channel

If the channel is in the closing state, which only happens via a channel_close_solo, then each dispute triggers an extension of the channel lock by lock_period blocks. If a channel is locked, the same rules as above apply. That is, as many channel_force_progress_tx or channel_slash_tx transactions as desired can be submitted—each one extending the lock—but as long as the channel is locked, the entire chain can be invalidated if a state with a higher round number can be posted.

Having the lock is necessary here because otherwise a malicious party might post a channel_force_progress_tx or channel_slash_tx containing an outdated state and then immediately try to have the channel be settled based on the result.

Force progress dispute with open channel example

Setup:

  • Bob and Alice open a channel between each other with lock_period := 100
  • At chain_height := 1000 Bob posts a channel_force_progress_tx with a payload containing round := 23 and produces round := 24 on chain
  • The channel state will now contain 23 as the latest round and 24 as the solo_round

With the selected strategy, Bob does not have to wait for the lock_period to expire and can post as many channel_force_progress_tx as he wants, e.g. at chain_height := 1001 he produces solo_round := 25 and by chain_height := 1011 arrives at solo_round := 31. The round is still at 23.

Now if Alice returns at chain_height := 1110, she can still dispute the initial update issued by Bob at chain_height := 1000 by providing an authenticated by both payload with round := 24 or higher via either a channel_force_progress_tx or a channel_snapshot_solo_tx.

Force progress dispute with closing channel example

Setup:

  • Bob and Alice open a channel between each other with lock_period := 100
  • At chain_height := 1000 Bob posts a channel_close_solo with a payload containing round := 23. The channel is now locked until chain_height == 1100

With the selected strategy, Bob has to wait for the lock_period to expire, if he wants to settle the channel. But while the channel is locked, he can still post as many channel_force_progress_tx as he wants, e.g. at chain_height := 1001 he produces solo_round := 24 and by chain_height := 1011 arrives at solo_round := 31. Each subsequent operation issued bumps the lock_period ahead. That is, by chain_height == 1011 the lock_period is set to run out at chain_height == 1111.

Now it is important to note, that at even at chain_height := 1110 Alice can still dispute the initial update issued by Bob at chain_height := 1000 by providing an authenticated by both payload with round := 24 or higher but her dispute would bump the lock by lock_period too.

Channel slash

If a malicious party sent a channel_close_solo or channel_force_progress_tx with an outdated state, the honest party has the opportunity to issue a channel_slash_tx transaction. This transaction MUST include a state with a higher round number than the one being disputed, authenticated by all peers for a successful challenge.

Serialization defined here

  • channel_id: channel id as recorded on-chain
  • from_id: channel participant or delegate that posts the slashing transaction
  • payload: an off-chain transaction proving that the proof of inclusion is part of the channel
  • poi: proof of inclusion
  • ttl: blockheight target until which this transaction can be included
  • fee: transaction fee
  • nonce: taken from the from_id's account

The proof of inclusion represents the channel's internal state. It has to include both participants' accounts and their balances. If there are any contracts in the channel and those have balances of their own, they are not provided in the proof of inclusion but they are rather to be force pushed in subsequent transactions. It is up to participants to decide if they want to post them at all. Thus the accumulative balances of the accounts in the slash transaction can be lower than the channel balance persisted on-chain.

Off-chain transaction payload

The payload is a transaction and it MUST be a channel_offchain_tx. It MUST be authenticated by both peers.

Payload is a valid transaction that has:

  • state_hash equal to the proof of inclusion's root hash. This is a proof that the PoI is correct
  • channel_id being the same as the transaction channel_id
  • round greater than the last on-chain provided co-authenticated channel state. If the latest on-chain round(s) were produced by channel_force_progress_tx transaction(s) that were based on an older channel state - the whole chain of forced progressed channel states are invalidated and replaced by the state provided by the slash. Co-authenticated channel transactions replace unilateral ones with the same round.

If true, the following changes will be made:

  • Channel(channel_id).round := payload.round
  • Channel(channel_id).solo_round := payload.round
  • Channel(channel_id).state_hash := payload.state_hash
  • Channel(channel_id).locked_until := Block.height + Channel(channel_id).lock_period

Requirements

MUST be authenticated using the method corresponding to the public key from_id.

Channel state tree

Each block MUST commit to a Merkle Patricia tree of open channels, where the channel_id specifies the path. At a leaf, nodes store information pertaining to the current state of the given channel.

  • channel_id
  • initiator_id
  • responder_id
  • delegator_ids
  • total_amount
  • initiator_amount
  • responder_amount
  • channel_reserve
  • state_hash: last published state_hash
  • round: last known round
  • solo_round: last round produced via a channel_force_progress_tx
  • lock_period: agreed upon locking period by peers
  • locked_until: on-chain channel height after which the channel can be settled
  • initiator_auth: (from Fortuna release) signing/authentication data for initiator
  • responder_auth: (from Fortuna release) signing/authentication data for responder

Keeping track of the state_hash, round, locked_until, and lock_period is necessary for nodes to be able to assess the validity of channel_slash_tx and channel_settle_tx transactions.

The locked_until is initialised with 0 and will stay 0 until the channel enters the closing state.

Serialization defined here