Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
140 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。
( l  x: v2 D5 n/ i! j  A  e  z) E; h! I2 D( l/ i4 {& g$ P! T

- d' @) N2 Y; I: M  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。- z/ `# H3 G+ j% L! b
5 n. I* a4 T0 F9 n6 X

7 K8 }& G1 M1 P( D0 p4 `8 h, L( C  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
$ b$ {" b, R4 a. }2 Q2 U; R( R  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。% X* Z" p  m% ]7 d
& O! y0 D" q+ V7 B! w

3 f" o; ?  T( s, ]; g' ?5 Y4 o# f  区块链网络
3 O- A5 \6 Y# f8 Q# ?' e& p  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。1 E: e! p+ J$ m/ I5 N
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
/ x' S' d8 [) I0 Y( w  _  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。+ h  r, I0 \/ w  L/ }( g. F: g

6 H$ [( F& p1 _( t9 N0 s: n" U
# I, f8 g8 L4 M4 c7 L  节点角色8 S$ ?" n4 ~; p! S( D6 F# V5 ~
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:; k1 u( V4 [* B. W# ?2 e/ c
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
& x1 g' @( x2 C" m! _  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。0 G7 P& x- U: x/ Z/ a9 T
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
4 o% Z5 {" F" N' \: A( b) u+ x! v8 Q& S* H$ A

' d) [: e0 F7 j, _- n0 e  网络简化
0 H5 b3 o+ j! F; o( b  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。; N. W  o, f( Z- t

! \' a! p6 A9 a6 B  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
1 A: u; b2 o. b1 d' n! s# P  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。1 P$ g2 S3 x8 |1 p5 w
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
4 _# M+ z. J8 Z3 v5 j8 K' `' R/ \& s$ Z

! \; G5 B, S( i; h3 |2 s3 w1 Z" B- @  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
+ z3 r0 a# m/ w0 Z9 G" G/ `8 x. n" x# e( [

' N9 F1 f! e3 o3 B3 J) V' V6 q- k  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?- ~  D4 J+ t: Q7 Q+ [
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
" P  k. e+ v! }% N9 V  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:% a  j% {& |# m$ [7 S* [+ a3 F
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
2 H  ^& l1 P* \3 F$ l+ P( |" @  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
' J' U' s5 ~$ J" i( ~( ?6 a0 Z  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
3 C. x" @  z! B  场景+ s3 p3 V: J! `; _
  本文的目标是实现如下场景:* `6 e, n1 _0 E( O
  中心节点创建一个区块链。% c# h- `2 R+ K/ z. z7 ]" q
  一个其他(钱包)节点连接到中心节点并下载区块链。* x. t. ?7 {( N
  另一个(矿工)节点连接到中心节点并下载区块链。
5 M. {% y1 K6 h+ ^: ^4 p  钱包节点创建一笔交易。8 \$ F8 i) P/ t$ H+ ]3 }0 Y7 G
  矿工节点接收交易,并将交易保存到内存池中。1 }. f' J3 n4 }% l) @9 b" c$ f
  当内存池中有足够的交易时,矿工开始挖一个新块。' \/ v3 d+ S- x: d/ u; O
  当挖出一个新块后,将其发送到中心节点。
9 h. h0 ~3 l3 V" e3 c8 I1 H  钱包节点与中心节点进行同步。+ ]9 b- J8 Z1 I& o
  钱包节点的用户检查他们的支付是否成功。
& ~) }0 M! }3 W+ ^7 g6 f+ }  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。' M3 r, b" }+ }
  版本1 `1 Q! c; k: v0 D
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
3 O0 j- K9 c. W  ]$ E
  1.   type version struct {
    6 A. h2 L: ^- k
  2.   Version int% g" Z8 p/ o7 Q* P, t
  3.   BestHeight int
    4 ]9 w- p9 E6 B
  4.   AddrFrom string}
复制代码
8 ~% T) c/ ?7 H: y& U; \3 F/ v
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
; F  B! s1 k/ O7 c: q% Y  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。8 b+ N1 ?  p) Q* e# O; ]( N* \
  为了接收消息,我们需要一个服务器:
, m/ A1 I, o" ~; S) b  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    8 r; H! I, I+ Y* P. R0 m9 k! B; \3 O
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)% q- l" h& Z- ?9 j  W3 r
  3.   miningAddress = minerAddress! p6 m. U: `5 T- L+ z# M
  4.   ln, err := net.Listen(protocol, nodeAddress)5 `  N) d  Y' [! |- N( C
  5.   defer ln.Close()
    2 n2 S4 W' i  O! P9 o
  6.   bc := NewBlockchain(nodeID)
    ( y8 Y/ Q; v2 A5 [) |* i
  7.   if nodeAddress != knownNodes[0] {
    : z. L" C9 P6 L
  8.   sendVersion(knownNodes[0], bc)3 T9 N5 V9 e5 |/ F. ?: `9 ]
  9.   }% z) N: n! b7 i2 {
  10.   for {
    5 q& o$ T: `2 w' z* {, a' V6 J6 Y
  11.   conn, err := ln.Accept()
    " U) ^% g: z* d8 Z0 n
  12.   go handleConnection(conn, bc)
    2 c3 d1 G. A! {
  13.   }}
复制代码
. Q9 H6 ^+ M6 \! }0 D4 k
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:9 f; Z4 ]8 ^4 s/ @5 P% {3 k: H" H
 
  1.  if nodeAddress != knownNodes[0] {
    # e3 i: S( ~5 j( ]4 `2 D
  2.   sendVersion(knownNodes[0], bc)}
复制代码

+ u8 j3 }' y* ~% x8 E1 g" a1 n  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。* d1 [; Z( n& \
 
  1.  func sendVersion(addr string, bc *Blockchain) {) w& ?# k: V- ?( i% ^0 C
  2.   bestHeight := bc.GetBestHeight()9 n' K  }  u$ v7 d* V
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})" j- O+ P9 A& H- K7 o
  4.   request := append(commandToBytes("version"), payload...)
    ! T, y8 W9 m# f
  5.   sendData(addr, request)}
复制代码

/ E- y2 ^/ w2 ?5 O  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
: U6 }0 r6 o; O& w, h! s: i4 N3 Y 
  1.  func commandToBytes(command string) []byte {8 d) _6 }# w- g
  2.   var bytes [commandLength]byte( V! Q) e! p2 |2 w
  3.   for i, c := range command {
    : C7 l2 w4 w# W0 }
  4.   bytes = byte(c)7 R' i# `% F' G2 C# g' h7 K1 S8 A" L
  5.   }
复制代码
3 @2 d6 b, Y+ h
  return bytes[:]}9 i8 h6 }" n7 d8 m
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:3 @& S5 k( e2 z0 v( `* L5 n- M" h" h9 G
  
  1. func bytesToCommand(bytes []byte) string {
    7 w* v% }7 @+ b3 J
  2.   var command []byte7 y- V4 n4 u5 k" e
  3.   for _, b := range bytes {5 T: N' a; [' F! I
  4.   if b != 0x0 {& E% q/ I! s5 K2 V( A
  5.   command = append(command, b)) j3 Y: t9 c4 E. C
  6.   }2 N" H/ P; r' k& P) D
  7.   }/ p/ G7 g* D1 @' t) I: E
  8.   return fmt.Sprintf("%s", command)}
复制代码

- A! S1 j- S/ z7 Y  w& g  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:* \/ L( Z7 x$ q3 Q* `' r. D
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {) F# a& I" n: d
  2.   request, err := ioutil.ReadAll(conn)
    " ^/ h# Y$ j7 h3 B0 u  }+ K
  3.   command := bytesToCommand(request[:commandLength])
    ; q) Q/ L4 P7 r8 k+ F- m$ ?
  4.   fmt.Printf("Received %s command\n", command)
    1 Y) P& c: @+ {0 ?/ g' U
  5.   switch command {
    ; i$ Q0 {; L4 c. P) x
  6.   ...
    7 Z' u$ N6 T# r& n
  7.   case "version":6 D# J7 K0 ]6 H$ Q7 M. h
  8.   handleVersion(request, bc)
    % I0 S, P6 y  c: K/ y+ ~
  9.   default:
    ; j8 _2 d  k' W: ?/ C/ [
  10.   fmt.Println("Unknown command!")- y/ c8 T# m8 q. D' V
  11.   }3 x- s6 J" O6 z) c: }  s7 S6 ~
  12.   conn.Close()}
复制代码
6 D7 a( \+ |! m0 w5 I, ]+ i2 g- W8 k
  下面是 version 命令处理器:
3 u; d  o4 X5 z1 D+ L  
  1. func handleVersion(request []byte, bc *Blockchain) {% Q# k% {% o& O% `7 o5 t* o' u
  2.   var buff bytes.Buffer var payload verzion
    + K; Z3 o1 `9 U1 l/ Z7 W  G
  3.   buff.Write(request[commandLength:]). [3 p+ j1 F6 i5 S- L
  4.   dec := gob.NewDecoder(&buff)4 b9 D& M% q1 |2 b% k& q+ W3 R
  5.   err := dec.Decode(&payload)$ T5 N+ [, P. E; x  n$ ]
  6.   myBestHeight := bc.GetBestHeight()
    . ?: }" u: r$ e6 ?! J
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {/ J4 S  M7 V0 ^
  8.   sendVersion(payload.AddrFrom, bc)
    ( q, `! D: Q) d; A- g
  9.   }
    $ O$ W" z- ]5 F# T; t! q" g( o
  10.   if !nodeIsKnown(payload.AddrFrom) {
    9 i0 \$ I, f# A8 G
  11.   knownNodes = append(knownNodes, payload.AddrFrom)" u3 A/ \1 S0 I* R5 x7 v! t. S
  12.   }}
复制代码
  ~* ^2 B7 l* _
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。8 @  O7 L* Z4 y

; ^* A% W, Z: c  G7 R  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。, J6 x2 k4 S6 J2 i4 \3 u1 ]
! Q$ w+ ~3 C5 T$ h. w, {6 c

+ ~3 p; f* k4 i: n% s8 S. Z
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10