深入理解比特币(四) 区块和网络

本文是 深入理解比特币系列 中的第四篇 区块和网络. 本篇文章主要包含以下内容:

  • 网络中的节点
  • 交易的广播与确认
  • 区块链和挖矿
  • 简易支付验证(SPV)

网络中的节点

比特币网络是一个由多个比特币节点共同组成的 P2P 网络. 在 P2P 网络中不存在中心化的服务端, 每个节点使用服务的同时也对网络中的其他节点提供服务. 比特币节点由以下几个功能模块组成: 路由, 区块链数据库, 挖矿, 钱包服务. 一个比特币节点根据目的不同, 可能包含这些功能模块中的一部分或者全部.

例如, 一些节点保存了完整的区块链数据, 这样的节点就可以被称为 全节点. 全节点能够独立地校验任何交易的有效性, 而不需外部服务的帮助.

而另一些节点只保存了区块链的一部分数据(区块头), 它们通过一种名为 简易支付验证(SPV)的方式来完成交易验证, 这样的节点被称为 SPV节点轻节点.

roles

当有新节点加入比特币网络后, 它必须和网络中的其他节点建立连接, 并根据需要从连接到的节点同步区块数据. 目前, 运行一个比特币全节点需要保存的区块链数据已经超过了 260G.

正是因为运行全节点所要保存的数据量太大了, 一般人很难在个人设备上维护一个全节点, 所以就有了轻节点(SPV节点), 关于SPV节点的更多细节我们在本文后面的内容里会了解到.

除此之外还有矿工节点, 正是因为有矿工们的辛勤工作比特币世界中发生的交易数据才得以被打包成区块并被永久的记录在区块链中.

交易的广播与确认

当我们从比特币钱包发起一笔提币交易时, 这笔交易数据将被广播给钱包连接的所有其他节点并在网络中进行转发最终被矿工节点收到. 比特币网络中的矿工节点会维护一个内存池用来存放那些尚未打包到区块中的交易, 内存池有时也被称作交易池. 内存池中的交易虽然尚未被打包到区块链中, 但钱包软件仍可以通过内存池获取到和自己相关的交易数据. 在一些小额支付场景中, 尚未确认的交易也可能作为交易发生的一种凭证.

每个矿工节点可以根据自己的意愿, 从内存池中选择交易打包成数据块(一般是根据手续费高低选择). 如果矿工节点能够抢先完成工作量证明, 就可以将自己打包的区块广播到网络中并获得系统的奖励. 而其他节点则需要将已广播的交易从自己的内存池中剔除, 重新选择交易打包数据块, 并开始新一轮的算力竞赛.

区块链和挖矿

所谓区块链, 是指由矿工节点打包并被网络确认的数据块所构成的链. 每个数据块的区块头(Header)中都保存了前一个区块Header的哈希值, 这就好像是我们熟悉的链表结构, 每个节点保存着指向它前一个节点的指针. 正是因为区块链的这种设计, 当一个节点首先完成工作量证明使自己的区块被网络确认后, 其他的矿工节点必须放弃之前的工作, 重新构造一个包含最新区块头哈希值的数据块.

比特币的一个区块由如下数据结构:

  • BlockSize (4 bytes): 区块大小
  • BlockHeader (80 bytes): 区块头
  • Transaction Count (1-9 bytes): 交易数量
  • Transacions: 交易

每个区块中的第一笔交易被称作 Coinbase 交易, Coinbase 交易只有一个 input 且为一个特殊的固定值以便和普通交易相区别, 而 Coinbase 交易 output 的总金额应该等于系统奖励与区块中其他交易手续费之和. 因为 Coinbase 交易的 input 并非来自于某个 UTXO 所以自然也就不需要相应的解锁脚本(ScriptSig). 取而代之的是 Coinbase Data, 矿工可以在往该字段中填充任何数据. 以创世块为例, 中本聪在 Coinbase Data 中填入了这样的数据 "The Times 03/Jan/ 2009 Chancellor on brink of second bailout for banks" (泰晤士报 2009年1月3日 财政大臣再次站在了拯救银行业的边缘), 表示对日期的证明, 同时也表达了对中心化信用货币体系的失望.

可以说 Coinbase 交易是系统中一切比特币的来源, 比特币系统通过这种方式同时实现了对矿工的激励和货币的发行.

矿工通过完成工作量证明获得系统奖励的行为又被称作 挖矿. 在理解挖矿之前, 我们首先需要对区块头的数据结构有一定的了解. 比特币区块 Header 由如下结构组成:

  • Version (4 bytes): 版本号
  • Previous Block Hash (32 bytes): 前一个区块的哈希值
  • Merkle Root (32 bytes): 默克尔树根哈希值
  • Timestamp (4 bytes): 区块构造的时间
  • Target (4 bytes): 工作量证明算法中的目标难度
  • Nonce (4 bytes): 工作量证明算法中的随机数

工作量证明算法(Prove-of-Work)是指通过不断改变 Nonce 值, 使区块头的哈希值小于系统要求的目标难度. 观察下面这个区块头信息我们可以发现, 如果将区块头哈希(十六进制)用二进制表示, 这个二进制数的前 75 位都是 0, 这意味着找到一个满足条件的数的概率大约是 0.5 ^ 75 !

在比特币上线的初期完成一次工作量证明的难度并没有现在这么高, 为了应对比特币网络中可能发生的算力变化, 比特币系统每过 2016 个区块会对目标难度进行一次调整, 目标是将比特币网络中产生一个新块的时间控制在 10 分钟左右. 如此设计的目的一方面是控制比特币发行的速度, 更重要的是避免因出块过于频繁使比特币网络中的节点难以达成共识.

简易支付验证(SPV)

正如本文一开始提到的那样, 全节点需要维护的数据量对个人设备而言是难以普及的, 而 SPV 节点正是为解决这一问题而设计的.因为 SPV 节点只需下载区块头而不用下载包含在每个区块中的交易信息, 由此产生的不含交易信息的区块链大小只有完整区块链的 1/1000. 也正因为如此, 使用 SPV 节点验证交易时必须依赖全节点的支持.

从上一小节的内容我们知道, 区块数据中包含了所有的交易数据, 而在区块头并没有包含交易的具体信息, 只有一个叫做 默克尔树根(Merkle Root) 的哈希值.

那么默克尔树到底是什么呢? 原来, 区块中的交易数据会两两组合, 分别算出一个哈希值, 所有的哈希值又会再次两两组合得到新的哈希值. 这些哈希和交易一起构成了一个二叉树的结构, 这就是默克尔树(如下图). 默克尔的叶子节点是区块中的交易数据, 而根节点的哈希值就叫做默克尔树根, 默克尔树根可以视作是区块中所有交易的一个信息摘要, 一旦区块中的任何一笔交易发生改变, 算出的默克尔树根的值也会不同. 默克尔树根的值会被记录在区块头中.

简单来说, 当一个 SPV 钱包需要验证某笔交易是否已经被打包到区块里时, 需要将要验证的节点发送给附近的全节点. 全节点收到来自 SPV 的验证请求后, 会扫描自己的交易数据库看该交易是否存在. 如果交易存在, 全节点会将通过该交易计算 Merkle Root 所要用到的其他哈希值返回给 SPV 节点. SPV 节点正是通过比较计算出的 Mercle Root 值与本地区块头中记录的值是否一致, 来判断一笔交易是否存在于某个区块中.

举个例子, 在上图所示的默克尔树中, 绿色的叶子节点 (HK 和 HN) 是 SPV 节点想要验证的交易. 在 SPV 发送验证请求后, 全节点会将图中蓝色的节点返回给 SPV 节点, 并通过某种方式告知计算的顺序. 根据全节点提供的信息, SPV 节点可以计算出 Merkle Root 的值, 又因为哈希函数的单向性, 在计算结果确定的前提下, 想要伪造源数据几乎是不可能的. 这就保证了 SPV 验证的可行性.

最后还要补充一点, SPV 在向全节点发起验证请求时, 实际上并不是直接将想验证的交易地址发送给全节点, 出于隐私保护方面的考虑. SPV 实际上会向全节点发送一个包含地址特征的布隆过滤器, 关于布隆过滤器这里就不在展开了, 更多细节可以参考维基百科上的介绍.

使用布隆过滤器保护SPV节点隐私性的方案在 BIP-37 里提出, 不过这个方案其实也并非完美, 首先采用布隆过滤器进行隐私保护的方案比较有限, 而且使用布隆过滤器对历史区块进行扫描对全节点来说是个重活, 攻击者可以利用这一特性向全节点发送验证请求, 从而引发全节点拒绝服务(DoS).

作为改进方案 BIP-157BIP-158 采用了相反的做法, 由全节点维护一个布隆过滤器提供给 SPV 钱包下载. 这样只是稍微加重了轻钱包的工作负担, 而全节点的工作负担可以大大减轻.

Show Comments