如果这不是您第一次读本文,请阅读第 2 部分 —— 这里!
本教程改编自这篇关于使用 JavaScript 编写基础区块链的优秀文章。我们已经将其移植到 Go 并添加了一些额外的好处 -- 比如在 Web 浏览器上查看您的区块链。如果您对下面的教程有任何疑问,请务必加入我们的 Telegram。可向我们咨询任何问题!
本教程中的数据示例将基于您的休息心跳。毕竟我们是一家医疗保健公司 :-) 为了有趣,记录您一分钟的脉搏数(每分钟的节拍)并记住这个数值。
世界上几乎每个开发者都听说过区块链,但大多数仍然不知道它的工作原理。他们可能仅仅是因为比特币才知道它,又或者是因为他们听说过智能合约之类的东西。这篇文章试图通过帮助您用 Go 编写自己的简单区块链,使用少于 200 行代码来揭开区块链的神秘面纱!到本教程结束时,您将能够编写并在本地运行您自己的区块链,以及在 Web 浏览器中查看它。
还有什么比通过创建自己的区块链来了解区块链更好的方法呢?
您将能够做什么
- 创建您自己的区块链
- 了解 hash 如何维护区块链的完整性
- 了解如何添加新块
- 了解当多个节点生成块时,tiebreakers 如何解决
- 在 web 浏览器中查看区块链
- 写新的块
- 获取区块链的基础知识,以便您可以决定您的旅程将从这里走向何处!
您不能做的事
为了保持本文的简单性,我们不会处理更高级的共识概念,比如工作证明和利害关系证明。为了让您查看您的区块链和区块的添加,我们将模拟网络交互,但网络广播作为文章的深度将被保留。
让我们开始吧!
准备工作
既然我们决定用 Go 编写代码,我们假设您已经有了一些 Go 方面的经验,在安装并配置 Go 之后,我们还需要获取以下软件包:
go get github.com/davecgh/go-spew/spew
Spew 允许我们在控制台中查看格式清晰的 structs
和 slices
,您值得拥有。
go get github.com/gorilla/mux
Gorilla/mux 是编写 Web 程序处理的常用包。我们将会使用它。
go get github.com/joho/godotenv
Gotdotenv 允许我们从根目录中读取 .env
文件,这样就不必对 HTTP 端口之类的内容进行硬编码。我们也需要这个。
我们在根目录中创建 .env
文件,定义为 http 请求提供服务的端口。只需要该文件添加一行:
ADDR=8080
创建 main.go
文件。从现在开始,所有的内容都会写进这个文件中,并且将用少于 200 代码进行编码。
导入
这是我们需要导入的以及包声明,我们把它们写入 main.go
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
复制代码
数据模型
我们将定义组成区块链的每个块的 struct
。别担心,我们会在一分钟内结束所有这些字段的含义。
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
复制代码
每个 Block
都包含将被写入区块链的数据,并表示当您获取脉搏率时的每一种情况(还记得您在文章的开头就这样做了么?)。
Index
是数据记录在区块链中的位置Timestamp
是自动确定写入数据的时间BPM
每分钟节拍数,是您的脉搏率Hash
是表示此数据记录的 SHA256 标识符PrevHash
是链中上一条记录的 SHA256 标识符
我们也对区块链本身建模,它只是 Block
中的 slice
:
var Blockchain []Block
复制代码
那么散列如何适合于块和区块链呢?我们使用散列表来识别和保持块的正确顺序。通过确保每个 Block
中的 PrevHash
与前面 Block
块中的 Hash
相同,我们知道组成链的块的正确顺序。
散列和生成新块
那我们为什么需要散列呢?我们散列数据的两个主要原因:
- 为了节省空间。散列从块上的所有数据派生。在我们的示例中,我们只有几个数据点,但是假设我们有来自数百、数千或者数百万以前的数据块的数据。将数据散列到单个 SHA256 字符串或散列这些散列表中要比一遍又一遍地复制前面块中的所有数据高效得多。
- 保护区块链的完整性。通过存储前面的散列,就像我们在上面的图中所做的那样,我们能够确保区块链中的块是按正确的顺序排列的。如果恶意的一方试图操纵数据(例如,改变我们的心率来确定人寿保险的价格),散列将迅速改变,链将“断裂”,每个人都会知道也不再信任这个恶意链。
让我们编写一个函数,该函数接受 Block
数据并创建 i 的 SHA256 散列值。
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
复制代码
这个 calculateHash
函数将 Block
的 Index
、Timestamp
、BPM
,我们提供块的 PrevHash
链接为一个参数,并以字符串的形式返回 SHA256 散列。现在我们可以用一个新的 generateBlock
函数来生成一个包含我们所需的所有元素的新块。我们需要提供它前面的块,以便我们可以得到它的散列以及在 BPM 中的脉搏率。不要担心传入 BPM int
参数。我们稍后再讨论这个问题。
func generateBlock(oldBlock Block, BPM int) (Block, error) {
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, nil
}
复制代码
注意当前时间使用 time.Now()
自动写入块中的。还请注意,我们之前的 calculateHash
函数是被调用的。从上一个块的散列复制到 PrevHash
。Index
从上一个块的索引中递增。
块校验
我们需要编写一些函数来确保这些块没有被篡改。我们还通过检查 Index
来实现这一点,以确保它们按预期的速度递增。我们还将检查以确保我们的 PrevHash
与前一个块的 Hash
相同。最后,我们希望通过在当前块上再次运行 calculateHash
函数来重新检查当前块的散列。让我们编写一个 isBlockValid
函数,它执行所有这些操作并返回一个 bool
。如果它通过了我们所有的检查,它就会返回 true
:
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
}
复制代码
如果我们遇到这样一个问题,即区块链生态系统的两个节点都向它们的链添加了区块,并且我们都收到了它们。我们选择哪一个作为真理的来源?我们选择最长的链条。这是一个经典的区块链问题,与邪恶的演员没有任何关系。
两个有意义的节点可能只是具有不同的链长,因此很自然地,较长的节点将是最新的,并且拥有最新的块。因此,让我们确保我们正在接受的新链要比我们现有的链长。如果是,我们可以用具有新块的新链覆盖我们的链。
为了实现这一点,我们简单地比较了链片的长度:
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
复制代码
如果您已经坚持做到这里,就鼓励一下自己!基本上,我们已经用我们需要的各种函数编写了区块链的内部结构。
现在,我们想要一个方便的方式来查看我们的区块链,并写入它,理想情况下是我们可以在一个网络浏览器显示我们的朋友!
Web 服务器
我们假设您已经熟悉 Web 服务器的工作方式,并有一些将它们连接到 Go 中的经验。我们现在就带你走一遍这个流程。
我们将使用您之前下载的 Gorilla/mux 包来为我们完成繁重的任务。
我们在稍后调用的 run
函数中创建服务器。
func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("ADDR")
log.Println("Listening on ", os.Getenv("ADDR"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
复制代码
注意我们选择的端口来自之前创建的 .env
文件。我们使用 log.Println
为自己提供一个实时的控制台消息来让我们的服务器启动并运行。我们对武器进行了一些配置,然后对 ListenAndServe
进行配置。很标准的 Go。
现在我们需要编写 makeMuxRouter
函数,该函数将定义所有的处理程序。要在浏览器中查看并写入我们的区块链,我们只需要两个路由,我们将保持它们的简单性。如果我们发送一个 GET
请求到 localhost
,我们将查看到区块链。如果我们发送一 POST
请求,我们可以进行写入。
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
复制代码
这是我们的 GET
处理器。
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
复制代码
我们只需以 JSON 格式回写完整的的区块链,就可以通过访问 localhost:8080
在任意浏览器中能够查看。我们在 .env
文件中将 ADDR
变量设置为 8080,如果您更改它,请确保访问您的正确端口。
我们的 POST
请求有些复杂(复杂情况并不多)。首先,我们需要一个新的 Message
struct
。稍后我们会解释为什么我们需要它。
type Message struct {
BPM int
}
复制代码
下面是编写新块的处理程序的代码。您看完后我们会带您再看一遍。
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
var m Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
复制代码
使用独立 Message
结构的原因是接收 JSON POST 请求的请求体,我们将使用它来编写新的块。这允许我们简单地发送带有以下主体的 POST 请求,我们的处理程序将为我们填充该块的其余部分:
{"BPM":50}
50
是一个以每分钟为单位的脉搏频率的例子。用一个整数值来改变您的脉搏率。
在将请求体解码成 var m Message
结构后,通过传入前一个块并将新的脉冲率传递到前面编写的 generateBlock
函数中来创建一个新块。这就是函数创建新块所需的全部内容。我们使用之前创建的 isBlockValid
函数,快速检查以确保新块是正常的。
一些笔记
_spew.Dump_
是一个方便的函数,它可以将我们的结构打印到控制台上。这对调试很有用。- 对于测试 POST 请求,我们喜欢使用 Postman。
curl
效果也很好,如果您不能离开终端的话。
当我们的 POST 请求成功或者失败时,我们希望得到相应的通知。我们使用了一个小包装器函数 respondWithJSON
来让我们知道发生了什么。记住,在 Go 中,千万不要忽略它们。要优雅地处理它们。
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}
复制代码
快要完成了!
让我们将所有不同的区块链函数、Web 处理程序和 Web 服务器链接在一个简短、干净的 main
函数中:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}()
log.Fatal(run())
}
复制代码
这是怎么回事?
godotenv.Load()
允许我们从根目录中的.env
文件中读取像端口号这样的变量,这样我们就不必在整个应用程序中对它们进行硬编码(恶心!)。genesisBlock
是main
函数中最重要的部分。我们需要为我们的区块链提供一个初始区块,否则新区块将无法将其先前的散列与任何东西比较,因为先前的散列并不存在。- 我们将初始块隔离到它自己的 Go 例程中,这样我们就可以将关注点从我们的区块链逻辑和 Web 服务器逻辑中分离出来。但它只是以这种没有 Go 例程情况下作更优雅的方式工作。
太好了!我们完成了!
以下是全部代码:
现在来讨论下有趣的事情。让我们试一下 :-)
使用 go run main.go
从终端启动应用程序
在终端中,我们看到 Web 服务器已经启动并运行,我们得到了我们的初始块的打印输出。
现在使用您的端口号来访问 localhost
,对我们来说是 8080。不出所料,我们看到了相同的初始块。
现在,让我们发送一些 POST 请求来添加块。使用 Postman,我们将添加一些具有不同 BPM 的新块。
让我们刷新一下浏览器。瞧,我们现在看到链中的所有新块都带有新块的 PrevHash
与旧块的 Hash
相匹配,正如我们预期的那样!
下一步
恭喜!您只是用适当的散列和块验证来编写自己的块链。现在您应该能够控制自己的区块链之旅,并探索更复杂的主题,如工作证明、利益证明、智能合约、Dapp、侧链等等。
本教程没有讨论的是如何使用工作证明来挖掘新的块。这将是一个单纯的教程,但大量的区块链存在,没有证明工作机制。此外,目前通过在 Web 服务器中写入和查看区块链来模拟广播。本教程中没有 P2P 组件。
如果您想我们添加诸如工作证明和人际关系之类的内容,请务必在我们的 Telegram 中告诉我们,并关注我们的 Twitter!这是和我们沟通的最好的方式。问我们问题,给出反馈,并建议新教程。我们很想听听您的意见。
通过大众需求,我们增加了本教程的后续内容!看看它们!
想了解更多关于珊瑚健康的信息,以及我们如何使用区块链来推进个性化用药/治疗研究,请访问我们的网站。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。