Opening

Opening a state channel works by defining the initial asset allocation, setting the channel parameters, creating a proposal and sending that proposal to all participants. The channel is open after all participants accept the proposal and finish the on-chain funding.

It looks like this:

func (n *node) openChannel() error {
	fmt.Printf("Opening channel from %v to %v\n", n.role, 1-n.role)
	// Alice and Bob will both start with 10 ETH.
	initBal := ethToWei(10)
	// Perun needs an initial allocation which defines the balances of all
	// participants. The same structure is used for multi-asset channels.
	initBals := &channel.Allocation{
		Assets:   []channel.Asset{ethwallet.AsWalletAddr(n.assetholder)},
		Balances: [][]*big.Int{{initBal, initBal}},
	}
	// All perun identities that we want to open a channel with. In this case
	// we use the same on- and off-chain accounts but you could use different.
	peers := []wire.Address{
		cfg.addrs[n.role],
		cfg.addrs[1-n.role],
	}
	// Prepare the proposal by defining the channel parameters.
	proposal, err := client.NewLedgerChannelProposal(10, cfg.addrs[n.role], initBals, peers)
	if err != nil {
		return fmt.Errorf("creating channel proposal: %w", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	// Send the proposal.
	channel, err := n.client.ProposeChannel(ctx, proposal)
	if err != nil {
		return fmt.Errorf("proposing channel: %w", err)
	}
	fmt.Printf("🎉 Opened channel with id 0x%x \n", channel.ID())
	return nil
}

The ProposeChannel call blocks until Alice either accepted or rejected the channel and funded it.

Warning

The channel that is returned by ProposeChannel should only be used to retrieve its id.

HandleProposal

An example Proposal handler looks like this:

func (n *node) HandleProposal(_proposal client.ChannelProposal, responder *client.ProposalResponder) {
	// Check that we got a ledger channel proposal.
	proposal, ok := _proposal.(*client.LedgerChannelProposal)
	if !ok {
		fmt.Println("Received a proposal that was not for a ledger channel.")
		return
	}
	// Print the proposers address (his index is always 0).
	fmt.Printf("Received channel proposal from 0x%x\n", proposal.Peers[0])
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	// Create a channel accept message and send it.
	accept := proposal.Accept(n.account.Address(), client.WithRandomNonce())
	channel, err := responder.Accept(ctx, accept)
	if err != nil {
		fmt.Println("Accepting channel: %w\n", err)
	} else {
		fmt.Printf("Accepted channel with id 0x%x\n", channel.ID())
	}
}

You can add additional check logic here but in our simple use case we always accept incoming proposals. After the channel is open, both participants will have their NewChannel callback called.

Warning

The Channel that ProposalResponder.Accept returns should only be used to retrieve its ID.

NewChannel

go-perun expects this handler to finish quickly. Use go routines if you want to do time-intensive tasks. You should also start the watcher as shown below:

func (n *node) HandleNewChannel(ch *client.Channel) {
	fmt.Printf("%v HandleNewChannel with id 0x%x\n", n.role, ch.ID())
	n.ch = ch
	// Start the on-chain watcher.
	go func() {
		err := ch.Watch(n)
		fmt.Println("Watcher returned with: ", err)
		close(n.done)
	}()
}

Note

Starting the watcher is not mandatory but strongly advised. go-perun can otherwise not react to malicious behavior of other participants.