我的区块链技术学习笔记(二十):比特币场景化实践(1) [复制链接]

1077
 
8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈attachments-2018-01-kwCCxGek5a7020259fe47. 8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈作者: Ivan Kuznetsov  吴寿鹤等
8btm.com-新币圈 8btm.com-新币圈著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
8btm.com-新币圈 8btm.com-新币圈

8btm.com-新币圈 8btm.com-新币圈本文的目标是实现如下场景:
8btm.com-新币圈 8btm.com-新币圈

    8btm.com-新币圈 8btm.com-新币圈
  • 中心节点创建一个区块链。
    8btm.com-新币圈 8btm.com-新币圈
  • 一个其他(钱包)节点连接到中心节点并下载区块链。
    8btm.com-新币圈 8btm.com-新币圈
  • 另一个(矿工)节点连接到中心节点并下载区块链。
    8btm.com-新币圈 8btm.com-新币圈
  • 钱包节点创建一笔交易。
    8btm.com-新币圈 8btm.com-新币圈
  • 矿工节点接收交易,并将交易保存到内存池中。
    8btm.com-新币圈 8btm.com-新币圈
  • 当内存池中有足够的交易时,矿工开始挖一个新块。
    8btm.com-新币圈 8btm.com-新币圈
  • 当挖出一个新块后,将其发送到中心节点。
    8btm.com-新币圈 8btm.com-新币圈
  • 钱包节点与中心节点进行同步。
    8btm.com-新币圈 8btm.com-新币圈
  • 钱包节点的用户检查他们的支付是否成功。
    8btm.com-新币圈 8btm.com-新币圈
这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真是,也是比特币最常见最重要的用户场景。
8btm.com-新币圈 8btm.com-新币圈版本
8btm.com-新币圈 8btm.com-新币圈节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
8btm.com-新币圈 8btm.com-新币圈type version struct { Version int BestHeight int AddrFrom string}由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
8btm.com-新币圈 8btm.com-新币圈接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是处于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
8btm.com-新币圈 8btm.com-新币圈为了接收消息,我们需要一个服务器:
8btm.com-新币圈 8btm.com-新币圈var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) { nodeAddress = fmt.Sprintf("localhost:%s", nodeID) miningAddress = minerAddress ln, err := net.Listen(protocol, nodeAddress) defer ln.Close() bc := NewBlockchain(nodeID) if nodeAddress != knownNodes[0] { sendVersion(knownNodes[0], bc) } for { conn, err := ln.Accept() go handleConnection(conn, bc) }}首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
8btm.com-新币圈 8btm.com-新币圈if nodeAddress != knownNodes[0] { sendVersion(knownNodes[0], bc)}这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
8btm.com-新币圈 8btm.com-新币圈func sendVersion(addr string, bc *Blockchain) { bestHeight := bc.GetBestHeight() payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress}) request := append(commandToBytes("version"), payload...) sendData(addr, request)}我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
8btm.com-新币圈 8btm.com-新币圈func commandToBytes(command string) []byte { var bytes [commandLength]byte for i, c := range command { bytes[i] = byte(c) } return bytes[:]}它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
8btm.com-新币圈 8btm.com-新币圈func bytesToCommand(bytes []byte) string { var command []byte for _, b := range bytes { if b != 0x0 { command = append(command, b) } } return fmt.Sprintf("%s", command)}当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
8btm.com-新币圈 8btm.com-新币圈func handleConnection(conn net.Conn, bc *Blockchain) { request, err := ioutil.ReadAll(conn) command := bytesToCommand(request[:commandLength]) fmt.Printf("Received %s commandn", command) switch command { ... case "version": handleVersion(request, bc) default: fmt.Println("Unknown command!") } conn.Close()}下面是 version 命令处理器:
8btm.com-新币圈 8btm.com-新币圈func handleVersion(request []byte, bc *Blockchain) { var buff bytes.Buffer var payload verzion buff.Write(request[commandLength:]) dec := gob.NewDecoder(&buff) err := dec.Decode(&payload) myBestHeight := bc.GetBestHeight() foreignerBestHeight := payload.BestHeight if myBestHeight </span foreignerBestHeight span style="font-size:inherit;color:rgb(94,102,135);"{/span span style="font-size:inherit;"sendGetBlocks/spanspan style="font-size:inherit;color:rgb(94,102,135);"(/spanpayloadspan style="font-size:inherit;color:rgb(94,102,135);"./spanAddrFromspan style="font-size:inherit;color:rgb(94,102,135);")/span span style="font-size:inherit;color:rgb(94,102,135);"}/span span style="font-size:inherit;color:rgb(172,151,57);"else/span span style="font-size:inherit;color:rgb(172,151,57);"if/span myBestHeight span style="font-size:inherit;color:rgb(199,107,41);"> foreignerBestHeight { sendVersion(payload.AddrFrom, bc) } if !nodeIsKnown(payload.AddrFrom) { knownNodes = append(knownNodes, payload.AddrFrom) }}首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
8btm.com-新币圈 8btm.com-新币圈然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
8btm.com-新币圈 8btm.com-新币圈getblocks
8btm.com-新币圈 8btm.com-新币圈type getblocks struct { AddrFrom string}getblocks 意为 “给我看一下你有什么区块”(在比特币中,这会更加复杂)。注意,它并没有说“把你全部的区块给我”,而是请求了一个块哈希的列表。这是为了减轻网络负载,因为区块可以从不同的节点下载,并且我们不想从一个单一节点下载数十 GB 的数据。
8btm.com-新币圈 8btm.com-新币圈处理命令十分简单:
8btm.com-新币圈 8btm.com-新币圈func handleGetBlocks(request []byte, bc *Blockchain) { ... blocks := bc.GetBlockHashes() sendInv(payload.AddrFrom, "block", blocks)}在我们简化版的实现中,它会返回 所有块哈希
8btm.com-新币圈 8btm.com-新币圈inv
8btm.com-新币圈 8btm.com-新币圈type inv struct { AddrFrom string Type string Items [][]byte}比特币使用 inv 来向其他节点展示当前节点有什么块和交易。再次提醒,它没有包含完整的区块链和交易,仅仅是哈希而已。Type 字段表明了这是块还是交易。
8btm.com-新币圈 8btm.com-新币圈处理 inv 稍显复杂:
8btm.com-新币圈 8btm.com-新币圈func handleInv(request []byte, bc *Blockchain) { ... fmt.Printf("Recevied inventory with %d %sn", len(payload.Items), payload.Type) if payload.Type == "block" { blocksInTransit = payload.Items blockHash := payload.Items[0] sendGetData(payload.AddrFrom, "block", blockHash) newInTransit := [][]byte{} for _, b := range blocksInTransit { if bytes.Compare(b, blockHash) != 0 { newInTransit = append(newInTransit, b) } } blocksInTransit = newInTransit } if payload.Type == "tx" { txID := payload.Items[0] if mempool[hex.EncodeToString(txID)].ID == nil { sendGetData(payload.AddrFrom, "tx", txID) } }}如果收到块哈希,我们想要将它们保存在 blocksInTransit 变量来跟踪已下载的块。这能够让我们从不同的节点下载块。在将块置于传送状态时,我们给 inv 消息的发送者发送 getdata 命令并更新 blocksInTransit。在一个真实的 P2P 网络中,我们会想要从不同节点来传送块。
8btm.com-新币圈 8btm.com-新币圈在我们的实现中,我们永远也不会发送有多重哈希的 inv。这就是为什么当 payload.Type == "tx"时,只会拿到第一个哈希。然后我们检查是否在内存池中已经有了这个哈希,如果没有,发送 getdata 消息。
8btm.com-新币圈 8btm.com-新币圈getdata
8btm.com-新币圈 8btm.com-新币圈type getdata struct { AddrFrom string Type string ID []byte}getdata 用于某个块或交易的请求,它可以仅包含一个块或交易的 ID。
8btm.com-新币圈 8btm.com-新币圈func handleGetData(request []byte, bc *Blockchain) { ... if payload.Type == "block" { block, err := bc.GetBlock([]byte(payload.ID)) sendBlock(payload.AddrFrom, &block) } if payload.Type == "tx" { txID := hex.EncodeToString(payload.ID) tx := mempool[txID] sendTx(payload.AddrFrom, &tx) }}这个处理器比较地直观:如果它们请求一个块,则返回块;如果它们请求一笔交易,则返回交易。注意,我们并不检查实际上是否已经有了这个块或交易。这是一个缺陷 :)
8btm.com-新币圈 8btm.com-新币圈block 和 tx
8btm.com-新币圈 8btm.com-新币圈type block struct { AddrFrom string Block []byte}type tx struct { AddFrom string Transaction []byte}实际完成数据转移的正是这些消息。
8btm.com-新币圈 8btm.com-新币圈处理 block 消息十分简单:
8btm.com-新币圈 8btm.com-新币圈func handleBlock(request []byte, bc *Blockchain) { ... blockData := payload.Block block := DeserializeBlock(blockData) fmt.Println("Recevied a new block!") bc.AddBlock(block) fmt.Printf("Added block %xn", block.Hash) if len(blocksInTransit) > 0 { blockHash := blocksInTransit[0] sendGetData(payload.AddrFrom, "block", blockHash) blocksInTransit = blocksInTransit[1:] } else { UTXOSet := UTXOSet{bc} UTXOSet.Reindex() }}当接收到一个新块时,我们把它放到区块链里面。如果还有更多的区块需要下载,我们继续从上一个下载的块的那个节点继续请求。当最后把所有块都下载完后,对 UTXO 集进行重新索引。
8btm.com-新币圈 8btm.com-新币圈TODO:并非无条件信任,我们应该在将每个块加入到区块链之前对它们进行验证。
8btm.com-新币圈 8btm.com-新币圈TODO: 并非运行 UTXOSet.Reindex(), 而是应该使用 UTXOSet.Update(block),因为如果区块链很大,它将需要很多时间来对整个 UTXO 集重新索引。
8btm.com-新币圈 8btm.com-新币圈处理 tx 消息是最困难的部分:
8btm.com-新币圈 8btm.com-新币圈func handleTx(request []byte, bc *Blockchain) { ... txData := payload.Transaction tx := DeserializeTransaction(txData) mempool[hex.EncodeToString(tx.ID)] = tx if nodeAddress == knownNodes[0] { for _, node := range knownNodes { if node != nodeAddress && node != payload.AddFrom { sendInv(node, "tx", [][]byte{tx.ID}) } } } else { if len(mempool) >= 2 && len(miningAddress) > 0 { MineTransactions: var txs []*Transaction for id := range mempool { tx := mempool[id] if bc.VerifyTransaction(&tx) { txs = append(txs, &tx) } } if len(txs) == 0 { fmt.Println("All transactions are invalid! Waiting for new ones...") return } cbTx := NewCoinbaseTX(miningAddress, "") txs = append(txs, cbTx) newBlock := bc.MineBlock(txs) UTXOSet := UTXOSet{bc} UTXOSet.Reindex() fmt.Println("New block is mined!") for _, tx := range txs { txID := hex.EncodeToString(tx.ID) delete(mempool, txID) } for _, node := range knownNodes { if node != nodeAddress { sendInv(node, "block", [][]byte{newBlock.Hash}) } } if len(mempool) > 0 { goto MineTransactions } } }}未完待续……
8btm.com-新币圈 8btm.com-新币圈上一篇:我的区块链技术学习笔记(十九):认识区块链网络
8btm.com-新币圈 8btm.com-新币圈下一篇:我的区块链技术学习笔记(二十):比特币场景化实践(2)
8btm.com-新币圈 8btm.com-新币圈
8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈attachments-2018-01-KR3OuPgA5a62135b82784.jpg 8btm.com-新币圈 8btm.com-新币圈 8btm.com-新币圈

本版积分规则

发表主题 回复
mailtopia,把去中心化做到极致!

(c) 2015-2018 8BTM Inc. M链、₥币 All Rights Reserved 区块链金融合作:福建反身投资管理有限公司

网站备案证书号: 闽ICP备18010811号  Ƀ猫商城 IoT&BlockChain:微物联(福州)网络科技有限公司 SiteMap