Closing a channel can be done in two ways, either cooperative or non-cooperative. This example focuses on the cooperative way. As you would expect from closing an off-chain channel, the on-chain balances will be updated accordingly. But before that can happen there are some other steps that we will go through first.

The Watcher

go-perun reacts automatically to on-chain events of a channel as long as its watcher routine is running. You should start the watcher in the NewChannel handler in a new go-routine since channel.Watch blocks.


The state of a channel in go-perun has a public boolean IsFinal flag. Final states can directly be closed on-chain without raising a dispute. This allows for a faster collaborative closing process. As soon as a channel has a final state, we call it finalized since it can not be updated anymore. Have a look again at Updating on how to do it.


Registering a channel means pushing its latest state onto the Adjudicator. A registered channel state is openly visible on the blockchain. This should only be done when a channel should be closed or disputed.


Registering non-finalized channels will raise a dispute.


Settlement is the last step in the lifetime of a channel. It consists of two steps: conclude and withdraw. go-perun takes care of both when channel.Settle is called.

conclude waits for any on-chain disputes to be resolved and then calls the Adjudicator to close the channel. After this is done, the channel can be withdrawn. This is done only once by one of the channel participants.

The last step is for each participant to withdraw their funds from the AssetHolder. The balance that can be withdrawn is the same as the final balance of the channel. Ethereum transaction fees still apply.


Trying to settle a channel that was not registered before is not advised and can result in a dispute.

Keep in mind that we already finalized the channel in the update that we sent. We therefore just need to register and settle which looks like this:

func (n *node) closeChannel() error {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	if err :=; err != nil {
		return fmt.Errorf("registering channel: %w", err)
	if err :=, false); err != nil {
		return fmt.Errorf("settling channel: %w", err)
	// .Close() closes the channel object and has nothing to do with the
	// go-perun channel protocol.
	if err :=; err != nil {
		return fmt.Errorf("closing channel object: %w", err)
	return nil

The other participant would then have its AdjudicatorEvent handler called with a ConcludedEvent and should then also execute closeChannel.

func (n *node) HandleAdjudicatorEvent(e channel.AdjudicatorEvent) {
	fmt.Printf("HandleAdjudicatorEvent called id=0x%x\n", e.ID())
	// Alice reacts to a channel closing by doing the same as Bob.
	if _, ok := e.(*channel.ConcludedEvent); ok && n.role == RoleAlice {