Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
106 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。
( y, O  }, X2 l+ f) ]( _4 V! p% L3 M
% T# `% ^& R9 D/ q1 c7 f! k+ z+ f
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。
7 F9 I  b1 r2 ^3 g$ ~, b& \: Z/ |* X) p3 _( C: C- L/ w) R

* c% \& b* L+ S& K6 V  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
: C) W0 n; K7 D  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。# s  x# f" C5 p3 @5 ~) W3 n7 e

) a* R/ N3 G4 x! p
: u/ [: c& B" a& k  区块链网络
2 C" G8 z8 l% C2 ~  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
: f& n! U3 |- A7 Y. x  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
3 e+ n, I) I9 s; _  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。
2 S6 }* _' y8 }( e2 u# d) C: K0 H, |" M0 A/ C
% h& }+ o0 I6 V( K% M' v6 U
  节点角色; }# c9 z6 G+ K6 e% _* v- {2 f
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:  V* g4 r& p& y) O
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。- D9 K" R0 V, J+ M, z
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
3 y0 i( [/ |& \1 _! W  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
7 X" h1 W  E0 Z) e7 c4 o( t8 q" r
- h. W2 T: h0 ^( E7 u$ v
9 R6 J, l4 W- h$ _# g  网络简化
- n; o8 x# Z0 l  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。  L5 X4 C+ v. c: p+ L# f
$ U- I$ E' s5 p0 D
  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
5 h7 |4 k( L. ]  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。
+ E9 l8 g: b7 r3 b; A  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
+ v$ W1 }1 H$ ?$ s/ ]/ v5 D+ g5 i8 `( a2 ?' \. J

  |- r2 t- h' ~# ?  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。3 J* ?; `8 I  y& g: C6 |/ Z! a
. c! H7 _! y- s/ \/ J- L

) S2 p' X6 Q: d: W, M: ^3 Q  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?, _# n) L" Z, ^
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。5 A/ O0 ]; e3 D: f& w$ k
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:. S  \5 y8 U! c) O0 _+ R
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
" L7 |# j4 L& F/ R: a) y9 i  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
# d1 D3 p* k8 D, s  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
. y+ ], m) ]8 u' }& L  场景! e- k: _+ F' u
  本文的目标是实现如下场景:! {% w) t$ ^- O+ a! A
  中心节点创建一个区块链。
% Z5 y% Q, A4 L1 K9 b+ W+ |. I1 b. s  一个其他(钱包)节点连接到中心节点并下载区块链。8 q5 c" Y: T* L+ B$ w: q
  另一个(矿工)节点连接到中心节点并下载区块链。
( Y) C( ^, w  k2 M) L& U% ?& z3 m  钱包节点创建一笔交易。. q$ l5 m" {. k) c5 T7 u8 g$ c
  矿工节点接收交易,并将交易保存到内存池中。
9 l4 A* ]3 j# @% b  当内存池中有足够的交易时,矿工开始挖一个新块。+ ~. ~- S# K$ R3 l+ j) e' y' r8 N+ T
  当挖出一个新块后,将其发送到中心节点。
1 g  W; f$ t& P1 Z  钱包节点与中心节点进行同步。
* q; z# m( ~& \  钱包节点的用户检查他们的支付是否成功。- I/ ~; N6 H6 `3 F( V$ G8 }
  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。0 n) x7 O! k" y- p6 @
  版本3 q0 c3 A8 Y: H, V6 u$ m: E  o! X
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:7 k$ @+ d2 w+ W' K4 x. f& y
  1.   type version struct {
    / f0 n! A+ N; j, B: q# K
  2.   Version int
    3 i+ A. r) _2 r7 j- V( t. b- u- L
  3.   BestHeight int" q: K; O: c" _" b9 h
  4.   AddrFrom string}
复制代码
$ a( x: K  K+ r
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
# Y8 T" x: \9 w' i& l  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
; Z) S; B+ N! W8 E4 @  为了接收消息,我们需要一个服务器:
7 R* u4 S, Z# J0 U1 P# L  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    ) x: N7 x+ ^* r" R3 V9 z2 p
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)9 x: \* t( H2 N: A1 U: [
  3.   miningAddress = minerAddress
    , ]* e0 v2 [1 B! C
  4.   ln, err := net.Listen(protocol, nodeAddress)
    ; N& e! v$ O1 l4 F, n; [
  5.   defer ln.Close()5 I& O! x/ A; ?, S) }
  6.   bc := NewBlockchain(nodeID)! m/ G" ~$ X$ j
  7.   if nodeAddress != knownNodes[0] {
    6 A$ q6 x( t0 o7 ~. |
  8.   sendVersion(knownNodes[0], bc)+ k* c9 s8 Q% T9 S% |; \
  9.   }0 G9 W0 n  L: v5 b  Q7 F: q
  10.   for {4 k: Q$ Q( Z0 C$ L. y, \8 k
  11.   conn, err := ln.Accept()
    ) c* ?+ H3 Z$ l7 f2 U0 Y$ o; A" p* `
  12.   go handleConnection(conn, bc)
    * a9 x9 F3 z: J  u1 o- ?
  13.   }}
复制代码
2 x- L  Q+ c6 }5 j
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:  U/ P9 j+ g6 t6 o1 [* l" i
 
  1.  if nodeAddress != knownNodes[0] {9 t. Y3 G" }4 g
  2.   sendVersion(knownNodes[0], bc)}
复制代码

6 v* ]- v7 |- a0 v3 h  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。) l6 a7 o( i) r6 P; y
 
  1.  func sendVersion(addr string, bc *Blockchain) {! e4 j8 O' z7 m
  2.   bestHeight := bc.GetBestHeight()
    , _# n% A0 a( g5 [, c* y6 h
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})$ g& l0 h. B/ L' g. U  R4 R
  4.   request := append(commandToBytes("version"), payload...): o$ V5 t; C5 C4 @
  5.   sendData(addr, request)}
复制代码

3 K3 V: ~; d6 s  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:3 r( V" F5 |" v6 u* v. N/ c% ^
 
  1.  func commandToBytes(command string) []byte {
    7 ]/ j5 M& C2 ]4 B. M3 H+ p
  2.   var bytes [commandLength]byte* S8 d" P3 M* n+ ]
  3.   for i, c := range command {
    : l/ v$ _0 X" u+ H! e: `9 e
  4.   bytes = byte(c)
    3 l  L" `- K! Y+ h8 t. h( X
  5.   }
复制代码

" l9 E% s! E" U, C  return bytes[:]}# X  p% K- R2 C3 T, o* o: W0 f
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
1 U9 \4 f: _' m3 d: Q+ ~7 {  
  1. func bytesToCommand(bytes []byte) string {
    / y5 E* `% J& k
  2.   var command []byte
    1 I! T6 @  |, b- S; k
  3.   for _, b := range bytes {. P6 G$ w" h( o6 j/ L. I  h  T
  4.   if b != 0x0 {- |4 N8 N8 b# M( o( X
  5.   command = append(command, b)
    , h$ T, Q( o8 Y
  6.   }
    1 F( L7 ^3 y$ d, W
  7.   }( r# ]' y- |5 z" ]0 K" f  W
  8.   return fmt.Sprintf("%s", command)}
复制代码

" h' z" @/ B: p# ?( F; c, M  R  j  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:1 O* G/ \# _4 p* @7 I- o
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    ' |' c# V# [0 x; S* F) Z& \3 W2 i
  2.   request, err := ioutil.ReadAll(conn)
    ! b% {4 D& y" H# k! c
  3.   command := bytesToCommand(request[:commandLength])
    0 f/ h0 O6 H: E" E$ W
  4.   fmt.Printf("Received %s command\n", command)
    & r' T0 W" X/ y: L
  5.   switch command {
    7 }$ X" i8 |2 K. V- u, U( P' s
  6.   ...
    . U+ D* {1 d! z  H7 q! K# M7 E
  7.   case "version":) z. |' P! |' Z1 C# w+ L2 l- E
  8.   handleVersion(request, bc)
    9 X# U0 ^1 R- l( @' c1 A, M
  9.   default:8 a& Z' k  F( P% |' ]' z* u
  10.   fmt.Println("Unknown command!")
    , b: s) T! H/ a( M9 s
  11.   }- E5 ~4 R5 P0 _2 k
  12.   conn.Close()}
复制代码

# _# P5 [6 f2 a6 V  下面是 version 命令处理器:# w" ^. ]" k/ \8 x
  
  1. func handleVersion(request []byte, bc *Blockchain) {, y5 ?, _) H1 P2 w/ {! R* W- ?
  2.   var buff bytes.Buffer var payload verzion7 I) s( o* I5 e& Z: T- @$ R! r. m% Z
  3.   buff.Write(request[commandLength:])
    # D# r5 g9 Y# y& B
  4.   dec := gob.NewDecoder(&buff): j, W9 W( [2 E  X, ^6 T" g7 s& V
  5.   err := dec.Decode(&payload)
    0 @; b. E: V! M! ^
  6.   myBestHeight := bc.GetBestHeight()
    " A8 V* x' G# L# S; f" g" W, Z
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {
    2 H5 I+ w3 D2 S7 ?$ ?6 P
  8.   sendVersion(payload.AddrFrom, bc): z8 [5 e4 Q; U" w
  9.   }
    & V" m- m! z9 L1 h* j( t% s7 D
  10.   if !nodeIsKnown(payload.AddrFrom) {# d+ L& S& _3 A0 ]
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    ! i. H; `3 r" b6 O: t& B! _
  12.   }}
复制代码

7 y$ b. ^! F2 l! U/ o/ A/ b. p  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
% d6 O- S+ `. A5 a! b4 X8 S9 P" N- M% z* Y" W
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。  _2 C8 n! m; E4 X% p
4 K7 H, Z3 C+ O5 @

. R& ]7 K- k' j8 e, R& z8 d% [
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10