注意:
这份文档代表着Hyperledger Fabric v1.0的初始架构提案。 Hyperledger Fabric项目的具体实现过程中从概念上遵循这份架构提案,但是有一些细节在实现过程中已经被修改。这份架构提案是最初准备的,更多关于架构的技术上的精确陈述请参考:Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains. (也可以直接打开pdf文档:https://arxiv.org/pdf/1801.10228v2.pdf)
Hyperledger Fabric 的构架提供了以下优势:
第一部分:Fabric v1版本相关的架构的元素
1.系统构架
2.交易背书的基本工作流
3.背书策略
第二部分:Fabric v1后续版本的构架元素
4.账本检查点(Ledger checkpointing) (正在修改中)
区块链是一个由多个相互通信的结点所组成的分布式系统。区块链运行被称为链码的程序,持有状态和账本数据,并且执行交易。链码是核心元素,因为交易就是链码上的一系列调用操作。交易必须被背书(endorsed),只有背书过的交易才可能被提交,并改变状态。区域链中可能存在一个或多个特殊的用于管理功能和参数的链码,统一被成为系统链码。
1.1.交易
交易可以是两种类型:
就像之后将描述的,部署交易其实是调用交易的特殊情况,一个部署交易创建了一个新的链码,对应的是在一个系统链码上执行一次调用交易。
注意:该文档假设交易包括创建新链码的交易和在已部署的链码上执行调用操作的交易。该文档尚未描述的部分有:
a)查询交易的优化方法(包含在v1版本中)
b)对跨链码交易的支持(v1之后版本的特征)。
1.2.1.状态
区块链最新的状态(或者简单点儿,状态)是按照版本化的“键-值”这样的键值对格式存储(key-value store ,KVS),键是名称,值则是任意的对象。这些条目是由运行在区域链上的链码通过put和get这样的键值存储操作(KVS-operation)来进行操作的。状态是永久性存储的,对状态的更新会被记录。注意状态模型是采用版本化的KVS,实现方案可能是使用现行的KVSs,也可能是关系型数据库管理系统(RDBMS)或者其他解决方案。
更正式的来说,状态s被模型化为K -> (V X N)的映射元素,其中:
V和N都包含一个特殊的元素⊥(空类型),该元素是N的最小值(即最小的版本号)。初始时所有的键被映射成(⊥,⊥)。比如对于 s(k)=(v,ver),我们可以用 s(k).value 表示v,用 s(k).version 表示ver。
KVS操作按如下建模:
State由peer结点维护,而不是由orderer节点或者客户端节点。
状态分区 在KVS中的键可以从他的名字上被识别出来是属于特定的链码,也就是说特定的链码所进行的交易只能修改属于他的键。原则上,任何链码都可以读取属于其他链码的键。对于跨链码交易(cross-chaincode transactions),修改属于两个或多个链码的状态是v1后续版本的特性。
1.2.2.账本
账本提供了一个可验证的历史记录,该记录保存了系统运行期间所有成功或失败的状态的改变。
账本是由ordering服务(参看1.3.3部分)构建的一个完全按顺序排列的包含交易的区块的哈希链。该哈希链使得账本中的区块全部依序排列,每个区块包含一组依序排列的交易。这确保了所有交易都是依序排列的。
所有的peer结点都保存着账本,一部分orderer结点也可以选择性的保存账本。在orderer结点中我们把Ledger称为OrdererLedger,在peer结点中我们把Ledger称为PeerLedger。PeerLedger不同于OrdererLedger之处在于,peer结点维护一个位掩码(bitmask)并以此分开有效的交易和无效的交易(更详细的说明请参考XX部分)。
peer结点可以修改PeerLedger,像在XX(v1后续版本特性)部分所描述的那样。orderer节点为了容错性和PeerLedger的可用性而维护OrderLedger,并可以在任何时间修改OrderLedger,提供的ordering服务的属性也被相应维护。
账本允许peer节点重现所有交易历史,重构状态。因此,在1.2.1部分描述的状态是一个可选择的数据结构。
结点是区块链的通信实体。一个结点仅仅是指一个逻辑功能,从这种意义上说多种不同类型的结点可以运行在同一个实体服务器上。重要的是,结点如何按照 “trust domains” 进行组织,并且如何与控制它们的逻辑实体相联系。
这里有三种类型的结点:
这些类型的结点在下文有更详细的解释。
1.3.1. Client(客户端)
客户端代表着一个扮演了终端用户角色的实体。它必须连接到一个peer结点,与区块链进行通信。客户端可以根据它的选择连接到任何一个peer结点上。客户端创建并因而调用交易。
如在章节2所详述的,客户端既与peer结点通信,也与ordering服务结点通信。
1.3.2. Peer
一个peer结点从ordering服务结点接收以区块形式存储的排序后的状态信息,以及维护相应的世界状态和账本。
peer可以额外的扮演一个endorsing peer(或者称为endorser,背书者)的角色。背书者这个特殊的功能对应一个特殊的链码,发挥作用于需要对一个交易在提交之前进行背书时。每个链码可以指定一个背书策略,该背书策略可能涉及到一个背书结点集合(即定义了需要哪些peer背书后,该交易才能提交,类似于现实中的一个文件必须有哪些人签过字之后才能生效或者往上层提交一样)。背书策略定义了一个有效的交易背书所需的必要的和充分的条件(典型的就是一个所有背书者的签名集合),如下文章节2和3描述的那样。在安装新链码的部署交易这种特殊情形中,背书策略和系统链码的的背书策略一样。
1.3.3. Ordering service nodes (Orderers)
orderer结点形成了排序服务(the ordering service),即,一个提供传递担保的通信fabric。排序服务可以以不同的方式被实现:从一个中心化的服务(例如,在开发和测试中使用)到面向不同的网络、结点错误模型的分布式协议。
ordering服务为clients和peers提供了一个共享的通信通道,为包含交易的消息提供了广播服务。客户端连接到通道后可以在通道上广播消息,之后消息会被分发给所有的peer结点。通道支持对所有的消息进行原子分发,即,消息通信是在完全排序分发和可靠下进行的。换句话说,这个通道把消息输送到所有连接到该通道的peer结点,并且是按照完全相同的逻辑顺序。这个原子通信担保(atomic communication guarantee)又被称作完全排序广播(total-order broadcast),原子广播(atomic broadcast),或者分布式系统环境下的共识(consensus in the context of distributed systems)。对于区块链状态来说,通信的消息就成为了候选交易(供各个peer进行背书,签名等操作)。
分割(ordering service channels,ordering服务通道)。ordering服务可以支持多通道,类似于一个发行-订阅消息系统(publish/subscribe messaging system)。Clients可以连接到给定的通道,之后可以发送消息和获取到达的消息。通道可以被认为是分隔物(partitions),连接到一个通道的客户端不会意识到其他通道的存在,但是客户端可以连接到多个通道。尽管一些ordering服务实现,包括fabric,支持多通道,但为了呈现的简洁,接下来的文章中,我们假定ordering服务由单通道组成。
ordering服务API。peer通过ordering服务提供的接口连接到由ordering服务所提供的通道。ordering服务的API接口由两个基本的操作(更通用的异步事件)组成:
TODO 为获取 client/peer 指定的的序列号对应的区块而增加了部分接口。
账本和区块的结构。账本(也可以参看1.2.2部分)包含ordering服务输出的所有数据。从表面上看,它是一个deliver(seqno, prevhash, blob)事件的序列,根据上文所述的prevhash的计算,形成了一个哈希链。
大部分时间里,出于效率原因,ordering服务会将多个blob消息组成一组,然后在一个单独的deliver事件中输出区块,以此替代单个blob一个交易一个交易地输出的方式。这种情况下,ordering服务必须在每一个区块中强制并传递确定顺序的blob信息。区块中 blobs 的数量可以通过ordering服务进行动态选择。
下文中,为了陈述的方便,我们定义ordering服务的属性,并在一个blob消息对应一个deliver事件的假设上解释交易背书(章节2)的工作流。通过上文提到的对一个区块内的对blob消息确定性的排序,这些很容易被扩展到区块上面,假设一个区块的deliver事件,对应的是一个由多个单独的针对区块中一个个blob消息的deliver事件队列。
ordering服务属性
排序服务(或原子广播通道)的保证规定了已广播的消息会发生什么,以及已完成传递的消息之间存在什么关系。这些保证如下:
安全性(一致性保证):只要对等结点连接到通道有足够长的时间(他们可以断开连接或者宕机,但是会重启和重连),他们会看到唯一一串完成传递的(seqno, prevhash, blob)的信息。这意味着输出(即deliver()事件)在所有对等结点上依照序列号以相同的顺序发生,并且同一序列号携带的内容(blob和prehash)是一样的。注意,这仅仅是一个逻辑顺序,而且一个对等结点上的delever(seqno, prevhash, blob)不要求与在其他对等结点上输出相同消息的delever(seqno, prevhash, blob)有实时关系。换句话说,给定一个特定的seqno,没有两个对等结点传递不同的prevhash或blob值。此外,除非某个客户端(对等结点)实际调用了broadcast(blob),否则没有blob值会被传递,并且最好是,每个已广播的blob只传递一次。
此外,deliver()事件包含了前一个deliver()事件里的数据的加密哈希值(prevhash)。当排序服务实施原子广播保证时,prevhash是序列号为seqno-1的deliver()事件中的参数的哈希值。这样建立了一条跨deliver()事件的哈希链,用于校验排序服务输出的完整性,这会在后面的4、5节讨论到。第一个deliver()事件是个特例,prevhash有一个默认值。
活跃度(传递保证):排序服务的活跃度保证由一个具体的排序服务实现来指定。精确地保证依赖于网络和节点故障模型。
原则上,如果正在提交的客户端没发生故障,排序服务应该保证每一个连接到排序服务的正确的对等结点最终都传递每一个提交的交易。
总的来说,排序服务确保了如下属性 :
deliver(seqno, prevhash0, blob0)
和 deliver(seqno, prevhash1, blob1)
,有:prevhash0==prevhash1
并且 blob0==blob1
;deliver(seqno-1, prevhash0, blob0)
和 deliver(seqno, prevhash, blob)
, 有:prevhash = HASH(seqno-1||prevhash0||blob0)
.deliver(seqno, prevhash, blob)
,并且seqno>0
,那么p已经传递过事件deliver(seqno-1, prevhash0, blob0)
;deliver(seqno, prevhash, blob)
必定被某些对等结点(可能是与该结点不同的结点)的broadcast(blob)
事件先处理过。broadcast(blob)
和 broadcast(blob')
, 当两个事件 deliver(seqno0, prevhash0, blob)
和 deliver(seqno1, prevhash1, blob’)
在正确的对等结点上发生并且blob == blob’,那么seqno0==seqno1
并且 prevhash0==prevhash1
broadcast(blob)
事件,那么每一个正确的对等结点最终会发起一个 deliver(*, *, blob)
事件,其中*
代表任意值。下面我们概述一笔交易的高层请求流程。
备注:注意下面这条协议:不要假定所有的交易都是确定性的,也就是说,允许不确定的交易。
2.1. 客户端创建一笔交易并且发送给它选择的背书节点(The client creates a transaction and sends it to endorsing peers of its choice)
客户端向它选定的一组背书节点发送一个PROPOSE
消息(可能不是同时发送,见2.1.2和2.3节)来invoke一笔交易。这一组给定chaincodeID的背书节点通过对等结点对客户端提供服务,对等结点通过背书策略认同一组背书节点(见第3节)。举个例子,交易可以被发送到给定chaincodeID的所有背书者。也就是说,某些背书者可以是离线的,其他背书者可以拒绝为这笔交易背书。正在提交交易的客户端则是尝试着用有效的背书者去满足背书的策略表达。
下面我们先详述PROPOSE
消息的格式,然后讨论客户端和背书者之间可能的交互模式。
2.1.1. PROPOSE
消息格式(PROPOSE
message format)
PROPOSE消息的格式为
,其中,tx
是必须的,anchor
是可选的参数,将在下面解释。
tx=
, 其中:
clientID
是提交客户端的ID
chaincodeID
指向交易相关的链码
txPayload
是包含提交的交易的负载
timestamp
是由客户端维护的单调增(对于每个交易)的正整数
clientSig
是客户端对tx的其它字段的签名
txPayload
的具体细节在调用交易(invoke transactions)和部署交易(deploy transactions)中是不同的(也就是说,调用交易指向一个部署用的系统链码)。
对于调用交易,txPayload
将包含两个字段:
txPayload =
,其中:
operation
(操作)表示链码操作(函数)和参数
metadata
(元数据)表示与调用相关的属性
对于部署交易,txPayload
则由三个字段组成:
txPayload =
, 其中:
source
(源码)表示链码的源码
metadata
(元数据)表示链码和应用相关的属性
policies
包含所有peer可访问的链码相关的策略,如背书策略。注意:背书策略不是通过部署交易的txPayload提供的,但是部署交易的txPayload中包含背书策略的ID和其参数(见Section 3)。
anchor
包含读版本依赖,或者更具体地说,key版本对(也就是说,anchor
是KxN
的子集),绑定或"锚定"
PROPOSE
请求到KVS(参见Section 1.2.)中的特定版本的key。如果客户端指定了anchor
参数,背书者仅仅根据读取的与其本地KVS中的anchor
相匹配的keys的版本号对交易进行背书(更多细节,参见Section 2.2)。
tx
的加密哈希被所有的节点作为唯一的交易标识符tid使用(即,tid=HASH(tx)
)。客户端保存tid
在内存中,并等待从背书节点(endorsing peers)返回的响应。
2.1.2 消息模式
客户端决定与endorsers交互的顺序。比如,客户端通常会发送
(也就是说没有anchor
参数)给单个endorser,这个endorser会产生一个版本依赖(ancho
r), 客户端之后使用其作为PROPOSE
消息的参数发送给其它的endorser。又如,客户端可以直接发送
(没有anchor
)给其选择的所有endorsers。不同的通信模式都是可能的,客户端可以自由决定(也可参考2.3)。
2.2背书节点模拟交易并产生背书签名(The endorsing peer simulates a transaction and produces an endorsement signature)
在接收到从客户端发来的
消息后,背书节点 epID
首先验证客户端签名clientSig
,然后模拟交易。如果客户端指定了 anchor
,那么背书者只根据本地 KVS 中的,与anchor
指定的版本号相匹配的keys的版本号(也就是下面将描述的readset)进行模拟交易。
模拟交易涉及到:背书节点临时性的执行交易(txPayload),具体是通过执行交易指定的链码(即交易的chaincodeID指定的链码)以及背书结点本地持有的状态拷贝。
作为执行的结果,背书节点计算读版本依赖(readset,读集)和状态更新(writeset ,写集),也在DB语言中也被称为 MVCC+postimage。
回想一下,状态由键/值对组成。所有 key-value 条目都是有版本的,也就是说,每个条目包含有排序的版本信息,每次key中存储的value更新时其值都会递增。解释交易的peer记录所有的 key-value 对,可以被链码访问,包括读取和写入,但是peer目前还没更新其状态。更具体的说:
s
,对每个被交易读取的 k
, (k, s(k).version)
对会被添加到 readset
k
,其新的值为 v'
,(k, v')
对会被添加到writeset
。另外,v'
可以是新值到之前值( s(k).value
)的增量。如果客户端在PROPSOE
消息中指定了anchor
,那么客户端指定的anchor
必须与背书节点模拟交易时产生的readset
相等。
然后,peer转发内部的tran-proposal
(也可能是tx
)到其为交易背书的逻辑部分,被称为背书逻辑(endorsing logic)。默认情况下,peer的背书逻辑接受tran-proposal
,然后简单签名tran-proposal
。然而,背书逻辑可以解释任意功能,比如,与legacy系统以tran-proposal
和tx
作为输入进行交互,以决定是否为交易背书。
如果背书逻辑决定为交易背书,它会发送
消息给提交的客户端(tx.clientID
),其中:
tran-proposal := (epID, tid, chaincodeID, txContentBlob, readset, writeset)
,其中 txContentBlob
是txContentBlob
作为tx
的某种表示(比如,txContentBlob = tx.txPayload
)epSig
是背书节点对tran-proposal
的签名反之,如果背书逻辑拒绝为交易背书,endorser可能会发送一个消息(TRANSACTION-INVALID, tid, REJECTED
)给提交客户端。
注意,endorser不会在这一步修改其状态,在背书环境中的交易模拟产生的更新并不会影响状态!
2.3 提交客户端为交易收集背书并通过ordering服务广播该背书
提交客户端会等待接收到"足够"的信息和 (TRANSACTION-ENDORSED, tid, *, *)
上的签名,以确认交易提案已经被背书。如在2.1.2节中讨论的,这可能涉及到与 endorser 的一轮或多轮的往返交互。
"足够"的具体准确数目取决与链码的背书策略(也看参见章节3)。如果满足背书策略,则交易被背书,注意:交易现在还没有被提交。从背书节点收集到的签名过的TRANSACTION-ENDORSED
消息集合被称为一个背书,记为endorsement
。
如果提交客户端没有为交易提案成功收集到一个提案,它会放弃该交易,也可以选择稍后重试。
对于有有效背书的交易,我们现在开始使用ordering服务。提交客户端使用broadcast(blob)
来调用排序服务,其中blob=endorsement
。如果客户端没有直接调用排序服务的能力,它可以通过它选择的peer来代理它的广播。这样的peer必须被客户端信任它不会从endorsement
中移除任何消息,否则交易可能被视为无效的。注意,代理peer不能伪造一个有效的endorsement
。
2.4. 排序服务向对等结点传送一笔交易(The ordering service delivers a transactions to the peers)
当一个deliver(seqno, prevhash, blob)
事件发生并且一个对等结点已经为blobs更新了序号小于seqno的事件的所有状态,那么对等结点会做如下事情:
blob.tran-proposal.chaincodeID
) 的策略检查blob.endorsement
是否是有效的。blob.endorsement.tran-proposal.readset
)。在更复杂的情况中,背书中的tran-proposal
字段可能不同,这种情况下背书策略(见第3节)指定了状态将如何改变。依赖关系的验证可以通过不同的方法实现,根据其为状态更新所选择的一致性属性或"隔离保证"。串行化( Serializability)是一种默认的隔离保证,除非链码的背书策略指定了其他方式。
通过要求readset
中每个key所关联的版本等于状态中的key的版本,不符合这种要求的交易会被拒绝,来提供串行化。
PeerLedger
中这笔交易的比特掩码为1,将blob.endorsement.tran-proposal.writeset
应用于区块链状态(仅当tran-proposals
相同时,否则背书策略逻辑定义了函数使用blob.endorsement
)。blob.endorsement
失败了,则交易是无效的,对等结点标记PeerLedger
中这笔交易的的比特掩码为0。需要注意的是无效交易不改变状态。请注意,这足以让所有(正确的)对等结点在处理完给定序列号的deliver事件(区块)后达到相同的状态。也就是说,借助排序服务的保证,所有正确的对等结点会收到一串相同序列的deliver(seqno, prevhash, blob)
事件。因为对于背书策略的评估和readset
中版本依赖关系的评估都是确定的,所有正确的对等结点对于一个blob里的一笔交易是否有效都会得出相同的结论。因此,所有的对等结点提交并应用相同的交易序列,并且用相同的方式更新他们的状态。
Figure 1. Illustration of one possible transaction flow (common-case path).
(图1. 一个可能交易流程的示意(常用路径),流程图的①②③④分别与2.1,2.2,2.3,2.4对应。)
3.1. 背书策略规范(Endorsement policy specification)
一个背书策略(endorsement policy),是一个如何背书交易的条件。区块链的peer有一个预先指定的背书策略集,该背书策略集与安装指定链码的一个部署交易(deploy transaction)相关。背书策略可以被参数化,这些参数可以通过一个部署交易来指定。
为了保证区块链和安全属性,背书策略集合应该是一个经过验证的由有限的功能集合组成的策略集,以确保执行时间是有限的(termination,可终止),确定性(determinism),性能(performance)和安全保证(security guarantees)。
背书策略的动态添加(如,通过在链码部署时间进行deploy
交易)在有限的策略评定时间(termination),决策,性能和安全保证这些项上是非常敏感的。因此,背书策略的动态添加是不允许的,但是在将来可以被支持。
3.2. 对背书策略的交易评估(Transaction evaluation against endorsement policy)
一笔交易只有在依据背书策略被成功背书以后才被声明为有效的。调用链码的一个调用交易(invoke transaction)首先必须获得一个满足这个链码的策略的背书,否则它不会被提交。这发生在第2节所解释的提交客户端(submitting client)和背书节点(endorsing peers)之间的交互中。
形式上,背书策略是对背书的一个断言,判断其潜在地下一步状态是TRUE还是FALSE。部署交易的背书是通过系统性的策略获得的(比如,根据系统链码)。
背书策略断言引用了某些特定的变量。可能会引用:
endorsement
和endorsement.tran-proposal
的元素;以上列表是按表达性和复杂性升序排序的,也就是说,仅需要支持节点的密钥和身份的策略是相对简单的。
背书策略断言的评估必须是确定的。一个背书应该可以被每一个peer在本地评估,而不需要与其他peer交互,而且所有正确的peer都用相同的方法评估背书策略。
3.3. 背书策略示例
断言可能包含逻辑表达式,评估为 TRUE 或 FALSE。通常情况,条件会用由背书节点为链码发出的交易调用上的数字签名。
设定链码指定的背书者集合E = {Alice, Bob, Charlie, Dave, Eve, Frank, George}
。则一些示例策略如下:
从 E 中所有成员来的相同 tran-proposal 上的有效的签名
E中任何单个成员的有效签名
根据(Alice OR Bob) AND (any two of: Charlie, Dave, Eve, Frank, George)
这个条件决定的背书节点的对于同一个tran-proposal
的有效签名
由7个背书者中的任意5个背书者对同一tran-proposal
的有效签名。(更一般的说,对于有n > 3f
个背书者的链码,如果有来自n
个背书者中的2f+1
个,或者多于(n+f)/2
个背书者的有效签名,则有效)
假设向背书者分配“股份”或“权重”,比如{Alice=49, Bob=15, Charlie=15, Dave=10, Eve=7, Frank=3, George=1}
,总的股份是100:策略要求有效签名来自一组拥有多数股份的背书者(即一组组合股份严格超过50的背书者),比如{Alice, X}
,其中X
是除了George以外的任意人,或者{everyone together except Alice}
,等等。
上例中的股份分配机制可以是静态的(固化在链码的元数据中),也可以是动态的(比如,依赖于链码的状态并在运行期间修改)。
来自Alice或者Bob的对于tran-proposal1
的有效签名,和来自于(any two of: Charlie, Dave, Eve, Frank, George)
的对tran-proposal2
的有效签名,其中tran-proposal1
和tran-proposal2
仅在它们的背书节点和状态更新上是不同的。
这些策略有多大作用取决于应用程序,所期望的对抗坏的背书结点解决方案的弹性,以及各种其他属性。
PeerLedger
检查点(正在修改中)(post-v1). Validated ledger and PeerLedger checkpointing (pruning)参考:
fabric源码解析10——文档翻译之Architecture
[译文]Hyperledger Fabric V1.0 架构说明
区块链超级账本Hyperledger Fabric架构
Hyperledger Fabric 架构概述
HYPERLEDGER FABRIC 架构梳理
官方英文原文档:
https://hyperledger-fabric.readthedocs.io/en/latest/architecture.html