Opening a payment 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.

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{
	// 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 accepted and funded the channel, or rejected.


Clients must implement the channel proposal handler interface in order to respond to incoming channel proposals.

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.")
	// 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.


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()) = ch
	// Start the on-chain watcher.
	go func() {
		err := ch.Watch(n)
		fmt.Println("Watcher returned with: ", err)


Starting the watcher is strongly advised. Otherwise go-perun will not react to malicious behavior of other participants and users risk losing funds.