×

签到

分享到微信

打开微信,使用扫一扫进入页面后,点击右上角菜单,

点击“发送给朋友”或“分享到朋友圈”完成分享

【CCCF】区块链到底有什么了不起 tangnian2020-04-09 11:04:05 回复 1 查看 资源共享 学术动态
【CCCF】区块链到底有什么了不起
分享到:

作者:王嘉平 

转载至CCCF


摘要:我们早就有了微信支付、支付宝这样的在线支付应用,也有银联这样的清算结算网络,为什么还需要基于区块链技术的比特币网络呢?在技术层面,因为有了云计算,在线支付系统、在线网银系统都已经非常普及,那么区块链系统有什么价值和意义,或者仅仅就是一个新的“轮子”?本文将从这个角度厘清区块链到底有什么了不起的地方,剖析区块链的技术本质。


关键词:区块链分布式系统


2009年1月3日,比特币网络开始启动,第一个区块携带50个比特币的挖矿激励被发出,继而全网同步。这是一个会被镌刻在人类科技发展史上的重要日子,就好像冯·诺伊曼架构第一次从内存中拾取指令,并执行它;也好像互联网的第一个IP报文被发出,然后被路由转发,到达目标主机。比特币支付网络是一个具体的应用,而其核心底层是区块链技术。但是我们早就有了高效便捷的在线支付系统,也有银联这样的清结算网络,为什么还需要基于区块链技术呢,会不会仅仅是一个新的“轮子”?本文将从技术角度厘清区块链到底有什么了不起的地方,剖析区块链的技术本质。

在区块链技术出现以前,大部分计算机技术都专注于如何完成计算任务,如何提高效率、降低成本、扩大规模。如果坚持从这个角度去看区块链技术,你会发现区块链效率低下,成本高昂,似乎找不到什么亮点 。还有一部分计算机技术专注于计算任务的数据安全性和访问控制。但是如果我们从安全角度去看区块链技术,你会发现区块链技术本身也只是利用了非对称加密体系实现访问控制。除此之外,区块链技术并没有发明什么独特的密码学技术。更有甚者,有人把区块链技术看成是另一种特殊的数据库,数据不可篡改。这些看法都是正确的,然而仅从这些角度我们是无法洞悉区块链技术的核心价值的。


监管和治理的技术化


区块链技术核心关注的是一个计算系统的自动化监管和治理。这个关注点并不是计算机系统一开始就有的。单机软件时代,整个计算系统都在用户掌控之中,此关注点则毫无意义。在互联网初期的通讯和内容分发时代,虽然计算过程已经有一部分交给了他人掌管的服务器,但是我们对计算结果和行为有确信的判断能力,或者计算结果即使背离了我们的预期,通常也无关痛痒,所以这个关注点在当时也不是什么大事。而我们现在处于社交、搜索和推荐服务时代,计算系统深刻影响着我们对世界的认知、行为的决策,甚至生存的保障。此时,这个关注点意义重大。当下两大局面决定了对一个大规模面向公众服务的计算系统的监管和治理是一个关乎国计民生的大事。

1. 用户对计算服务系统给出的结果无法有效判断其正确性和合理性,无力甄别获取到的信息是否经过了别有用心的筛选和排序。

2. 计算服务系统可以直接触及和覆盖比例很高的社会人群,并且承载了人们的社交关系、经济资产、物理身份、信誉征信等关键社会性元素。

下面我们用一个小案例来看看区块链技术实现了怎样的前所未有的能力。以太坊是另一个全球性的公开区块链网络,并支持智能合约使其在区块链网络中执行逻辑可编程,部署各种第三方应用。2018年7月,以太坊网络上出现了一个游戏应用——Fomo3D,玩家众多,一时间严重阻塞了以太坊网络。这是一个典型的奖池游戏,玩家陆续以递增的价格向主办方买钥匙,所有购买资金计入一个公共的奖金池。如果一段时间内始终无人买钥匙,则游戏停止,最后一个钥匙买家赢得沉淀资金的一半,另一半资金则向之前的玩家均价回购所有的钥匙。首轮结束时,游戏玩家瓜分了价值180万美元的以太币。最有意思的不是这个游戏本身,而是游戏执行的方式。游戏主办方完全匿名,没有任何背景可查。游戏是在一个完全没有信任背书和监管的情况下开始并执行的。一切都如期发生,之前约定的规则都被严格执行了。

且不论游戏本身是否合法合规,这样的活动要公正地开展通常需要严格的监管和治理才能保证。奖池资金需要被托管,主办方需要被监管。以现今的互联网云计算架构来实施这个项目,如果没有监管措施,最后的结果要么是游戏停止,主办方把服务器一关,卷款跑路;要么是暗中篡改上述规则,中饱私囊。现在这种以服务器/客户端为基础的互联网服务的黑箱计算模式,是阻碍互联网服务可信公正的根本症结。

现在初级阶段的区块链技术已经在特定场景中实现监管和治理的技术化,使大家公认的规则能够可信地执行,没有人可以篡改,甚至最初制定规则、实际运营业务的人也无法在大家不认同的情况下篡改规则。前提是这些规则及其涉及到的状态,能够精准完备地被形式化、数字化,能够完全落实到计算机中的数据结构和程序代码。这是计算机技术的核心突破,而其中涉及的非对称加密技术、安全哈希、P2P网络通讯等都是实现区块链系统的技术手段,而非其技术本质。

科技进步,本质上是要提升生产力,提高效率,降低成本。而区块链技术的出现也一样,只不过效率提升的并不是计算本身,而是关乎计算背后映射的实际意义。区块链技术使得我们可以以更高的效率、更低的成本对大规模计算服务系统进行全自动的监管和治理,从而保障其背后映射的实际意义和价值不被恶意操纵和利用,同时也使制度化的监管和治理实施得更广,穿透得更深,定位得更精准。


开放的分布式冯·诺伊曼架构


冯·诺伊曼架构是现代通用计算机的基础架构,根据可编程的逻辑处理输入,产生输出(见图1(a))。在现代面向服务的计算机系统中,这个架构中最具实际意义的是执行逻辑和存储中的状态。如果我们用计算机体系结构的视角去看区块链,可以看到非常类似的结构。在区块链(见图1(b))中,“输入”是未定序、未确认的交易,“输出”是有序的经过确认的交易,“内存”是账簿的状态,中央处理器执行的是硬编码(hardcoded)在区块链节点软件中的交易逻辑,或者是第三方部署的智能合约。与冯·诺伊曼架构相比,区块链逻辑架构除了更换了实体的名称之外,似乎并无特别之处。



两者的核心区别在于,冯·诺伊曼架构假设的是一台单机,而区块链架构刻画的是一个由成百上千台计算机所构成的开放网络。冯·诺伊曼架构最初是在一个完全中心化的设定下给出的,现在的计算机单机、大型机,乃至集群和云服务,本质上都是这样的模型。它们关心的是高效地完成计算,正确地更新状态。整个系统的输入、执行逻辑和初始状态都被单一方控制。所以这个单一方可以任意操控系统中的执行逻辑,甚至可以在外界不知情的情况下肆意修改。这使得任何需要公信力的服务系统,我们都需要在其上施加各种额外的监管措施。


计算过程的随机接力


一个开放的分布式计算系统如何被有效地监管和治理,并且自证清白,正是区块链技术所攻克的难题。区块链的核心是将计算执行过程同特定的物理计算设备分离,从根本上避免了计算过程被单一的控制方掌控,让所有人或者一定范围的人都可以监管此计算系统。

在区块链系统中,所有节点全量同步所有的输入数据(即未确认交易),全量同步历史上已经完成的计算步骤(即之前给出的所有的块及其包含的交易),从而使得每个节点都能得到最新的计算上下文,然后其中一个随机的节点执行下一步计算,出一个块,将整个网络的计算过程推进一步,继而开始下一步计算。值得注意的是,每次出块的节点大都是不同的节点给出的,并且不可预测,从而实现了计算过程在各个节点中的接力(见图2)。无论是工作量证明(Proof-of-Work, PoW),还是拜占庭容错(Byzantine Fault Tolerance, BFT)等共识机制,上述计算过程的实际步骤都是在全球不同的物理基础(节点)上完成。而不同共识算法的本质就是给出一个依次选择这些物理节点的不同方案,使得这个计算系统能够在大家的监督下可信地完成计算过程。其执行的逻辑不可篡改,才是区块链系统的本质,可不篡改数据库只是其上的一种应用而已。

保障一致性的共识机制


为了确保接力计算过程可以有效地分散到各个不同的节点(控制方),区块链系统采用了非常重要的技术——共识机制(consensus),其中包含两个内涵:

1. 冲突消解 当不同的控制方出现分歧的时候,如何收敛到一个大家公认的结果?具体在区块链系统中,就是当不同的节点可能在同一个高度(即相同的前驱区块)产生不一样的区块时,我们如何使得全网可以自动仲裁这个分歧,一致地认定其中一个块,而抛弃另一个?

2. 权重定义 如何定义控制方,保证分散的程度?全网的自动仲裁本质上是少数控制方服从多数控制方的过程。但是我们根据什么来定义这个少数和多数(即权重)?不同的共识算法给出了不同的答案,但是大家都试图趋同一个标准,尽量将此权重锚定在某种稀缺的不易集中的资源上,并且此资源在网络远端是能够被观察和测量的。


冲突消解


拜占庭共识类算法是冲突消解的一类算法。它们基于拜占庭容错算法进行逐轮投票,每轮投票会产生一个全网一致的区块,也就是说在前一个区块的冲突没有消解之前不允许产生下一个区块。当一个区块被全网确认之后,再也不可能被取消。BFT算法最初源于分布式系统中处理失效节点的问题。最早在1978年被提出,1982年由莱斯利·兰伯特(Leslie Lamport)等人完成了严格的理论解。而在1999年就有了高效的实用拜占庭容错算法(Practical Byzantine Fault Tolerance, PBFT),使得处理数百个节点的失效容错成为可能。但有意思的是,诞生于2008年的第一个区块链系统(比特币)却没有采用拜占庭共识类的算法,而是采用了完全不同的设计。

在BFT中,少数服从多数,需要依赖共识的参与者总体的先验知识。参与者的集合必须是预先设定好的,并且是大家预先知道的,或者大家至少需要知道一个总数。这意味着谁是参与者,需要有个预先协商和设定的过程,这在区块链系统中,被称为联盟链或者许可链(Permissioning Blockchain)。但在比特币网络这样一个纯粹开放的网络中,谁来批准你成为一个“拜占庭将军”呢?这是比特币系统不采用此类共识算法的根本原因。

在比特币网络中,冲突消解以一种不同的方式实现。当网络中同一个高度出现不同的区块时,比特币网络并不急于消解,而是允许这种不一致暂时存在,并允许产生后续的区块。这样区块链系统就会出现分叉(fork),区块链此时不再是一个链,而是一棵树。系统允许不同的控制方沿着不同的分叉产生区块。最终不同的分叉会出现长度差异,这时比特币共识协议约定,所有节点都应该认可最长的那条分叉,即最长链原则,从而最终实现冲突的消解,达成全网状态的一致,这个做法被称为最终一致性。

在完全开放的区块链网络中实现共识,比拜占庭将军问题更有挑战。两者的核心区别在于,前者的参与方的集合是未知的,甚至连参与方的总数都不知道。这种情况下,类似BFT的投票机制无法工作。而最终一致性共识算法本质上是让冲突双方在全网可观测的评判标准下(如最长子链)进行对抗,胜出者获得最终认可,达成一致。


权重定义


无论是即时一致性算法(图3)的投票,还是最终一致性算法(图4)的对抗,都是在比较冲突双方哪个比较强大,哪个是大多数。这个数(权重)具体用什么来定义,决定了这个网络的共识安全依赖于什么资源来保障。权重的定义通常需要权衡以下几个方面:

1. 任何一个节点都能够高效地观测到出块者的权重,并且各个节点能得到一致的结果;

2. 权重在全网的总量,最好比较稳定,不会在短时间内有巨大的变化;

3. 出块者为了给出权重的证明,尽可能涉及比较少的带宽、存储、计算等资源。

在一个网络系统中可以直接想到的是节点个数,或者将独立IP地址个数作为权重,但由于IP资源的控制方比较集中(运营商),单一控制方也很可能可以控制大量的IP地址(例如控制僵尸网络的黑客)。所以,至今没有任何区块链系统采用独立IP数量作为权重。

在比特币系统中,采用算力作为权重,即将控制者拥有的计算能力——每秒计算SHA256函数的次数作为权重。但事实上,算力是无法直接向远端证明的,比特币系统很巧妙地用工作量证明来间接证明控制方拥有的算力。工作量证明要求出块者找到一个段数据,使其哈希值具备某种规律(例如前面n个比特全部为0)。由于哈希函数的不可逆,穷举刺探是唯一的解法,从而使得掌握的算力越多,每秒可以完成穷举刺探的次数越多,就能更快地找到解,从而在同样的时间里,产出更多的新的区块。与之对应的,比特币系统采用最长链规则来消解冲突,本质上就是在比拼哪条分叉的子链需要更多的算力来构造。工作量证明具有一个非常大的优势:无论网络中的共识参与者有多少个,验证工作量证明仅需要固定的计算复杂度和通讯复杂度,不到100个字节。这使其具备非常好的可伸缩性,可以承载大规模的共识参与者。

在授权受控的联盟链系统中,权重通常被定义为签名个数,一签一票来完成投票过程。原理很简单,各个节点自己数收到多少个来自不同控制方的签名,并逐个验证。每个节点至少需要承载的计算复杂度和通讯复杂度是O(n),使共识机制的可伸缩性受限。当然在联盟链的业务场景中,共识的参与方也不多。资产证明(Proof-of-Stake, PoS)是另一种定义权重的方案,利用资产的数量定义少数服从多数的数量,先行定义BFT共识算法中所需要的预设的参与者集合。这样,也可以实现无许可链。

值得一提的是,大家曾对工作量证明有些误解。工作量证明带来算力竞争,即所谓的挖矿,确实消耗了大量能源。不过这也为工作量证明系统发行的每一个数字货币奠定了基础成本,使之价值有底线。但是工作量证明的算力与其区块链系统的交易处理的计算性能没有任何联系,任何加速挖矿算法的软件或者硬件都不会提高区块链系统单位时间的吞吐量 (Transaction Per-Second, TPS)。这就是为什么比特币全网哈希算力提高了万亿倍,但是其吞吐量一直是7 TPS 左右。另外,任何宣称节省挖矿能源的公开技术,都不可能在实际中减少能源消耗。因为投入挖矿的能源总量在矿场建立的时候已经确定,当有更高能效的挖矿技术或者设备出现时,算力竞争将导致所有矿工都应用新的技术,最终只是哄抬了全网的挖矿难度而已。实际的总能源消耗的量由矿工们能烧得起多少钱决定,在宏观上和币价、电价以及投资信心相关,和挖矿算法的效率无关。


来自分布式系统的挑战


区块链技术要实现更大规模的实际应用,在技术层面最大的挑战是性能和规模的提升。区块链系统本身是分布式系统的一种,和其他分布式系统一样,性能提升最大的障碍来自“分布式”这三个字。网络传输所带来的延迟和带宽限制是提升性能的第一绊脚石。为了脱离特定的物理计算设备,区块链在性能上付出了更多额外的代价。在不同的节点上间歇完成计算步骤,需要每个节点都准备好计算所需要的上下文和输入数据。在一个计算步骤完成之后,需要每一个节点都获取到最新的输出数据,并更新上下文。期间涉及到大量冗余的信息传递和存储以及相应的计算。无论设计如何优化,这些冗余的通讯、存储和计算是不可能彻底避免的。

一个区块链系统的性能瓶颈主要体现在两个方面,一个是吞吐量,另一个是状态容量。一个开放公链系统看起来有很多节点,其内存和计算能力总和是非常可观的,但是由于区块链系统要求每个节点都全量同步和构造整个网络的状态,导致每个节点实际上都在做完全重复的交易计算和状态表示。这是约束区块链系统性能的根本原因,使得全网的实际处理能力和容量等效于一台单机电脑。

吞吐量瓶颈 实际工作中的区块链节点,无时无刻不在接受全网的已确认区块以及未确认交易,并构造成链,不断维护账簿的最新状态,然后抓紧机会尝试在链尾追加新的区块。无论采用哪种共识算法,都会历经以下几个步骤:

1. 根据账簿的最新状态,在未确认交易集合中选出若干验证合法的交易,然后构造一个新的区块;

2. 新的区块参与出块的竞争或者候选,在这个阶段,很可能会因为账簿状态更新(其他节点成功出块)而中断,回到第一步;

3. 获得出块的权力之后,向全网广播新块,更新账簿状态,回到第一步。

无论哪种共识算法,都有一个不可调和的性能矛盾,本质上由区块数据的广播延迟导致。这个矛盾使得如果每次出块比较大(可以包含更多的交易),就必须有比较长的出块间隔,以保障该区块在下一次出块之前在全网被充分传播。如果传播不充分,在最终一致性共识系统中,将表现为较高的分叉率(出了无效的块),而在即时一致性共识系统中则表现为较高的失败率(区块在投票周期内拿不到2/3的同意票签名)。所以任何区块链系统都需要预先设定两个参数——块大小和出块间隔,使得在当时的网络环境中每次出块大都可以保证在下一个块出现之前完成充分的广播。这从本质上决定了一个区块链的吞吐量,其实和具体的共识算法没有太大关系,只要共识机制本身不拖后腿就行。

状态容量瓶颈 为了完成接力计算过程,每个节点都需要维持计算所需要的上下文,其中包括每个账户的状态(例如余额),以及待确认的所有交易。由于广播接力必须在新块被验证之后完成,为了保证接力过程以最快速度完成而不产生额外的广播延迟,这些信息需要常驻内存。在单链系统中,没有任何办法可以降低这个压力,无论采用什么共识算法都无法解决。这个瓶颈直接限制了全网用户量的上限,尤其在链上有很多应用的情况下。


中国自主创新的技术突破


相比比特币 7 TPS的性能,这几年新启动的区块链系统性能提高了不少,主要得益于现在的互联网带宽的提高。这些新系统可以采用更大的块,或者更短的出块时间,轻松获得至少几十倍的性能提升。当然,这对状态容量瓶颈的改善并没什么帮助。另外,提高参与节点的要求,甚至把节点全部部署在数据中心里面,这样很容易把性能提升上去,但是完全违背了区块链本身分散执行的初衷。

从根本上提升性能的办法,是从区块链系统性能受限的根本原因入手,即现在的区块链系统无限冗余,有多少个节点参与,就有多少倍冗余。基于P2P网络的开放区块链系统,有一定程度的冗余是必要的,有助于提高系统的安全性和鲁棒性。但是当全网有几千甚至上万节点的时候,这成千上万倍的冗余将使边际效应急速递减,着实不必要。那么是不是可以将区块链网络的全网工作量切分开来,不同部分的节点负责网络的不同部分?

这种分而治之的想法,在之前的并行计算系统中早已广泛采用,例如大数据处理中的Spark架构和MapReduce架构。同样,区块链系统也可以应用这样的思路,将全网工作量切分成多个分组,将账簿(与内存相关)、广播的范围(与通讯相关)、交易处理(与CPU相关)、交易归档(与硬盘存储相关)这四个方面的工作量全部切分开来。然后对应地将全网节点分成不同的组,每组承担一个分组。但是如何有效安全地实现这种切分,一直是区块链系统技术中的一个难题。在区块链系统中,应用这种分而治之的思路,不仅面临切分分布式系统通常都有的挑战,例如涉及有效的数据工作流以及要牺牲一些编程的灵活性,还面临区块链系统特有的挑战,例如缺乏中心调度,以及可能削弱共识的安全性。

这个技术方向终于在2019年取得突破性进展,是由毕业于中科院计算所的两名博士完成的,发表于该年的计算机网络系统顶级会议NSDI (Networked Systems Design and Implementation)上。这篇名为“Monoxide: Scale Out Blockchain with Asynchronized Consensus Zones”的论文1,以非常简洁的方式实现账户模型区块链系统的全面切分,并支持灵活的智能合约。在不引入任何中心化调度和协调的情况下,将全网切分成多个完全独立的异步并行子区块链系统,分摊全网的工作量,从本质上解决了区块链系统在性能上的两个瓶颈。论文提出了“最终原子性理论”,使得区块链系统在切分之后仍旧可以非常高效地完成业务逻辑在不同切分中的接力执行,使得整体性能实现线性提升。同时,论文还提出了“诸葛连弩挖矿”,保障了网络切分之后的安全性没有明显下降。在论文的真实互联网环境验证试验中,即使在非常保守的参数设定下,系统吞吐量高达1.16万TPS,状态容量高达16 TB。 ■


附:一个最小化的区块链系统

这里我们将以最简化的加密数字货币为例介绍区块链的精确工作原理,为了便于理解将省略手续费,大部分优化,互操作性等层面的东西。这里会用到伪代码,来精确定义其数据结构和执行逻辑。这里我们将从零开始实现一个类似比特币那样的区块链系统,为了便于理解,我们将采用以太坊所采用的账户-状态模型来表示账簿,而不是比特币的那种UTXO。


我们先从一系列基础实体和原语的定义开始:


● String  // 基础字符串数据结构

● Blob    // 基础二进制数据,用来表示对象序列化之后的线性二进制数据

● CriticalSection // 临界区,多线程互斥对象

● typedef BYTE PublicKey[32];   //公钥

● typedef BYTE PrivateKey[64];  //私钥

● typedef BYTE Signature[64];  //数字签名

● bool VerifySignature(Blob data, PublicKey pk, Signature sigdata); //验签


标准的非对称加密系统里面的实体,公私钥对可以在不联网的情况下,任意生成,并且全球唯一。通常为32到64字节的无结构二进制数据。其中公钥会公开,在区块链系统中用来表明特定身份,供他人验证其对特定账户的控制权。而私钥则用来通过数字签名来证明其对账户的控制。VerifySignature原语,用来对于给定数据和签名,验证是不是对应的签名者签署的。


● typedef BYTE HashValue[32]; //SHA256的哈希值

在我们这里的例子中,所有哈希函数都采用SHA256,其将产生一个32字节的哈希值。


● typedef HashValue Address;   //地址

地址是账户的标识符,是一个20字节或32字节的无结构二进制数据,由公钥的哈希值 SHA256(PublicKey) 得到。那么也就是说每个公钥,对应一个唯一的地址,对应一个唯一的账户。


● typedef BYTE BigInt[32];   //大整数

区块链中很多地方的数值采用大整数来表示,例如余额,挖矿难度等。一个32字节的无符号大整数,表示0到2^256-1的整数。


智能合约 (Smart Contract): 有点像一个 C++的类,定义了一些状态,以及修改这些状态的函数。一个区块链系统中,可以有多个智能合约同时存在,但是每个仅会有一个实例。这里我们就数字货币给出一个极度简化的智能合约的例子:

● Smart Contract

class MyCoin

{

       // internal state

       hash_map<Address, BigInt> _Ledger;


       // internal function

       BigInt _GetBalance(Address addr)

       {      if(_Ledger.has(addr))return _Ledger[addr];

              else return 0;

       }


       // transfer function, invoked by transaction

       void Transfer(Address signer, Address from, Address to, BigInt amount)

       {      if(signer != from)return;

              if(amount > 0 && _GetBalance(from) >= amount)

              {      _Ledger[from] -= amount;

                     amount += _GetBalance(to);

                     _Ledger[to] = amount;

              }

       }


       // mining reward function, invoked by block

       void Coin (Int64 height, Address miner)

       {      BigInt reward = 50 * 2^(-(int)(height/100000));

              if(reward > 0)

              {      reward += _GetBalance(miner);

                     _Ledger[miner] = reward;

              }

       }

};

其中,Coin 函数用来执行每次出块者的奖励,同时完成这个数字货币的发行过程。这里给出的是每出十万个奖励减半,直到奖励为0,发行结束。


● Transaction (交易): 一个交易表示对特定相关账户一次状态修改请求。交易中不携带任何逻辑代码,仅仅是指定这个交易将调用智能合约里面的哪个公开函数及其调用参数。当然在我们这个极度简化的系统中,只有一种交易,即前面的转账(Transfer)。交易的发起方必须为扣款方(from),并且整个交易携带对交易内容的数字签名,以确信该交易由扣款方发起。基于我们这里的例子,一个交易至少含有以下结构:


struct Transaction

{

       String InvokeFunctionName;  // 在我们这里 始终为 "Transfer"

       Blob InvokeArguments; // 序列化之后的调用参数

       PublicKey Signer;  // 发起者的公钥,注意这里不是地址

       Signature SignData;  // 由发起者的私钥对交易的签名

};



● Block (区块): 一个区块表示区块链接力执行中的一步,里面主要包含这一步中确认的一批交易,以及共识机制验证数据和块头元数据。一个最简化的定义可以是这样:


struct Block

{

       Int64 Timestamp;  // 出块时间

       HashValue PrevBlock;   // 上一个块的哈希值

       Address Miner;  // 矿工地址

       Int32 TxnCount;   // 这个块中包含的交易个数

       Transaction Txns[TxnCount];  // 完整的交易列表

       BigInt PowTarget;  // 工作量证明的目标 (共识验证数据)

       Int64  PowNonce; // 工作量证明的Nonce值 (共识验证数据)

};


这里我们给出了最简化的工作量证明(Proof-of-Work)的验证数据结构,如果采用其他共识算法,这个部分会有变化。从这个结构可以看出,区块链之所以称为链,就是因为区块结构中包含一个指向上一个区块的"指针",PrevBlock。任何一个被确认的区块,同时也意味着承认其全部的前驱区块,以及这些区块所携带的全部交易。一个区块被确认有三个条件:

1. 这个区块的共识验证要满足其特定共识算法的要求。在工作量证明算法中,PowTarget必须小于当前挖矿难度的要求,同时 ((BigInt&)SHA256(Block)) < Block::PowTarget。

2. 这个块所包含的交易必须没有被之前的区块包含过,并且每个交易必须能够保证其数字签名能够被其Signer的公钥正确验证。至于交易所执行的逻辑是否正确,是否出错则无关紧要。

3. 在所有分叉块中,即具有相同PrevBlock的块,只有优先的块会被确认。这一点不同的共识算法有不同的情况,在BFT系统中,分叉不会发生,所以无需考虑。而在PoW系统中,也有不同的规则定义分叉块的优先级。这一点会在后面的章节展开。



P2P通讯原语

区块链的网络层仅用到了P2P网络技术中简单的部分,用基于TCP长连接的Gossip协议实现一个数据块的全网广播(Flooding)。我们这里将其抽象下面的通讯原语:

● P2P Broadcast

Interface BroadcastNetwork

{

       template<typename T>

       void Broadcast(const T& );  // 将对象序列化并广播出去


       functor OnRecvBlock;  // 接收到一个区块的回调函数

       functor OnRecvTransaction;  // 接收到一个交易的回调函数

};


内存池(Mempool)原语

内存池在区块链系统中用来记录尚未被确认的交易,很容易用比如哈希表来实现。

● Mempool

Interface Mempool

{

       bool    Has(Transaction txn);

       void    Insert(Transaction new_txn);

       void    Remove(Transaction txns[count]);

       Int32   Collect(Transaction txns[max_count]);

};

其中Collect原语用于挖矿时合成新的区块,从mempool中挑出一系列交易来填充Txns数组,最多挑TxnMaxCount个,并返回实际填充的个数。



区块归档数据库原语

区块链系统中的区块以及交易,在被确认之后,将从内存中移除,并写入归档数据库中。这个部分很容易用一个Key-value storage系统来实现,当然用SQL数据可也是可以的,就是效率低一些。

● ArchiveData

Interface ArchiveData

{

       void  Archive(Transaction txns[count]);

       void  Archive(Block blk);

       void  Has(Transaction txn);

       void  Has(Block blk);

}




有了这些定义之后,我们可以给出一个不考虑分叉情况下最简单的基于工作量证明的区块链系统的伪代码:


static const Int32   TARGET_ADJUST_INTERVAL = 256;  // 每隔256个块调整一次算力难度

static const Int64   BLOCK_CREATION_INTERVAL = 600*1000; //每十分钟出一个块

static const Int32   TRANSCATION_PERBLOCK_MAX = 1024;   // 每块最多包含1024个交易


BroadcastNetwork*  g_pNet = BroadcastNetwork::Create(...);

Mempool*      g_pMempool = Mempool::Create(...);

ArchiveData *  g_pArchiDB = ArchiveData ::Create();


MyCoin   g_MyLedger;  // 账簿


// 当前区块链的头

Block   g_BlockHead = Block::GenesisBlock(6); // 初始化为创始区块

HashValue  g_BlockHeadHash = SHA256(g_BlockHead);

Int64    g_BlockNextHeight = 1;

CriticalSection   g_BlockHeadCS;


// 下一个块的共识相关信息 (工作量证明)

PowTarget   g_NextPowTarget = Block::InitialPowTarget(); // 初始挖矿难度

Int64    g_LastTargetAdjustedTime;



// 收到来自网络广播的交易

g_pNet-> OnRecvTransaction = [](Transaction txn) {

       if(g_pMempool->Has(txn) || g_pArchiDB->Has(txn))

              return;  // 忽略已经存在的交易

       if(!VerifySignature(

              txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,

              txn.Signer, 

              txn.Signature

       ))     return;   // 验证签名是否正确

       g_pNet->Broadcast(txn);  // 基本验证合法之后,接力这个交易的广播

       g_pMempool->Insert(txn);

};


// 收到来自网络广播的区块

g_pNet-> OnRecvBlock = [](Block blk) {

       if(blk.PrevBlock != g_BlockHeadHash)

              return;  // 忽略乱序到达的块,忽略分叉块

       if(blk.PowTarget > g_NextPowTarget)

              return;  // 忽略不满足当前算力要求的块

       if(blk.TxnCount > TRANSCATION_PERBLOCK_MAX)

              return;  // 忽略过于大的块


       HashValue h = SHA256(blk);

       if( ((BigInt&)h) >= blk.PowTarget )

              return;  // 忽略未达到当前标称算力要求的块


       for(Int32 i=0; i<blk.TxnsCount; i++)

       {      

              auto& txn = blk.Txns[i];

              if( g_pArchiDB->Has(txn) || // 包含之前已经被确认过的交易

                  !VerifySignature(

                     txn.InvokeFunctionName + txn.InvokeArguments +txn.Signer,

                     txn.Signer, 

                     txn.Signature

                   ) // 包含验签失败的交易

              )return;

       }


       // 至此这个区块被确认

       g_pNet->Broadcast(txn);  // 确认之后,尽快接力这个区块的广播


       g_MyLedger.Coin (g_BlockNextHeight, Miner); // 执行出块奖励

       for(auto& txn : blk.Txns)   // 执行每一条交易,然后归档

       {

              g_MyLedger[txn.InvokeFunctionName](txn.Signer, txn.InvokeArguments…);

              g_pArchiDB->Archive(txn);

              g_pMempool->Remove(txn); // 从内存池中删除,如果存在的话

       }


       g_pArchiDB->Archive(g_BlockHead);  // 归档上一个区块


       // 更新区块链头,这部分代码需要和挖矿过程中构造新的块的步骤互斥

       g_BlockHeadCS.Lock();

       if(g_BlockNextHeight%TARGET_ADJUST_INTERVAL == 1)

       {      // 进行算力调整,周期为 TARGET_ADJUST_INTERVAL 个区块

              if(g_BlockNextHeight > 1)

              {      g_NextPowTarget = PowTargetAdjustment(g_NextPowTarget, 

                         blk.Timestamp - g_LastTargetAdjustedTime

                                          );

              }

              g_LastTargetAdjustedTime = blk.Timestamp;

       }


       // 更新区块链头在最新的这个块

       g_BlockHeadHash = h;

       g_BlockHead = blk;

       g_BlockNextHeight++;

       g_BlockHeadCS.Unlock();

};


这里涉及到一个上面没有定义的算法,PowTargetAdjustment是用来根据近期出块速度来调整出块算力难度要求,从而使得出块的平均间隔的期望可以大体稳定在一个预先设定的值(BLOCK_CREATION_INTERVAL)。这是一个和工作量证明共识算法有关的算法,并不是所有区块链系统都有。这个算法的一个最简化定义如下:

● 算力难度调整

BigInt PowTargetAdjustment(BigInt cur_target, Int64 nth_block_interval)

{

       return cur_target*nth_block_interval

              /(BLOCK_CREATION_INTERVAL*TARGET_ADJUST_INTERVAL);

}


到这里一个不出块的区块链节点,即全节点就可以工作了。全节点是区块链网络中的大多数节点,是区块链底层P2P网络得以稳定鲁棒运行的保障,同时也实现了区块数据和交易数据的高度冗余的全网存储。虽然不出块,全节点不同于互联网架构的客户端。一个全节点不需要信赖其他节点,更不存在一个服务器。全节点能够独立自主地验证区块链完整的历史演进过程,进而重构其上的状态 (例如一个账户的余额),而不是去向一个需要信赖的服务器查询。


当然,区块链网络计算接力过程是由出块节点完成了,也就是所谓的矿工节点。这些少数节点,和大量的全节点混在一起,大部分节点收到最新的区块是来自于其他全节点的接力广播,而不是直接来自于一个出块节点。当然,作为接受方,也无从判断发送方是中继的全节点,还是刚刚出块的矿工节点。这也有效地保护了真正出块节点的安全性,避免暴露矿工节点的物理IP地址。


一个出块节点,首先是一个全节点,除了上面定义的这些行为之外,还需要一个额外的过程,运行在一个或者多个线程上。我们定义最简化的出块过程如下:


static const Address g_MyAddress =  Address("1ThisIsMyAddresse2V4GX7rMsKCGn6B");

static bool g_KeepMining = true;


void Mining()

{

       while(g_KeepMining)

       {

              // 构造新的块,这个部分需要和区块链头更新代码互斥

              g_BlockHeadCS.Lock();

              Int64 next_height = g_BlockNextHeight;

              Block new_block;

              new_block.Timestamp = os::GetCurrentTime();

              new_block.PrevBlock = g_BlockHeadHash;   // 指向最新的块

              new_block.Miner = g_MyAddress;

              new_block.TxnCount = g_pMempool->Collect(

                              new_block.Txns[TRANSCATION_PERBLOCK_MAX]);

              new_block.PowTarget = g_NextPowTarget;

              new_block.PowNonce = os::Random<Int64>();  // 随机初始值

              g_BlockHeadCS.Unlock();


              // 开始挖矿

              while(next_height ==  g_BlockNextHeight)

              {

                     if( ((BigInt64&)SHA256(new_block)) < new_block.PowTarget )

                     {

                            // 挖矿成功

                            g_pNet->Broadcast(new_block);  // 立即广播出去

                            g_pNet->OnRecvBlock(new_block); // 更新本节点的区块链头

                            break; // 开始去挖下一个块

                     }

                     new_block.PowNonce++; // 尝试下一个Nonce

              }

              // 大多数情况下,其他节点出了新的块,并更新了区块高度

              // 则挖矿被打断,去挖更新之后的下一个块

       }

}


 

 

脚注:

1 参阅https://zhuanlan.zhihu.com/p/55490520。





版权所有 © 2024 寒武纪 Cambricon.com 备案/许可证号:京ICP备17003415号-1
关闭