在Go中编写一个简单的P2P区块链!
转载:https://medium.com/@mycoralhealth/code-a-simple-p2p-blockchain-in-go-46662601f417(需翻墙)
背景
什么是点对点(P2P)?
在真正的Peer-to-Peer体系结构中,您不需要中央服务器来维护区块链的状态。例如,当您向朋友发送一些比特币时,比特币区块链的“状态”应该更新,以便您朋友的余额增加,您的余额减少。
没有像银行这样维护国家的中央机关。相反,比特币网络中希望维护比特币区块链副本的所有节点都会更新区块链的副本以包含您的交易。这样,只要网络中51%的节点“同意”区块链的状态,它就会保持其保真度。了解更多关于这一共识的概念在这里。
在本教程中,我们将重新编写我们的帖子,在不到200行Go中编写您自己的区块链!所以它使用点对点架构而不是中央服务器。我们强烈建议您在继续之前阅读它。它将帮助您理解即将推出的代码。
我们来编码吧!
编码P2P网络不是开玩笑。它有大量边缘情况,需要大量工程才能使其具有可扩展性和可靠性。像任何优秀的工程师一样,我们将首先看到可用的工具作为起点。让我们站在巨人的肩膀上。
幸运的是,有一个优秀的P2P库用Go编写,名为go-libp2p。巧合的是,它是由创建IPFS的同一个人创建的。如果您尚未查看我们的IPFS教程,请查看此处(不要担心本教程不是必需的)。
警告
据我们所知,这个go-libp2p
库有两个缺点:
- 安装很痛苦。它
gx
用作包管理器,我们发现它们不太方便。 - 它似乎仍处于重大发展阶段。使用代码时,您将遇到一些小的数据争用。他们有一些纠结可以解决问题。
不要担心#1。我们会帮助您完成它。#2是一个更大的问题,但不会影响我们的代码。但是,如果您确实注意到数据竞争,则它们很可能来自此库的基础代码。所以一定要打开一个问题让他们知道。
现有的开源P2P库很少,特别是在Go中。总的来说,go-libp2p
它非常好,非常适合我们的目标。
建立
设置代码环境的最佳方法是克隆整个库并在其中编写代码。您可以在他们提供的环境之外进行开发,但需要知道如何使用gx
。我们将向您展示简单的方法。假设你已经转到安装:
go get -d github.com/libp2p/go-libp2p/...
从上面导航到克隆目录
make
make deps
这将通过gx包管理器从您的repo获取所需的所有包和依赖项。同样,我们不喜欢gx它,因为它打破了很多Go惯例(此外,为什么不坚持go get?)但是使用这个不错的库是不值得的。
我们将在examples子目录中进行开发。所以让我们创建一个examples名为p2pwith 的目录
mkdir ./examples/p2p
然后导航到新p2p文件夹并创建一个main.go文件。我们将在此文件中编写所有代码。
您的目录树应如下所示:
打开你的main.go文件,让我们开始编写我们的代码吧!
进口
让我们制作包装声明并列出我们的进口。这些导入中的大多数是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表示区块链中的每个“项目”
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
// 区块链是一系列经过验证的区块
var Blockchain []Block
var mutex = &sync.Mutex{}
我们想要的交易信息。我们使用BPM(每分钟节拍或我们的脉搏率)作为每个块中的关键数据点。记下你的脉搏率并记住这个数字。请记住,我们是一家医疗保健公司,因此我们不会将枯燥的金融交易用作我们的块数据;-)
Blockchain 是我们的“国家”,或最新的区块链,这只是一小部分 Block
我们声明了一个,mutex所以我们可以控制和防止我们的代码中的竞争条件
写出以下区块链特定功能。
// 通过检查索引并比较前一个块的哈希来确保块有效
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)
}
// 使用前一个块的哈希创建一个新块
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创建一个LibP2P主机,其上有一个随机对等ID
//给出多地址。如果secio为true,它将使用secio。
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))
}
//为此主机生成密钥对。我们会用它
//获取有效的主机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
}
//构建主机多地址
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
//现在我们可以构建一个完整的多地址来访问这个主机
//通过封装两个地址:
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!")
//为非阻塞读写创建缓冲区流。
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
go readData(rw)
go writeData(rw)
// stream's'将保持打开状态,直到你关闭它(或另一边关闭它)。
}
我们创建一个新的,ReadWriter
因为我们需要读取和写入,我们启动单独的去例程来处理读写逻辑。
读
让我们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)
}
// 绿色控制台颜色: \x1b[32m
// 重置控制台颜色: \x1b[0m
fmt.Printf("\x1b[32m%s\x1b[0m> ", string(bytes))
}
mutex.Unlock()
}
}
}
我们的函数是一个无限循环,因为它需要对传入的区块链保持开放。我们从一个对等体解析传入的区块链,这只是一个JSON blob
的字符串ReadString
。如果它不是空的(!= “\n”)我们Unmarshal
首先是blob
。
然后我们检查传入链的长度是否比我们自己存储的区块链长。出于我们的目的,我们只需按区块链长度来确定谁胜出。如果传入的链比我们的更长,我们会接受它作为最新的网络状态(或最新的“真正的”区块链)。
我们将Marshal
它恢复为JSON
格式,因此它更容易阅读,然后我们将它打印到我们的控制台。该fmt.Printf
命令以不同的颜色打印,因此我们很容易知道它是一个新的链条。
现在我们已经接受了我们同行的区块链,如果我们在区块链中添加一个新区块,我们需要一种方法让我们的连接对手了解它,这样他们就可以接受我们的区块链了。我们用我们的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.NewReader
因此它可以读取我们的stdin
(控制台输入)。我们希望能够不断添加新块,因此我们将其置于无限循环中。
我们做了一些字符串操作,以确保我们输入的BPM是一个整数,并且格式正确,可以作为新块添加。我们通过标准的区块链功能(参见上面的“ 区块链内容”部分)。然后我们Marshal 它看起来很漂亮,打印到我们的控制台进行验证spew.Dump
。然后我们将它广播给我们的连接对等体rw.WriteString
。
大!我们现在已经完成了区块链功能和大多数P2P功能。我们已经创建了我们的处理程序,以及处理传入和传出区块链的读写逻辑。通过这些函数,我们为每个对等体创建了一种方法,可以不断地检查区块链相互之间的状态,并且它们共同更新到最新状态(最长的有效区块链)。
现在剩下的就是连接我们的main
功能。
主功能
这是我们的主要功能。先看看它然后我们将逐步完成它。
func main() {
t := time.Now()
genesisBlock := Block{}
genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
Blockchain = append(Blockchain, genesisBlock)
// LibP2P代码使用golog来记录消息。他们用不同的日志登录
//字符串ID(即“swarm”)。我们可以控制详细程度
//所有记录器:
golog.SetAllLoggers(gologging.INFO) //更改到调试的额外信息
//从命令行解析选项
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")
}
//创建一个侦听给定多地址的主机
ha, err := makeBasicHost(*listenF, *secio, *seed)
if err != nil {
log.Fatal(err)
}
if *target == "" {
log.Println("listening for connections")
//在主机A上设置流处理程序./p2p/1.0.0是
//用户定义的协议名称。
ha.SetStreamHandler("/p2p/1.0.0", handleStream)
select {} //永远挂起
/ * ***这是听众代码结束的地方*** * /
} else {
ha.SetStreamHandler("/p2p/1.0.0", handleStream)
//以下代码从中提取目标的对等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)
}
//从目标中解封/ ipfs / <peerID>部分
// / ip4 / <abcd> / ipfs / <peer>变为/ ip4 / <abcd>
targetPeerAddr, _ := ma.NewMultiaddr(
fmt.Sprintf("/ipfs/%s", peer.IDB58Encode(peerid)))
targetAddr := ipfsaddr.Decapsulate(targetPeerAddr)
//我们有一个peer ID和一个targetAddr,所以我们将它添加到peerstore
//所以LibP2P知道如何联系它
ha.Peerstore().AddAddr(peerid, targetAddr, pstore.PermanentAddrTTL)
log.Println("opening stream")
//从主机B创建一个新流到主机A.
//它应该由我们在上面设置的处理程序在主机A上处理,因为
//我们使用相同的/p2p/1.0.0协议
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 {} //永远挂起
}
}
我们首先创建一个Genesis
块,它是我们区块链的种子块。同样,如果您阅读我们之前的教程,则应该进行审核。
我们使用go-libp2p
库的记录器来处理日志记录SetAllLoggers
。这是可选的。
然后我们设置所有命令行标志。
-
secio
我们之前介绍过并允许安全流。我们将确保在运行程序时通过设置标志来始终使用它。 -
target
让我们指定我们想要连接的另一个主机的地址,这意味着如果我们使用这个标志,我们将充当主机的对等体。 -
listenF
打开我们想要允许连接的端口,这意味着我们充当主机。我们既可以是主机(接收连接),也可以是对等体(连接到其他主机)。这就是使这个系统真正成为P2P的原因! -
seed
是用于构造我们的地址的可选随机播种器,其他对等体可以使用它来连接我们。
然后,makeBasicHost
我们使用我们之前创建的函数创建一个新主机。如果我们只是作为主机(即我们没有连接到其他主机),我们指定with if *target == “”
,使用setStreamHandle
我们之前创建的函数启动我们的处理程序,这就是我们的侦听器代码的结束。
如果我们确实想要连接到另一个主机,我们将移动到该else
部分。我们再次设置处理程序,因为我们充当主机和连接对等体。
接下来的几行解构了我们提供的字符串,target
这样我们就可以找到想要连接的主机。这也称为解封装。
我们最终得到了我们想要连接的主机的peerID
目标地址targetAddr
,并将该记录添加到我们的“商店”中,以便我们可以跟踪我们连接到的人。我们这样做ha.Peerstore().AddAddr
然后,我们创建连接流到我们想要连接的对等体ha.NewStream
。我们希望能够接收和发送数据他们(我们blockchain)的视频流,以便就像我们在我们的处理程序一样,我们创建了一个ReadWriter
与旋转起来单独去例程readData
和writeData
。我们通过阻止一个空的select
声明结束,所以我们的程序不只是完成并退出。
嗬!
试跑
你猜怎么着?我们完成了!我知道这有点少,但考虑到P2P工程有多复杂,你应该感到自豪,因为你把它做到了最后!这不是太糟糕了吗?
试驾
现在让我们来看看我们在这里的所有内容并尝试一下吧!我们将使用3个独立的终端作为个人同行。
在启动应用程序之前,请帮自己一个忙,然后转到根目录go-libp2p并make deps再次运行。这可以确保您的依赖项有序。同样,这个库使用恼人的gx包管理器,我们必须做一些事情才能使它发挥得很好。
回到你的工作目录。
在你的第一个终端上,
go run main.go -l 10000 -secio
按照说“正在运行...”的说明进行操作。打开第二个终端,转到同一目录
go run main.go -l 10001 -d <given address in the instructions> -secio
您将看到第一个终端检测到新连接!
现在按照第二个终端的说明打开第三个终端,进入同一个工作目录
go run main.go -l 10002 -d <given address in the instructions> -secio
现在让我们开始输入我们的BPM数据。在我们的第一个终端输入“70”,给它几秒钟,观察每个终端发生的情况。
刚刚发生了什么?这真的很酷,让我们一起来思考。
1号航站楼为其区块链增加了一个新区块
然后它将它广播到2号航站楼
2号航站楼将其与自己的区块链进行了比较,区块链仅包含其成因区块。它看到1号航站楼有一条较长的链条,因此它用1号航站楼的链条取代了自己的链条。然后它将新链传播到3号航站楼。
3号航站楼将新链与自己的链相比较而取而代之。
所有3个终端都将其区块链更新为最新状态,没有中央权限!这是Peer-to-Peer的力量。
下一步
休息一下,享受您的便利工作。您只需在几百行代码中编写一个简单但功能齐全的P2P区块链。这不是开玩笑。P2P编程非常复杂,这就是为什么你没有看到很多关于如何创建自己的P2P网络的简单教程。
有一些改进和警告可以带走。挑战自己并尝试解决其中一个或多个问题:
- 如上所述,
go-libp2p
图书馆并不完美。我们小心翼翼地确保我们自己的代码有最小的(如果有的话)数据竞赛,但是当我们测试这个库时,我们注意到他们在代码中有一些数据竞争。它并没有影响我们期望看到的结果,但如果你在生产中使用这个外部库,则要格外小心。让他们知道你发现的错误,因为他们的图书馆正在进行中。 - 尝试将共识机制纳入本教程。我们只是做了一个简单的检查,看看哪个区块链是最长的,但你可以结合我们的工作证明或证明教程教程,以获得更强大的检查。
- 为代码添加持久性。现在,为简单起见,如果您杀死其中一个节点,我们会关闭所有节点。即使关闭,您也可以让所有其他节点保持运行。如果一个节点死亡,它应该能够将其对等存储中的信息中继到其连接的节点,以便它们可以相互连接。
- 此代码尚未经过数百个节点的测试。尝试编写shell脚本来扩展许多节点,并了解性能如何受到影响。如果您发现任何错误,请务必在我们的Github回购中提交问题或提交拉取请求!
- 查看节点发现。新节点如何找到现有节点?这是一个很好的起点。