Run a go-libp2p node

The getting started tutorial covers setting up a development environment, getting familiar with libp2p basics, and implementing a simple node that can send and receive “ping” messages in go-libp2p.

The Protocol Labs Launchpad curriculum also includes a tutorial on spinning up a libp2p node using a go-libp2p boilerplate. Check it out here.

This is the first in a series of tutorials on working with libp2p’s Go implementation, go-libp2p. We’ll cover installing Go, setting up a new Go module, starting libp2p nodes, and sending ping messages between them.

Install Go

  • Ensure your Go version is at least 1.20.
  • You can install a recent version of Go by following the official installation instructions.
  • Once installed, you should be able to run go version and see a version >= 1.20, for example:
$ go version
go version go1.20 darwin/arm64

Create a Go module

We’re going to create a Go module that can be run from the command line.

Let’s create a new directory and use go mod to initialize it as a module. We’ll create it in /tmp, but you can equally create it anywhere on your filesystem. We’ll also initialize it with the module name github.com/user/go-libp2p-tutorial, but you may want to replace this with a name that corresponds to a repository name you have the rights to push to if you want to publish your version of the code.

mkdir -p /tmp/go-libp2p-tutorial
cd /tmp/go-libp2p-tutorial
go mod init github.com/user/go-libp2p-tutorial

You should now have a go.mod file in the current directory containing the name of the module you initialized and the version of Go you’re using, for example:

$ cat go.mod
module github.com/user/go-libp2p-tutorial

go 1.20

Start a libp2p node

We’ll now add some code to our module to start a libp2p node. Let’s start by creating a main.go file that simply starts a libp2p node with default settings, prints the node’s listening addresses, then shuts the node down:

package main

import (
    "fmt"
    "github.com/libp2p/go-libp2p"
)

func main() {
    // start a libp2p node with default settings
    node, err := libp2p.New()
    if err != nil {
        panic(err)
    }

    // print the node's listening addresses
    fmt.Println("Listen addresses:", node.Addrs())

    // shut the node down
    if err := node.Close(); err != nil {
        panic(err)
    }
}

Import the libp2p/go-libp2p module:

go get github.com/libp2p/go-libp2p

We can now compile this into an executable using go build and run it from the command line:

$ go build -o libp2p-node

$ ./libp2p-node
Listen addresses: [/ip6/::1/tcp/57666 /ip4/192.0.2.0/tcp/57665 /ip4/198.51.100.0/tcp/57665]

The listening addresses are formatted using the multiaddr format, and there is typically more than one printed because go-libp2p will listen on all available IPv4 and IPv6 network interfaces by default.

Configure the node

A node’s default settings can be overridden by passing extra arguments to libp2p.New. Let’s use libp2p.ListenAddrStrings to configure the node to listen on TCP port 2000 on the IPv4 loopback interface:

func main() {
        ...

        // start a libp2p node that listens on TCP port 2000 on the IPv4
        // loopback interface
        node, err := libp2p.New(
                libp2p.ListenAddrStrings("/ip4/192.0.2.0/tcp/2000"),
        )
    if err != nil {
        panic(err)
    }

        ...
}

Re-building and running the executable again now prints the explicit listen address we’ve configured:

$ go build -o libp2p-node

$ ./libp2p-node
Listening addresses: [/ip4/192.0.2.0/tcp/2000]

libp2p.New accepts a variety of arguments to configure most aspects of the node. See options.go for a full list of those options.

Wait for a signal

A node that immediately exits is not all that useful. Let’s add the following towards the end of the main function that blocks waiting for an OS signal before shutting down the node:

func main() {
        ...

        // wait for a SIGINT or SIGTERM signal
        ch := make(chan os.Signal, 1)
        signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
        <-ch
        fmt.Println("Received signal, shutting down...")

        // shut the node down
        if err := node.Close(); err != nil {
                panic(err)
        }
}

We also need to update the list of imports at the top of the file to include the os, os/signal and syscall packages we’re now using:

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/libp2p/go-libp2p"
)

Running the node now waits until it receives a SIGINT (i.e. a ctrl-c key press) or a SIGTERM signal before shutting down:

$ ./libp2p-node
Listening addresses: [/ip4/192.0.2.0/tcp/2000]
^CReceived signal, shutting down...

Run the ping protocol

Now that we have the ability to configure and start libp2p nodes, we can start communicating!

Set a stream handler

A node started with go-libp2p will run its own ping protocol by default, but let’s disable it and set it up manually to demonstrate the process of running protocols by registering stream handlers.

The object returned from libp2p.New implements the Host interface, and we’ll use the SetStreamHandler method to set a handler for our ping protocol.

First, let’s add the github.com/libp2p/go-libp2p/p2p/protocol/ping package to our list of imported packages:

import (
    ...

    "github.com/libp2p/go-libp2p"
    "github.com/libp2p/go-libp2p/p2p/protocol/ping"
)

Now we’ll pass an argument to libp2p.New to disable the built-in ping protocol, and then use the PingService type from the ping package to set a stream handler manually (note that we’re also configuring the node to listen on a random local TCP port rather than a hard coded one, which means we’ll be able to run multiple nodes on the same machine without them trying to listen on the same port):

func main() {
    ...

    // start a libp2p node that listens on a random local TCP port,
    // but without running the built-in ping protocol
    node, err := libp2p.New(
        libp2p.ListenAddrStrings("/ip4/192.0.2.0/tcp/0"),
        libp2p.Ping(false),
    )
    if err != nil {
        panic(err)
    }

    // configure our own ping protocol
    pingService := &ping.PingService{Host: node}
    node.SetStreamHandler(ping.ID, pingService.PingHandler)

    ...
}

Connect to a peer

With the ping protocol configured, we need a way to instruct the node to connect to another node and send it ping messages.

We’ll first expand the log message that we’ve been printing after starting the node to include its PeerId value, as we’ll need that to instruct other nodes to connect to it. Let’s import the github.com/libp2p/go-libp2p/core/peer package and use it to replace the “Listen addresses” log message with something that prints both the listen address and the PeerId as a multiaddr string:

import (
    ...

    "github.com/libp2p/go-libp2p"
    peerstore "github.com/libp2p/go-libp2p/core/peer"
    "github.com/libp2p/go-libp2p/p2p/protocol/ping"
)

func main() {
    ...

    // print the node's PeerInfo in multiaddr format
    peerInfo := peerstore.AddrInfo{
        ID:    node.ID(),
        Addrs: node.Addrs(),
    }
    addrs, err := peerstore.AddrInfoToP2pAddrs(&peerInfo)
    fmt.Println("libp2p node address:", addrs[0])

    ...
}

Running the node now prints the node’s address that can be used to connect to it:

$ ./libp2p-node
libp2p node address: /ip4/192.0.2.0/tcp/62268/ipfs/QmfQzWnLu4UX1cW7upgyuFLyuBXqze7nrPB4qWYqQiTHwt

Let’s also accept a command line argument that is the address of a peer to send ping messages to, allowing us to either just run a listening node that waits for a signal, or run a node that connects to another node and pings it a few times before shutting down (we’ll use the github.com/multiformats/go-multiaddr package to parse the peer’s address from the command line argument):

import (
    ...

    "github.com/libp2p/go-libp2p"
    peerstore "github.com/libp2p/go-libp2p/core/peer"
    "github.com/libp2p/go-libp2p/p2p/protocol/ping"
    multiaddr "github.com/multiformats/go-multiaddr"
)

func main() {
    ...
    fmt.Println("libp2p node address:", addrs[0])

    // if a remote peer has been passed on the command line, connect to it
    // and send it 5 ping messages, otherwise wait for a signal to stop
    if len(os.Args) > 1 {
        addr, err := multiaddr.NewMultiaddr(os.Args[1])
        if err != nil {
            panic(err)
        }
        peer, err := peerstore.AddrInfoFromP2pAddr(addr)
        if err != nil {
            panic(err)
        }
        if err := node.Connect(context.Background(), *peer); err != nil {
            panic(err)
        }
        fmt.Println("sending 5 ping messages to", addr)
        ch := pingService.Ping(context.Background(), peer.ID)
        for i := 0; i < 5; i++ {
            res := <-ch
            fmt.Println("got ping response!", "RTT:", res.RTT)
        }
    } else {
        // wait for a SIGINT or SIGTERM signal
        ch := make(chan os.Signal, 1)
        signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
        <-ch
        fmt.Println("Received signal, shutting down...")
    }

    // shut the node down
    if err := node.Close(); err != nil {
        panic(err)
    }
}

Let’s play ping pong!

We are finally in a position to run two libp2p nodes, have one connect to the other and for them to run a protocol!

To recap, here is the full program we have written:

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/libp2p/go-libp2p"
    peerstore "github.com/libp2p/go-libp2p/core/peer"
    "github.com/libp2p/go-libp2p/p2p/protocol/ping"
    multiaddr "github.com/multiformats/go-multiaddr"
)

func main() {
    // start a libp2p node that listens on a random local TCP port,
    // but without running the built-in ping protocol
    node, err := libp2p.New(
        libp2p.ListenAddrStrings("/ip4/192.0.2.0/tcp/0"),
        libp2p.Ping(false),
    )
    if err != nil {
        panic(err)
    }

    // configure our own ping protocol
    pingService := &ping.PingService{Host: node}
    node.SetStreamHandler(ping.ID, pingService.PingHandler)

    // print the node's PeerInfo in multiaddr format
    peerInfo := peerstore.AddrInfo{
        ID:    node.ID(),
        Addrs: node.Addrs(),
    }
    addrs, err := peerstore.AddrInfoToP2pAddrs(&peerInfo)
    if err != nil {
        panic(err)
    }
    fmt.Println("libp2p node address:", addrs[0])

    // if a remote peer has been passed on the command line, connect to it
    // and send it 5 ping messages, otherwise wait for a signal to stop
    if len(os.Args) > 1 {
        addr, err := multiaddr.NewMultiaddr(os.Args[1])
        if err != nil {
            panic(err)
        }
        peer, err := peerstore.AddrInfoFromP2pAddr(addr)
        if err != nil {
            panic(err)
        }
        if err := node.Connect(context.Background(), *peer); err != nil {
            panic(err)
        }
        fmt.Println("sending 5 ping messages to", addr)
        ch := pingService.Ping(context.Background(), peer.ID)
        for i := 0; i < 5; i++ {
            res := <-ch
            fmt.Println("pinged", addr, "in", res.RTT)
        }
    } else {
        // wait for a SIGINT or SIGTERM signal
        ch := make(chan os.Signal, 1)
        signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
        <-ch
        fmt.Println("Received signal, shutting down...")
    }

    // shut the node down
    if err := node.Close(); err != nil {
        panic(err)
    }
}

In one terminal window, let’s start a listening node (i.e. don’t pass any command line arguments):

$ ./libp2p-node
libp2p node address: /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL

In another terminal window, let’s run a second node but pass the address of the first node, and we should see some ping responses logged:

$ ./libp2p-node /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL
libp2p node address: /ip4/192.0.2.0/tcp/61846/ipfs/QmVyKLTLswap3VYbpBATsgNpi6JdwSwsZALPxEnEbEndup
sending 5 ping messages to /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL
pinged /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL in 431.231µs
pinged /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL in 164.94µs
pinged /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL in 220.544µs
pinged /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL in 208.761µs
pinged /ip4/192.0.2.0/tcp/61790/ipfs/QmZKjsGJ6ukXVRXVEcExx9GhiyWoJC97onYpzBwCHPWqpL in 201.37µs

Success! Our two peers are now communicating using go-libp2p! Sure, they can only say “ping”, but it’s a start!

Top