用go语言实现一个简单的P2P区块链

你是否已经加入我们的电报群了?如果没有的话,现在加入吧 :-) ,如果你在阅读这篇教程的时候遇到了问题,也可以通过电报群向我们咨询。

我们的一系列区块链教程已经非常流行了。这些教程的阅读量已达到数万次并且有几百个用户已经加入我们的电报群社区并给我们反馈和提问。

人们给出的最常见的反馈是我们的文章使得非常复杂的区块链概念变得简单易懂了。可是,它们缺失了点对点(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,并为readDatawriteData创建单独的Go例程。我们用一个空的select语句完成阻塞,所以我们的程序不只是完成和退出。

哇哈!

你猜怎么着?我们搞定啦!我知道这只是一小部分,但是考虑到P2P工程是多么复杂,你应该为你最终成功而感到自豪!那不是太糟糕,难道不是吗?

完成

可以在这儿查看全部代码

mycoralhealth/blockchain-tutorial

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容