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.
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/spewBlock 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> -secioEach 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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'.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
