原文链接如下,因为需要FQ,所以摘录过来:
https://medium.com/@mycoralhealth/code-a-simple-p2p-blockchain-in-go-46662601f417
Our series of blockchain tutorials has been very popular. The tutorials have been read tens of thousands of times and several hundred people have joined our Telegram community to give us feedback and ask questions.
The most common feedback people give is that our blog posts are useful in making complicated blockchain concepts simple and understandable. However, they are missing Peer-to-Peer functionality.
We’ve done everything from showing you how to code Proof of Work to letting you know how IPFS works. However, in each of our tutorials, we relied on a central server to show you these concepts.
We will now show you how to run a simple blockchain in a truly decentralized, Peer-to-Peer fashion. Let’s get started!
Background
What is Peer-to-Peer (P2P)?
In true Peer-to-Peer architecture, you don’t need a central server to maintain the state of a blockchain. For example, when you send your friend some Bitcoin, the “state” of the Bitcoin blockchain should be updated so your friend’s balance increases and your balance decreases.
There isn’t a central authority like a bank that maintains the state. Instead, all the nodes in the Bitcoin network that wish to maintain a copy of the Bitcoin blockchain update their copies of the blockchain to include your transaction. That way, as long as 51% of the nodes in the network “agree” on the state of the blockchain, it maintains its fidelity. Read more on this consensus concept here.
In this tutorial, we will rework our post, Code your own blockchain in less than 200 lines of Go! so it uses a Peer-to-Peer architecture instead of a central server. We strongly recommend reading it before proceeding. It will help you understand the forthcoming code.
Let’s get coding!
Coding a P2P network is no joke. It has a ton of edge cases and requires a lot of engineering to make it scalable and reliable. Like any good engineer, we will first see what tools are available as a starting point. Let’s stand on the shoulders of giants.
Luckily, there is an excellent P2P library written in Go called go-libp2p. Coincidentally, it is made by the same folks who created IPFS. If you haven’t checked out our IPFS tutorial, take a look here (don’t worry it’s not mandatory for this tutorial).
Warning
As far as we can tell, this go-libp2p library has 2 drawbacks:
Setup is a pain. It uses gx as a package manager that we don’t find very convenient.
It appears to still be under heavy development. When working with the code, you will encounter some minor data races. They have some kinks to iron out.
Don’t worry about #1. We’ll help you through it. #2 is a bigger issue but won’t affect our code. However, if you do notice data races, they are most likely coming from the underlying code of this library. So make sure to open an issue and let them know.
There are very few modern, open source P2P libraries available, particularly in Go. Overall, go-libp2p is quite good and it’s well suited to our objectives.
Setup
The best way to get your code environment set up is to clone this entire library and write your code within it. You can develop outside the environment they provide but it requires knowing how to work with gx. We’ll show you the easy way. Assuming you already have Go installed:
go get -d github.com/libp2p/go-libp2p/...
navigate to the cloned directory from above
make
make deps
This gets you all the packages and dependencies you need from this repo through the gx package manager. Again, we don’t like gx as it breaks a lot of Go convention (besides, why not just stick with go get?) but it’s worth the inconvenience to use this otherwise nice library.
We’re going to develop inside the examples subdirectory. So let’s create a directory under examples called p2p with
mkdir ./examples/p2p
Then navigate to your new p2p folder and create a main.go file. We will be writing all our code in this file.
Your directory tree should look like this:
Open up your main.go file and let’s start writing our code!
Imports
Let’s make our package declaration and list our imports. Most of these imports are packages provided by the go-libp2p library. You’ll learn how to use each of them throughout this tutorial.
package main
import (
"bufio"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
mrand "math/rand"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
golog "github.com/ipfs/go-log"
libp2p "github.com/libp2p/go-libp2p"
crypto "github.com/libp2p/go-libp2p-crypto"
host "github.com/libp2p/go-libp2p-host"
net "github.com/libp2p/go-libp2p-net"
peer "github.com/libp2p/go-libp2p-peer"
pstore "github.com/libp2p/go-libp2p-peerstore"
ma "github.com/multiformats/go-multiaddr"
gologging "github.com/whyrusleeping/go-logging"
)
The spew package is a simple convenience package to pretty print our blockchain. Make sure to:
go get github.com/davecgh/go-spew/spew
Blockchain stuff
Remember! Read this tutorial before proceeding. The following section will be much easier to understand after reading it!
Let’s declare our global variables.
// Block represents each 'item' in the blockchain
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
// Blockchain is a series of validated Blocks
var Blockchain []Block
var mutex = &sync.Mutex{}
Block
is the transaction info we want. We are using BPM (beats per minute, or our pulse rate) as the key data point going in each block. Take your pulse rate and remember that number. Remember, we’re a healthcare company so we’re not going to use boring financial transactions as our block data ?
Blockchain
is our “state”, or the latest blockchain, which is just a slice of Block
We declare a mutex
so we can control for and prevent race conditions in our code
Write out the following blockchain-specific functions.
// make sure block is valid by checking index, and comparing the hash of the previous block
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
// SHA256 hashing
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
// create a new block using previous block's hash
func generateBlock(oldBlock Block, BPM int) Block {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)
return newBlock
}
isBlockValid
checks to see that the chain of hashes in each block of the blockchain is consistentcalculateHash
uses sha256 to hash raw datagenerateBlock
creates a new block to be added to the blockchain, with the necessary transaction info inside itP2P Stuff
Host
Now we get to the meat of our tutorial. The first thing we want to do is write the logic that allows a host to be created. When a node runs our Go program, it should act as a host to which other nodes (or peers) can connect. Here’s the code. Don’t stress, we’ll walk you through it ?
// makeBasicHost creates a LibP2P host with a random peer ID listening on the
// given multiaddress. It will use secio if secio is true.
func makeBasicHost(listenPort int, secio bool, randseed int64) (host.Host, error) {
// If the seed is zero, use real cryptographic randomness. Otherwise, use a
// deterministic randomness source to make generated keys stay the same
// across multiple runs
var r io.Reader
if randseed == 0 {
r = rand.Reader
} else {
r = mrand.New(mrand.NewSource(randseed))
}
// Generate a key pair for this host. We will use it
// to obtain a valid host ID.
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
}
// Build host multiaddress
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
// Now we can build a full multiaddress to reach this host
// by encapsulating both addresses:
addr := basicHost.Addrs()[0]
fullAddr := addr.Encapsulate(hostAddr)
log.Printf("I am %s\n", fullAddr)
if secio {
log.Printf("Now run \"go run main.go -l %d -d %s -secio\" on a different terminal\n", listenPort+1, fullAddr)
} else {
log.Printf("Now run \"go run main.go -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
}
return basicHost, nil
}
Our makeBasicHost
function takes 3 arguments and returns the host and an error (which is nil if no errors are encountered).
listenPort
is the port we specify in our command line flags that other peers connect tosecio
is a boolean that turns on and off secure data streams. It’s generally a good idea to use it. It stands for “secure input/output”randSeed
is an optional command line flag that allows us to supply a seed to create a random address for our host. We won’t use this but it’s nice to have.The function’s first if
statement determines whether the seed was supplied and generates keys for our host accordingly. We then generate our public and private keys so our host stays secure. The opts section starts to construct the address to which other peers can connect.
The !secio
part bypasses encryption but we’re going to use secio for security so this line doesn’t apply to us for now. It’s fine to have this option though.
We then create the host and finalize the addresses that other peers can connect to. The log.Printf
part at the end is the helpful console message we print that tells a new node how to connect to the host we just created.
We then return the fully created host to the caller of the function. We now have our host!
Stream handler
We need to allow our host to deal with incoming data streams. When another node connects to our host and wants to propose a new blockchain to overwrite our own, we need logic to determine whether or not we should accept it.
When we add blocks to our blockchain, we want to broadcast it to our connected peer, so we need logic to do that as well.
Let’s create the skeleton of our handler.
func handleStream(s net.Stream) {
log.Println("Got a new stream!")
// Create a buffer stream for non blocking read and write.
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
go readData(rw)
go writeData(rw)
// stream 's' will stay open until you close it (or the other side closes it).
}
We create a new ReadWriter since we need both read and write and we spin up separate Go routines to deal with the read and write logic.
Read
Let’s create our readData function first.
func readData(rw *bufio.ReadWriter) {
for {
str, err := rw.ReadString('\n')
if err != nil {
log.Fatal(err)
}
if str == "" {
return
}
if str != "\n" {
chain := make([]Block, 0)
if err := json.Unmarshal([]byte(str), &chain); err != nil {
log.Fatal(err)
}
mutex.Lock()
if len(chain) > len(Blockchain) {
Blockchain = chain
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
log.Fatal(err)
}
// Green console color: \x1b[32m
// Reset console color: \x1b[0m
fmt.Printf("\x1b[32m%s\x1b[0m> ", string(bytes))
}
mutex.Unlock()
}
}
}
Our function is an infinite loop since it needs to stay open to incoming blockchains. We parse our incoming blockchain from a peer which is just a string of a JSON blob with ReadString
. If it’s not empty (!= “\n”)
we Unmarshal
the blob first.
Then we check to see if the length of the incoming chain is longer than the blockchain we’re storing ourselves. For our purposes, we are simply going by blockchain length to determine who wins out. If the incoming chain is longer than ours, we’ll accept it as the latest network state (or the latest, “true” blockchain).
We’ll Marshal
it back into a JSON format so it’s easier to read then we print it to our console. The fmt.Printf
command prints in a different color so it’s easy for us to know it’s a new chain.
Now we’ve accepted our peer’s blockchain, if we add a new block to our blockchain, we need a way to let our connected peer know about it, so they can accept ours. We do that with our writeData
function.
Write
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()
mutex.Lock()
rw.WriteString(fmt.Sprintf("%s\n", string(bytes)))
rw.Flush()
mutex.Unlock()
}
}()
stdReader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
sendData, err := stdReader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
sendData = strings.Replace(sendData, "\n", "", -1)
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()
}
bytes, err := json.Marshal(Blockchain)
if err != nil {
log.Println(err)
}
spew.Dump(Blockchain)
mutex.Lock()
rw.WriteString(fmt.Sprintf("%s\n", string(bytes)))
rw.Flush()
mutex.Unlock()
}
}
We start the function off with a Go routine that broadcasts the latest state of our blockchain every 5 seconds to our peers. They’ll receive it and throw it away if the length is shorter than theirs. They’ll accept it if it’s longer. Either way, all the peers are constantly getting their blockchains updated by the latest state of the network.
We now need a way to create a new block with the pulse rate (BPM) we took earlier. We create a new reader with bufio.NewReader so it can read our stdin (console input). We want to be able to continually add new blocks so we put this in an infinite loop.
We do a little bit of string manipulation to make sure the BPM we type in is an integer and properly formatted to be added as a new block. We go through our standard blockchain functions (see the “Blockchain stuff” section above). Then we Marshal it so it looks pretty, print it to our console to verify with spew.Dump. We then broadcast it to our connected peer with rw.WriteString.
Great! We’ve now finished our blockchain functions and most of our P2P functions. We’ve created our handler, and the read and write logic to deal with incoming and outgoing blockchains. Through these functions we’ve created a way for each peer to continually check the state of their blockchain against each other and collectively, they all get updated to the latest state (longest valid blockchain).
All that’s left now is wiring up our main function.
Main function
Here is our main function. Have a look through it first then we’ll go through it step by step.
func main() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
Blockchain = append(Blockchain, genesisBlock)
// LibP2P code uses golog to log messages. They log with different
// string IDs (i.e. "swarm"). We can control the verbosity level for
// all loggers with:
golog.SetAllLoggers(gologging.INFO) // Change to DEBUG for extra info
// Parse options from the command line
listenF := flag.Int("l", 0, "wait for incoming connections")
target := flag.String("d", "", "target peer to dial")
secio := flag.Bool("secio", false, "enable secio")
seed := flag.Int64("seed", 0, "set random seed for id generation")
flag.Parse()
if *listenF == 0 {
log.Fatal("Please provide a port to bind on with -l")
}
// Make a host that listens on the given multiaddress
ha, err := makeBasicHost(*listenF, *secio, *seed)
if err != nil {
log.Fatal(err)
}
if *target == "" {
log.Println("listening for connections")
// Set a stream handler on host A. /p2p/1.0.0 is
// a user-defined protocol name.
ha.SetStreamHandler("/p2p/1.0.0", handleStream)
select {} // hang forever
/**** This is where the listener code ends ****/
} else {
ha.SetStreamHandler("/p2p/1.0.0", handleStream)
// The following code extracts target's peer ID from the
// given multiaddress
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)
}
// Decapsulate the /ipfs/ part from the target
// /ip4//ipfs/ becomes /ip4/
targetPeerAddr, _ := ma.NewMultiaddr(
fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerid)))
targetAddr := ipfsaddr.Decapsulate(targetPeerAddr)
// We have a peer ID and a targetAddr so we add it to the peerstore
// so LibP2P knows how to contact it
ha.Peerstore().AddAddr(peerid, targetAddr, pstore.PermanentAddrTTL)
log.Println("opening stream")
// make a new stream from host B to host A
// it should be handled on host A by the handler we set above because
// we use the same /p2p/1.0.0 protocol
s, err := ha.NewStream(context.Background(), peerid, "/p2p/1.0.0")
if err != nil {
log.Fatalln(err)
}
// Create a buffered stream so that read and writes are non blocking.
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
// Create a thread to read and write data.
go writeData(rw)
go readData(rw)
select {} // hang forever
}
}
We start off by creating a Genesis block, which is our seed block for our blockchain. Again, if you read our previous tutorial this should be review.
We use the go-libp2p library’s logger to handle logging with SetAllLoggers. This is optional.
We then set all our command line flags.
secio we covered previously and allows for secure streams. We’ll make sure to always use this by setting the flag when running our program.
target lets us specify the address of another host we want to connect to, which means we’re acting as a peer to a host if we use this flag.
listenF opens the port we want to allow connections to, which means we’re acting as a host. We can be both a host (receive connections) and a peer (connect to other hosts). This is what makes this system truly P2P!
seed is the optional randomness seeder used to construct our address that other peers can use to connect to us.
We then create a new host with the makeBasicHost function we created earlier. If we’re only acting as a host (i.e. we’re not connecting to other hosts) we specify that with if *target == “” , fire up our handler with the setStreamHandle function we made earlier and that’s the end of our listener code.
If we do want to connect to another host we move onto the else section. We set our handler again, since we’re acting as a host AND a connecting peer.
The next few lines deconstruct the string we supply to target so we can find the host we want to connect to. This is also called Decapsulation.
We end up with the peerID and target address targetAddr of the host we want to connect to and add that record into our “store” so we can keep track of who we’re connected to. We do that with ha.Peerstore().AddAddr
We then create our connection stream to the peer we want to connect to with ha.NewStream. We want to be able to receive and send them streams of data (our blockchain) so just like we did in our handler, we create a ReadWriter and spin up separate Go routines for readData and writeData. We finish off by blocking with an empty select statement so our program doesn’t just finish and quit.
Wahoo!
Guess what? We’re done! I know that was a bit of a handful but considering how complicated P2P engineering is you should proud that you made it to the end! That wasn’t too bad was it?
Complete Code
Here is the code in full.
https://github.com/mycoralhealth/blockchain-tutorial/blob/master/p2p/main.go
Now let’s get to what we’re all here for and try this out! We’re going to be using 3 separate terminals to act as individual peers.
Before starting the app, do yourself a favor and go to the root directory of go-libp2p and run make deps again. This makes sure your dependencies are in order. Again, this library uses the annoying gx package manager and we have to do a couple things to make it play nice.
Go back to your working directory.
On your first terminal, go run main.go -l 10000 -secio
Terminal 1
Follow the instructions that say “Now run…”. Open up a 2nd terminal, go to the same directory and go run main.go -l 10001 -d
Terminal 2
You’ll see the first terminal detected the new connection!
Terminal 1
Now following the instructions in the 2nd terminal, open up a 3rd terminal, go to the same working directory and go run main.go -l 10002 -d
Terminal 3
Check out the 2nd terminal, which detected the connection from the 3rd terminal.
Terminal 2
Now let’s start inputting our BPM data. Type in “70”
in our 1st terminal, give it a few seconds and watch what happens in each terminal.
Terminal 1
Terminal 2
Terminal 3
What just happened here? It’s really cool so let’s think it through.
Terminal 1 added a new block to its blockchain
It then broadcast it to Terminal 2
Terminal 2 compared it against its own blockchain, which only contained its genesis block. It saw Terminal 1 had a longer chain so it replaced its own chain with Terminal 1’s chain. Then it broadcast the new chain to Terminal 3.
Terminal 3 compared the new chain against its own and replaced it.
All 3 terminals updated their blockchains to the latest state with no central authority! This is the power of Peer-to-Peer.
Let’s test it again but this time allow Terminal 2 to add a block. Type in “80” into Terminal 2.
Terminal 2
Terminal 1
Terminal 3
Awesome! As expected, this time Terminal 2 added a new block and broadcast it to the rest of the network. Each of the peers ran its own internal checks and updated their blockchains to the latest blockchain of the network!
Next Steps
Take a breather and enjoy your handy work. You just coded up a simple but fully functioning P2P blockchain in just a few hundred lines of code. This is no joke. P2P programming is very complicated, which is why you don’t see many simple tutorials out there on how to create your own P2P network.
There are some improvements and caveats to take away. Challenge yourself and try to tackle one or more of these:
As mentioned, the go-libp2p library is not perfect. We were careful to ensure our own code has minimal (if any) data races but while we were testing this library we noticed they had some data races in their code. It didn’t affect the results we were expecting to see but be extra cautious if you’re using this external library in production. Let them know about bugs you find as their library is a work in progress.
Try incorporating consensus mechanisms to this tutorial. We just did a simple check to see which blockchain was the longest, but you could incorporate either our Proof of Work or Proof of Stake tutorials to have a more robust check.
Add persistence to the code. Right now, for simplicity, we shut down all the nodes if you kill one of them. You can leave all other nodes running even if one closes. If one node dies, it should be able to relay the info in its Peer Store to its connected nodes so they can connect to each other.
This code hasn’t been tested with hundreds of nodes. Try writing a shell script to scale many nodes and see how performance is affected. If you notice any bugs, make sure to file an issue or submit a pull request in our Github repo!
Look into node discovery. How do new nodes find existing nodes? Here’s a good place to start.