你是否已经加入我们的电报群了?如果没有的话,现在加入吧 :-) ,如果你在阅读这篇教程的时候遇到了问题,也可以通过电报群向我们咨询。
我们的一系列区块链教程已经非常流行了。这些教程的阅读量已达到数万次并且有几百个用户已经加入我们的电报群社区并给我们反馈和提问。
人们给出的最常见的反馈是我们的文章使得非常复杂的区块链概念变得简单易懂了。可是,它们缺失了点对点(p2p)功能。
在向你展示如何实现PoW算法这篇文章中,使你知道IPFS(分布式文件系统)是如何工作的。只是,在我们之前的任何一篇教程里,都仅依靠一个中心化的服务器来展示这些概念。
我们将要向你展示一个去中心化的、P2P形式的区块链是如何工作的。让我们开始吧!
背景
What is Peer-to-Peer (P2P)?
什么是点对点(P2P)?
在真实的P2P架构里,你不需要一个中心化的服务器去维持一个区块链的状态。例如,当你给你的朋友发送一些比特币的时候,这个比特币区块链的状态必须要被更新,因此你朋友的余额会增加,你的账户余额会被减少。
这儿没有一个像银行一样的权威机构来维持区块链的状态。取而代之的是,在比特币网络上的所有节点都希望维持一个包含你的交易在内的副本去更新它们自己的副本。这样的话,只要网络中51%的节点“同意”区块链的状态,就能达到共识。可以在这里阅读更多关于这个共识的概念。
在这篇教程里,我们将重构那篇:用不到200行代码,实现一个go语言区块链。它将使用P2P架构取代一个中心化服务器,我们强烈建议你在开始编写代码之前阅读它。它将帮助你理解接下来的代码。
让我们来获取代码!
编写一个p2p网络的的代码可不是闹着玩的,它有大量的边缘情况并且需要大量的工程师来保证其可扩展性和可靠性。像任何好的工程师一样,我们在开始之前首先看看有哪些可用的工具,让我们站在巨人的肩膀上。
幸运的是,这儿有一个用go写的优秀的p2p类库,叫做go-libp2p。巧合的是,它的作者跟创造IPFS的是同一个人。如果你还未查验我们的IPFS教程,可以去这里看看(不用担心,这个教程不是强制的)。
警告
据我们所知,这个go-libp2p
类库有2个弊端:
安装是一种痛苦。它使用gx作为一个包管理器,我们觉得不太方便。
它似乎仍然处于密集的开发中。当使用代码时,您将遇到一些较小的数据竞争。他们有一些纠结。
不必担心#1
,我们会帮你度过难关的。#2
是一个更大的问题,不过不会影响我们的代码。但是,如果你注意到数据竞争,它们很可能来自这个库的底层代码。所以一定要打开一个问题,让他们知道。
目前很少有可用的开源P2P库,尤其是在Go中。总体来说,go-libp2p
相当好,非常适合我们的目标。
安装
获取代码环境的最佳方法是克隆整个库并在其中编写代码。你也可以在他们提供的环境之外开发,但它需要知道如何使用gx工作。我们会告诉你简单的方法。假设您已经安装了Go:
go get -d github.com/libp2p/go-libp2p/...
navigate to the cloned directory from above
切换到上面的目录
make
make deps
通过gx包管理器,您可以从中获得所有需要的包和依赖项。再说一遍,我们不喜欢gx
,因为它打破了很多Go约定(另外,为什么不坚持go get
?)但是为了使用这个好的类库是值得的。
我们将在示例的examples
子目录里面开发,因此让我们在examples
里面,用下面的命令,创建一个叫做p2p
的子目录
mkdir ./examples/p2p
接下来切换到你新建的p2p
文件夹中,然后创建一个main.go
文件。我们接下来所有的代码都将写在这个文件里。
你的目录树看起来应该像这样:
打开你的main.go
文件,让我们开始写代码吧!
导入
让我们先做包声明并列出我们的imports。这些imports大部分是由go-libp2p
库提供的包。在本教程中,您将学习如何使用它们。
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"
)
spew
是一个简单方便的包,可以很好地打印我们的区块链。确保:
go get github.com/davecgh/go-spew/spew
区块链部分
记得!在继续之前阅读这篇教程。下面的部分在阅读之后会更容易理解!
我们来声明全局变量。
// 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
是我们想要的交易信息。我们使用BPM(每分钟心跳或脉冲速率)作为每个块中的关键数据点。记住你的脉搏,记住这个数字。请记住,我们是一家医疗保健公司,所以我们不会使用无聊的财务交易作为我们的数据块;Blockchain
是我们的“状态”,或者是最新的块链,它只是一系列的Block
。我们声明了
mutex
,这样我们就可以控制和防止代码中的竞争条件。
写出下面的blockchain-specific函数。
// 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
检查区块链中每个块的哈希是否一致。calculateHash
使用sha256算法散列原始数据generateBlock
创建要添加到区块链中的新块,其中包含必要的交易信息
p2p 部分
主机
现在我们进入到本教程最核心的部分。我们要做的第一件事是编写允许创建主机的逻辑。当一个节点运行我们的Go程序时,它应该充当其他节点(或对等体)可以连接的主机。这是代码。别紧张,我们会陪你走过的:-)
// 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
}
我们的makeBasicHost
函数需要3个参数,并返回主机和一个错误(如果没有遇到错误,则为nil)。
listenPort
是我们在命令行标志中指定的端口,可以被其他节点连接到secio
是一个打开和关闭安全数据流的布尔变量。使用它通常是个好主意。它代表“安全输入/输出”。randSeed
是一个可选的命令行标志,允许我们提供种子来为主机创建随机地址。我们不会用这个,但是很好。
函数的第一个if
语句确定种子是否被提供,并相应地为主机生成密钥。然后,我们生成我们的公共密钥和私有密钥,这样我们的主机就保持安全。opts
部分开始构造其他节点可以连接的地址。
这个!secio
部分绕过加密,但我们将使用secio
保证安全,所以这行代码现在不适用于我们。尽管如此,还是有选择的。
然后,我们创建主机,并完成其他节点可以连接到的地址。最后,log.Printf
部分是我们打印有用的控制台消息,它告诉新节点如何连接到刚刚创建的主机。
然后,我们将完全创建的主机返回给函数的调用方。我们现在有自己的主机了!
流处理
我们需要让我们的主机处理传入的数据流。当另一个节点连接到我们的主机并希望提出一个新的块链来覆盖我们自己的节点时,我们需要逻辑来确定我们是否应该接受它。
当我们将块添加到我们的区块链时,我们想把它广播到我们连接的对等体上,所以我们也需要逻辑来实现。
让我们创建处理器的骨架.
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).
}
我们创建了一个新的ReadWriter
,因为我们既需要读又需要写,并且我们创建单独的Go例程来处理读和写逻辑。
读取
我们先创建readData函数。
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()
}
}
}
我们的函数是无限循环的,因为它需要对进入的区块链保持开放。我们从一个对等体解析传入的区块链,它只是一个带有ReadString
的JSON blob字符串。如果不是空的话(!= “\n”
)我们首先解析blob。
然后我们检查进来的链的长度是否比我们储存的区块链长。为了我们的目的,我们只是通过链长来确定谁赢了。如果传入链比我们长,我们将接受它作为最新的网络状态(或者最新的“真”块链)。
我们会把它Marshal
成JSON格式,这样就更容易阅读了,然后我们把它打印到控制台。fmt.Primtf
命令以不同的颜色打印,所以我们很容易知道它是一个新的链。
现在我们已经接受了我们的对等方区块链,如果我们在块链中添加一个新的块,我们需要一种方法让我们连接的对等方知道它,这样他们就可以接受我们的。我们用我们的writeData
函数来实现这一点。
写入
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()
}
}
我们用一个Go例程启动函数,它每隔5秒广播我们的区块链的最新状态给我们的对等体。如果长度比他们的短,他们会接受并扔掉。如果更长,他们会接受的。无论是哪种方式,所有的对等体都在不断地通过网络的最新状态来更新他们的链链。
我们现在需要一种方法来创建一个新的块与脉冲速率(BPM)。我们用BuFio.NeadReader
创建一个新的阅读器,这样它就可以读取我们的stdin
(控制台输入)。我们希望能够不断地添加新的块,所以把它放在一个无限的循环中。
我们进行一些字符串操作,以确保输入的BPM是一个整数,并且格式正确,可以添加为新块。我们通过我们的标准BangLink函数(见上面的“Blockchain stuff”部分)。然后,我们Marshal
它,使它看起来漂亮,打印到我们的控制台,用spew.Dump
验证。然后我们用rw.WriteString
将它广播到我们的连接的对等体。
很棒!现在我们已经完成了区块链函数和大部分P2P函数。已经创建了我们的处理程序和读写逻辑来处理输入和输出的块链。通过这些函数,我们已经为每个对等点创建了一种方法,以连续地相互检查其块链的状态,并且在同一时间,它们都被更新到最新状态(最长的有效块链)。
现在剩下的就是布线我们的main函数。
Main 函数
这是我们的main函数。先整体看一下,然后我们一步一步的来看。
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/<peerID> part from the target
// /ip4/<a.b.c.d>/ipfs/<peer> becomes /ip4/<a.b.c.d>
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
}
}
我们首先创建一个创始块,这是我们的种子块为我们的链链。再次,如果你读我们以前的教程,这应该是重审。
我们使用go-libp2p
库的记录器来处理SetAllLoggers
日志记录。这是可选的。
然后,我们设置所有的命令行标志。
secio
我们以前覆盖并允许安全流。我们将确保在运行程序时始终使用这个标志。target
让我们指定要连接的另一主机的地址,这意味着如果使用此标志,我们将充当主机的对等方。listenF
打开了我们希望允许连接的端口,这意味着我们作为主机。我们既可以是主机(接收连接),也可以是对等体(连接到其他主机)。这就是这个系统真正成为P2P的原因!seed
是可选的随机播种器,用来构造我们的地址,其他节点可以用来连接我们。
然后,我们创建了一个新的主机,我们之前创建了makeBasicHost
函数。如果我们只充当主机(即,我们没有连接到其他主机),我们指定如果*target==“”
,则使用我们之前创建的setStreamHandle
函数启动处理程序,这是我们的侦听器代码的结束。
如果我们确实想要连接到另一个主机,我们移动到else
部分。我们再次设置我们的处理程序,因为我们作为一个主机和一个连接的对等体。
接下来的几行解构了我们提供给目标的字符串,这样我们就可以找到我们想要连接的主机。这也被称为去包裹。
我们最终得到要连接的主机的peerID
和目标地址targetAddr
,并将该记录添加到“存储”中,以便跟踪我们与谁连接。我们用ha.Peerstore().AddAddr
然后,我们使用ha.NewStream
创建想要连接到的对等体连接。我们希望能够接收和发送数据流(我们的区块链),因此就像我们在处理程序中做的那样,我们创建一个ReadWriter
,并为readData
和writeData
创建单独的Go例程。我们用一个空的select
语句完成阻塞,所以我们的程序不只是完成和退出。
哇哈!
你猜怎么着?我们搞定啦!我知道这只是一小部分,但是考虑到P2P工程是多么复杂,你应该为你最终成功而感到自豪!那不是太糟糕,难道不是吗?
完成
可以在这儿查看全部代码