Blockchain 22 min read

How to Build a Simple P2P Blockchain in Go Using libp2p

This tutorial walks through creating a fully decentralized peer‑to‑peer blockchain in Go, covering the background of P2P networks, installing the go‑libp2p library, defining block structures, implementing validation and host creation, handling streams for data exchange, and running multiple nodes to synchronize state without any central server.

Senior Brother's Insights
Senior Brother's Insights
Senior Brother's Insights
How to Build a Simple P2P Blockchain in Go Using libp2p

Background

In a true peer‑to‑peer (P2P) blockchain each node stores a full copy of the chain and reaches consensus when more than 51% of nodes agree on the latest state.

Dependencies

Install the required Go packages:

go get -u github.com/libp2p/go-libp2p
go get -u github.com/davecgh/go-spew/spew

Block structure

The blockchain is represented by the following struct and global variables:

type Block struct {
    Index     int
    Timestamp string
    BPM       int
    Hash      string
    PrevHash  string
}

var Blockchain []Block
var mutex = &sync.Mutex{}

Core functions

calculateHash(block Block) string

– computes a SHA‑256 hash of the block fields. isBlockValid(newBlock, oldBlock Block) bool – checks index continuity, previous‑hash match and recomputed hash. generateBlock(oldBlock Block, BPM int) Block – creates a new block with an incremented index, current timestamp, the supplied BPM value and a calculated hash.

Libp2p host creation

The function

makeBasicHost(listenPort int, secio bool, randseed int64) (host.Host, error)

builds a libp2p host, generates an RSA key pair, sets the listen address, optionally disables encryption, and prints the full multi‑address.

func makeBasicHost(listenPort int, secio bool, randseed int64) (host.Host, error) {
    var r io.Reader
    if randseed == 0 {
        r = rand.Reader
    } else {
        r = mrand.New(mrand.NewSource(randseed))
    }
    priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
    if err != nil {
        return nil, err
    }
    opts := []libp2p.Option{
        libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", listenPort)),
        libp2p.Identity(priv),
    }
    if !secio {
        opts = append(opts, libp2p.NoEncryption())
    }
    basicHost, err := libp2p.New(context.Background(), opts...)
    if err != nil {
        return nil, err
    }
    hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
    fullAddr := basicHost.Addrs()[0].Encapsulate(hostAddr)
    log.Printf("I am %s
", fullAddr)
    if secio {
        log.Printf("Now run \"go run main.go -l %d -d %s -secio\" on another terminal
", listenPort+1, fullAddr)
    } else {
        log.Printf("Now run \"go run main.go -l %d -d %s\" on another terminal
", listenPort+1, fullAddr)
    }
    return basicHost, nil
}

Stream handling

When a peer connects, libp2p invokes handleStream. It creates a buffered ReadWriter and launches two goroutines: readData and writeData.

func handleStream(s net.Stream) {
    log.Println("Got a new stream!")
    rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
    go readData(rw)
    go writeData(rw)
}

Reading data

Continuously reads newline‑terminated JSON strings, unmarshals them into a slice of Block, and replaces the local chain if the received chain is longer.

func readData(rw *bufio.ReadWriter) {
    for {
        str, err := rw.ReadString('
')
        if err != nil {
            log.Fatal(err)
        }
        if str == "" || str == "
" {
            return
        }
        var chain []Block
        if err := json.Unmarshal([]byte(str), &chain); err != nil {
            log.Fatal(err)
        }
        mutex.Lock()
        if len(chain) > len(Blockchain) {
            Blockchain = chain
            bytes, _ := json.MarshalIndent(Blockchain, "", "  ")
            fmt.Printf("\x1b[32m%s\x1b[0m> ", string(bytes))
        }
        mutex.Unlock()
    }
}

Writing data

Every 5 seconds the current blockchain is marshaled and sent to the peer. The function also reads BPM values from stdin, creates a new block, validates it, appends it to the chain, and broadcasts the updated chain.

func writeData(rw *bufio.ReadWriter) {
    go func() {
        for {
            time.Sleep(5 * time.Second)
            mutex.Lock()
            bytes, err := json.Marshal(Blockchain)
            if err != nil {
                log.Println(err)
                mutex.Unlock()
                continue
            }
            rw.WriteString(fmt.Sprintf("%s
", string(bytes)))
            rw.Flush()
            mutex.Unlock()
        }
    }()

    stdReader := bufio.NewReader(os.Stdin)
    for {
        fmt.Print("> ")
        sendData, err := stdReader.ReadString('
')
        if err != nil {
            log.Fatal(err)
        }
        sendData = strings.TrimSpace(sendData)
        bpm, err := strconv.Atoi(sendData)
        if err != nil {
            log.Fatal(err)
        }
        newBlock := generateBlock(Blockchain[len(Blockchain)-1], bpm)
        if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
            mutex.Lock()
            Blockchain = append(Blockchain, newBlock)
            mutex.Unlock()
            spew.Dump(Blockchain)
            bytes, _ := json.Marshal(Blockchain)
            rw.WriteString(fmt.Sprintf("%s
", string(bytes)))
            rw.Flush()
        }
    }
}

Main program and command‑line flags

The program parses the following flags: -l – local TCP port to listen on (required). -d – multi‑address of a remote peer to dial (optional). -secio – enable encrypted streams. -seed – optional random seed for deterministic key generation.

It creates a genesis block, starts the libp2p host, registers the stream handler /p2p/1.0.0, and either waits for incoming connections or dials the target peer.

func main() {
    t := time.Now()
    genesisBlock := Block{0, t.String(), 0, calculateHash(Block{0, t.String(), 0, "", ""}), ""}
    Blockchain = append(Blockchain, genesisBlock)

    listenF := flag.Int("l", 0, "listen port")
    target := flag.String("d", "", "target peer address")
    secio := flag.Bool("secio", false, "enable secio")
    seed := flag.Int64("seed", 0, "random seed")
    flag.Parse()

    if *listenF == 0 {
        log.Fatal("Please provide a port to bind on with -l")
    }

    ha, err := makeBasicHost(*listenF, *secio, *seed)
    if err != nil {
        log.Fatal(err)
    }

    ha.SetStreamHandler("/p2p/1.0.0", handleStream)

    if *target == "" {
        log.Println("listening for connections")
        select {}
    }

    // Resolve target address and peer ID
    ipfsaddr, err := ma.NewMultiaddr(*target)
    if err != nil {
        log.Fatalln(err)
    }
    pid, err := ipfsaddr.ValueForProtocol(ma.P_IPFS)
    if err != nil {
        log.Fatalln(err)
    }
    peerid, err := peer.IDB58Decode(pid)
    if err != nil {
        log.Fatalln(err)
    }
    targetPeerAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerid)))
    targetAddr := ipfsaddr.Decapsulate(targetPeerAddr)

    ha.Peerstore().AddAddr(peerid, targetAddr, pstore.PermanentAddrTTL)
    log.Println("opening stream")
    s, err := ha.NewStream(context.Background(), peerid, "/p2p/1.0.0")
    if err != nil {
        log.Fatalln(err)
    }
    rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
    go writeData(rw)
    go readData(rw)
    select {}
}

Running the example

Clone the repository mycoralhealth/blockchain-tutorial and navigate to examples/p2p. Start three nodes:

go run main.go -l 10000 -secio
go run main.go -l 10001 -d <address‑from‑node‑1> -secio
go run main.go -l 10002 -d <address‑from‑node‑1> -secio

Each node prints its full multi‑address; copy the address from the first node for the others. Typing a BPM value (e.g., 70) in any terminal creates a new block, which is broadcast to the peers. The longest‑chain rule ensures all nodes converge to the same blockchain state without any central server.

Further improvements

Address the known data‑race issue in go-libp2p before using this code in production.

Replace the simple longest‑chain rule with a proper consensus algorithm such as Proof‑of‑Stake.

Add persistent storage (LevelDB, Badger, etc.) so the chain survives node restarts.

Stress‑test with many nodes and explore peer‑discovery mechanisms (mDNS, DHT) to automate node discovery.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

P2Plibp2pDecentralized
Senior Brother's Insights
Written by

Senior Brother's Insights

A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.