一个企业级区块链平台¶

这是一个为多数企业级业务场景提供模块化且灵活性支持的企业级授权分布式账本平台。
我们是谁¶
本文档由超级账本中国技术工作组(TWGC,Technical Working Group China)组织翻译,翻译工作都是由工作组成员工作之余贡献的,并非专业翻译人员,经过数月努力,v1.4.2 版本的 Hyperledger Fabric 终于翻译完成,在页面左下角可以选择英文版和中文版,中文翻译如有不妥之处,请在文档翻译仓库中直接提交 PR,文档翻译规则请参考 wiki 页面,参与过贡献的成员请将名字留在这里,如有问题可以联系这里的任意一位社区成员,大家都会热心解答。
Fabric 的后续版本以及超级账本下其他项目的文档,我们也会逐步进行翻译。
介绍¶
一般来说,区块链是一个由分布式网络中的节点维护的不可篡改的账本。这些节点通过执行被共识协议验证过的交易来各自维护一个账本的副本,账本以区块的形式存在,每个区块通过哈希和之前的区块相连。
第一个被广为人知的区块链应用是加密货币比特币,而其他应用都是从它衍生出来的。以太坊是另一种加密货币,它采用了不同方法,整合了许多类似比特币的特征,但是新增了智能合约为分布式应用创建了一个平台。比特币和以太坊属于同一类区块链,我们将其归类为*公共非许可(Public Permissionless)*区块链技术。这些基本上都是公共网络,允许任何人在上面匿名互动。
随着比特币、以太坊和其他一些衍生技术的普及,越来越多的人想要将区块链基础技术、分布式账本和分布式应用平台用到企业业务中去。但是,许多企业业务对性能要求较高,目前非许可区块链技术无法达到。此外,在许多业务中,对参与者身份要求比较严格,如在金融交易业务中,必须遵循“了解客户(Know-Your-Customer,KYC)”和“反洗钱(Anti-Money Laundering,AML)”的相关法规。
对于企业应用,我们需要考虑以下要求:
- 参与者必须是已认证的或者可识别的
- 网络需要获得许可
- 高交易吞吐量性能
- 交易确认低延迟
- 与商业交易有关的交易和数据的隐私和机密性
当前许多早期的区块链平台正在为企业应用做调整,而 Hyperledger Fabric 从一开始就设计为企业用途。下面的部分描述了 Hyperledger Fabric(Fabric)与其他区块链平台的不同,并讲解了其架构设计的一些理念。
Hyperledger Fabric¶
Hyperledger Fabric 是一个开源的企业级许可分布式账本技术(Distributed Ledger Technology,DLT)平台,专为在企业环境中使用而设计,与其他流行的分布式账本或区块链平台相比,它有一些主要的区别。
一个主要区别是 Hyperledger 是在 Linux 基金会下建立的,该基金会本身在开放式治理的模式下培育开源项目的历史悠久且非常成功,发展了强大的可持续社区和繁荣的生态系统。Hyperledger 由多元化的技术指导委员会进行管理,Hyperledger Fabric 项目由多个组织的不同的维护人员管理。从第一次提交以来,它的开发社区已经发展到超过35个组织和近200个开发人员。
Fabric 具有高度模块化和可配置的架构,可为各行各业的业务提供创新性、多样性和优化,其中包括银行、金融、保险、医疗保健、人力资源、供应链甚至数字音乐分发。
Fabric 是第一个支持通用编程语言编写智能合约(如 Java、Go 和 Node.js)的分布式账本平台,不受限于特定领域语言(Domain-Specific Languages,DSL)。这意味着大多数企业已经拥有开发智能合约所需的技能,并且不需要额外的培训来学习新的语言或特定领域语言。
Fabric 平台也是许可的,这意味着它与公共非许可网络不同,参与者彼此了解而不是匿名的或完全不信任的。也就是说,尽管参与者可能不会完全信任彼此(例如,在同行业竞争对手),但网络可以在一个治理模式下运行,这个治理模式是建立在参与者之间确实存在的信任之上的,如处理纠纷的法律协议或框架。
该平台最重要的区别之一是它支持可插拔的共识协议,使得平台能够更有效地进行定制,以适应特定的业务场景和信任模型。例如,当部署在单个企业内或由可信任的权威机构管理时,完全拜占庭容错的共识可能是不必要的,并且大大降低了性能和吞吐量。在这种的情况下,崩溃容错(Crash Fault-Tolerant,CFT)共识协议可能就够了,而在去中心化的场景中,可能需要更传统的拜占庭容错(Byzantine Fault Tolerant,BFT)共识协议。
Fabric 可以利用不需要原生加密货币的共识协议来激励昂贵的挖矿或推动智能合约执行。不使用加密货币会降低系统的风险,并且没有挖矿操作意味着可以使用与任何其他分布式系统大致相同的运营成本来部署平台。
这些差异化设计特性的结合使 Fabric 成为当今交易处理和交易确认延迟方面性能较好的平台之一,并且它实现了交易的隐私和保密以及智能合约(Fabric 称之为“链码”)。
让我们更详细地探索这些区别。
模块化¶
Hyperledger Fabric 被专门设计为模块化架构。无论是可插拔的共识、可插拔的身份管理协议(如 LDAP 或 OpenID Connect)、密钥管理协议还是加密库,该平台的核心设计旨在满足企业业务需求的多样性。
总体来看,Fabric 由以下模块化的组件组成:
- 可插拔的排序服务对交易顺序建立共识,然后向节点广播区块;
- 可插拔的成员服务提供者负责将网络中的实体与加密身份相关联;
- 可选的P2P gossip 服务通过排序服务将区块发送到其他节点;
- 智能合约(“链码”)隔离运行在容器环境(例如 Docker)中。它们可以用标准编程语言编写,但不能直接 访问账本状态;
- 账本可以通过配置支持多种 DBMS;
- 可插拔的背书和验证策略,每个应用程序可以独立配置。
业界一致公认,没有“可以一统天下的链(one blockchain to rule them all)”。Hyperledger Fabric 可以通过多种方式进行配置,以满足不同行业应用的需求。
许可和非许可区块链¶
在一个非许可区块链中,几乎任何人都可以参与,每个参与者都是匿名的。在这样的情况下,区块链状态达到不可变的区块深度前不存在信任。为了弥补这种信任的缺失,非许可区块链通常采用“挖矿”或交易费来提供经济激励,以抵消参与基于“工作量证明(PoW)”的拜占庭容错共识形式的特殊成本。
另一方面,许可区块链在一组已知的、已识别的且经常经过审查的参与者中操作区块链,这些参与者在产生一定程度信任的治理模型下运作。许可区块链提供了一种方法来保护具有共同目标,但可能彼此不完全信任的一组实体之间的交互。通过依赖参与者的身份,许可区块链可以使用更传统的崩溃容错(CFT)或拜占庭容错(BFT)共识协议,而不需要昂贵的挖掘。
另外,在许可的情况下,降低了参与者故意通过智能合约引入恶意代码的风险。首先,参与者彼此了解对方以及所有的操作,无论是提交交易、修改网络配置还是部署智能合约都根据网络中已经确定的背书策略和相关交易类型被记录在区块链上。与完全匿名相比,可以很容易地识别犯罪方,并根据治理模式的条款进行处理。
智能合约¶
智能合约,在 Fabric 中称之为“链码”,作为受信任的分布式应用程序,从区块链中获得信任,在节点中达成基本共识。它是区块链应用的业务逻辑。
有三个关键点适用于智能合约,尤其是应用于平台时:
- 多个智能合约在网络中同时运行,
- 它们可以动态部署(很多情况下任何人都可以部署),
- 应用代码应视为不被信任的,甚至可能是恶意的。
大多数现有的具有智能合约能力的区块链平台遵循顺序执行架构,其中共识协议:
- 验证并将交易排序,然后将它们传播到所有的节点,
- 每个节点按顺序执行交易。
几乎所有现有的区块链系统都可以找到顺序执行架构,从非许可平台,如 Ethereum(基于 PoW 共识)到许可平台,如 Tendermint、Chain 和 Quorum 。
采用顺序执行架构的区块链执行智能合约的结果一定是确定的,否则,可能永远不会达成共识。为了解决非确定性问题,许多平台要求智能合约以非标准或特定领域的语言(例如 Solidity)编写,以便消除非确定性操作。这阻碍了平台的广泛采用,因为它要求开发人员学习新语言来编写智能合约,而且可能会编写错误的程序。
此外,由于所有节点都按顺序执行所有交易,性能和规模被限制。事实上系统要求智能合约代码要在每个节点上都执行,这就需要采取复杂措施来保护整个系统免受恶意合约的影响,以确保整个系统的弹性。
一种新方法¶
针对交易 Fabric 引入了一种新的架构,我们称为执行-排序-验证。为了解决顺序执行模型面临的弹性、灵活性、可伸缩性、性能和机密性问题,它将交易流分为三个步骤:
- 执行一个交易并检查其正确性,从而给它背书,
- 通过(可插拔的)共识协议将交易排序,
- 提交交易到账本前先根据特定应用程序的背书策略验证交易
这种设计与顺序执行模式完全不同,因为 Fabric 在交易顺序达成最终一致前执行交易。
在 Fabric 中,特定应用程序的背书策略可以指定需要哪些节点或多少节点来保证给定的智能合约正确执行。因此,每个交易只需要由满足交易的背书策略所必需的节点的子集来执行(背书)。这样可以并行执行,从而提高系统的整体性能和规模。第一阶段也消除了任何非确定性,因为在排序之前可以过滤掉不一致的结果。
因为我们已经消除了非确定性,Fabric 是第一个能使用标准编程语言的区块链技术。在1.1.0版本中,智能合约可以用 Go 或 Node.js 编写,而在后续版本中,也将计划支持其他流行语言,包括 Java。
隐私和保密性¶
正如我们所讨论的,在一个公共的、非许可的区块链网络中,利用 PoW 作为其共识模型,交易在每个节点上执行。这意味着合约本身和他们处理的交易数据都不保密。每个交易以及实现它的代码,对于网络中的每个节点都是可见的。在这种情况下,我们得到了基于 PoW 的拜占庭容错共识却牺牲了合约和数据的保密性。
对于许多商业业务而言,缺乏保密性就会有问题。例如,在供应链合作伙伴组成的网络中,作为巩固关系或促进额外销售的手段,某些消费者可能会获得优惠利率。如果每个参与者都可以看到每个合约和交易,在一个完全透明的网络中就不可能维持这种商业关系,因为每个消费者都会想要优惠利率。
第二个例子考虑到证券行业,无论一个交易者建仓(或出仓)都会不希望她的竞争对手知道,否则他们将会试图入局,进而影响交易者的策略。
为了解决缺乏隐私和机密性的问题来满足企业业务需求,区块链平台采用了多种方法。所有方法都需要权衡利弊。
加密数据是提供保密性的一种方法;然而,在利用 PoW 达成共识的非许可网络中,加密数据位于每个节点上。如果有足够的时间和计算资源,加密可能会被破解。对于许多企业业务而言,不能接受信息可能受损的风险。
零知识证明(Zero Knowledge Proofs,ZKP)是正在探索解决该问题的另一个研究领域。目前这里的权衡是计算 ZKP 需要相当多的时间和计算资源。因此,在这种情况下需要权衡资源消耗与保密性能。
如果可以使用其他共识,或许可以探索将机密信息限制于授权节点内。
Hyperledger Fabric 是一个许可平台,通过其通道架构实现保密。基本上,Fabric 网络上的参与者可以在参与者子集之间建立“通道”,该子集中所包含的参与者应获权看到一部分特定交易。将此视为网络覆盖层。从而只有参与通道的节点才有权访问智能合约(链码)和交易的数据,保证了两者的隐私和保密性。
为了提高其隐私和机密性能,Fabric 增加了对私有数据(private data)的支持,并且正在开发的零知识证明。随着它的使用,将会有更多这方面的研究。
可插拔共识¶
交易的排序被委托给模块化组件以达成共识,该组件在逻辑上与执行交易和维护帐本的节点解耦。具体来说,就是排序服务。由于共识是模块化的,可以根据特定部署或解决方案的信任假设来定制其实现。这种模块化架构允许平台依赖完善的工具包进行 CFT(崩溃容错)或 BFT(拜占庭容错)的排序。
Fabric 目前提供了两种 CFT 排序服务的实现。第一个是基于 etcd
包的 Raft 协议。另外一个是 Kafka(其内部使用了 Zookeeper)。更多当前可用的排序服务请查阅排序服务概念文档。
另外,请注意,这些并不相互排斥。一个 Fabric 网络中可以有多种排序服务以支持不同的应用或应用需求。
性能和可扩展性¶
一个区块链平台的性能可能会受到许多因素的影响,例如交易大小、区块大小、网络大小以及硬件限制等。Hyperledger 社区性能和规模工作组(Performance and Scale working group)目前正在拟定关于性能和扩展等方面的一套衡量标准草案,同时实现一个叫 Hyperledger Caliper的基准测试框架。
虽然这项工作还在持续完善中,但应视为区块链平台性能和规模特征的确定性标准。来自 IBM Research 的一个团队发表了一份同行评审论文,评估了 Hyperledger Fabric 的架构和性能。该论文提供了对 Fabric 架构的深入讨论,并公布了该团队基于 Hyperledger Fabric v1.1 预发行版本对 Fabric 平台的性能评估。
研究团队所做的基准测试工作为 Fabric v1.1.0版本带来了巨大的性能改进,与v1.0.0版本性能水平相比,平台的整体性能提高了一倍以上。
结论¶
任何对区块链平台严谨的评估都应该在其名单中包含 Hyperledger Fabric。
而且,Fabric 的这些特性使其成为一个高度可扩展的系统,该平台是支持灵活的信任假设的许可区块链,因此能够支持从政府、金融、供应链物流到医疗保健等各种的行业应用。
更重要的是,Hyperledger Fabric 是(目前)十个 Hyperledger 项目中最活跃的。围绕平台的社区建 设正在稳步增长,相继每个版本提供的创新远远超过任何其他企业区块链平台。
致谢¶
前面的内容源自同行审阅的”Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains”(“Hyperledger Fabric:一个许可区块链的分布式操作系统”) - Elli Androulaki,Artem Barger,Vita Bortnikov,Christian Cachin,Konstantinos Christidis,Angelo De Caro,David Enyeart,Christopher Ferris,Gennady Laventman,Yacov Manevich,Srinivasan Muralidharan,Chet Murthy,Binh Nguyen,Manish Sethi,Gari Singh,Keith Smith,Alessandro Sorniotti,Chrysoula Stathakopoulou,Marko Vukolic,Sharon Weed Cocco,Jason Yellick
v1.4 更新说明¶
Hyperledger Fabric 的第一个长期支持发布版本¶
Hyperledger Fabric 从最初的v1.0发布开始就已经成熟了,Fabric 运维人员社区也是如此。Fabric 开发者和网络维护者一起合作发布了v1.4,该版本致力于稳定性和生产运营。同时,v1.4.x将是我们第一个长期支持发布版本。
我们目前的策略是为我们主要和次要版本提供 bug 修复版本,直到下一个主要或者次要版本发布。我们计划在后续的版本中继续实行这一原则。但是,对于 Hyperledger Fabric v1.4, Fabric 维护者们承诺将从发布之日起提供为期一年的 bug 修复。这将产生一系列的补丁版本(v1.4.1, v1.4.2等等),一个补丁版本会包含多个修复内容。
如果你正在使用 Hyperledger Fabric v1.4.x,你将可以安全地升级到后续的每个补丁版本中。当需要一些升级过程来修复缺陷时,我们将在补丁发布的时候提供升级步骤。
Raft 排序服务¶
在 v1.4.1 中介绍过, Raft 是一种崩溃容错(Crash Fault Tolerant,CFT)排序服务,它是 Raft 协议基于 etcd 的实现。Raft 使用了“领导者和跟随者”模型,领导者是(每个通道)选举出来的,他们的决定复制给跟随者。Raft 排序服务的配置和管理比基于 Kafka 的排序服务更容易,并且可以让分布在世界各地的组织为分散的排序服务贡献节点。
- 排序服务: 讲解了 Fabric 中排序服务和目前三种可用的排序类型:Solo, Kafka 和 Raft。
- 配置并使用 Raft 排序服务: 演示了部署 Raft 排序服务时需要配置的参数和注意事项。
- 设置排序节点: 讲解了部署排序节点的过程。部署排序节点并不意味着实现了排序服务。
- 构建你的第一个网络: 该教程中增加了启动一个使用 Raft 排序服务的示例网络的内容。
- 从 Kafka 迁移到 Raft: 如果你正在使用 Kafka 排序服务,这个文档演示了迁移到 Raft 的过程。v1.4.2以后可用。
可用性和可操作的改进¶
越来越多的 Hyperledger Fabric 网络进入了生产阶段,可用性和可操作性方便变得至关重要。 Fabric v1.4 在日志记录改进、健康检查和操作指标方面取得了巨大的进步。因此,Fabric v1.4是生产操作的推荐版本。
- 操作服务:
新的 RESTful 操作服务为操作者提供了三种服务来监控和管理 Peer 节点和排序节点:
- 日志的
/logspec
可以让操作者获取和设置 Peer 节点和排序节点的日志级别。 /healthz
可以让操作者和容器业务流程服务检查 Peer 节点和排序节点的存活和健康状态。/metrics
可以让操作者用 Prometheus 从 Peer 节点和排序节点拉取 Matrics(操作度量)。Matrics 也可以推送到 StatsD。
- 日志的
改进了应用开发编程模型¶
编写 DAPP 变得更简单了。Node.js SDK 和 Node.js 链码编程模型的改进使得 DAPP 的开发更加直观,使你能够专注于应用程序逻辑。
通过个商业票据业务网络场景的新文档帮你理解开发 Hyperledger Fabric DAPP 中的各种概念。
- 场景: 以一个假设的业务网络作为案例来讲解编程模型,这个网络包含了六个组织,他们想要构建一个要一起进行交易的应用程序。
- 分析: 描述了商业票据的结构和交易是如何随时间影响它的。演示了使用状态和交易进行建模,对理解和去中心化业务流程建模提供了精确的方法。
- 流程和数据设计: 展示了如何设计商业票据流程和相关的数据结构。
- 智能合约处理: 展示了如何设计一个智能合约来管理去中心化业务流程中的发行、购买和赎回。
- 应用 从概念上描述了使用 智能合约处理 中描述的智能合约的客户端应用程序。
- 应用程序设计元素: 描述了智能合约中命名空间、交易上下文、交易处理、连接配置、连接选项、钱包和网关的详细信息。
最后,一个教程和示例将商业票据场景带到了现实中:
新教程¶
- 编写你的第一个应用: 该教程使用改进的 Node.js SDK 和链码模型进行了更新。教程中提供了 JavaScript 和 Typescript 版客户端程序和链码。
- 商业票据教程 如上所述,这是新的应用开发文档附带的教程。
- 升级到最新版 Fabric: 使用 构建你的第一个网络 中的网络演示了从 v1.3 升级到 v1.4 过程。包含了脚本(可以作为升级的模板)和独立命令让你来理解升级中的每一步骤。
发布说明¶
发布说明为用户使用新版本提供更多细节,定点击下边的链接获取完整的版本变更日志。
关键概念¶
介绍¶
Hyperledger Fabric 是一个分布式账本解决方案的平台,采用模块化架构,提供高度机密性,弹性,灵活性和可扩展性。它旨在支持不同组件的可插拔实现,并适应经济生态系统中存在的复杂性。
我们建议初次使用的用户从通读下面的部分开始,以便熟悉区块链的工作原理以及 Hyperledger Fabric 的特性和组件。
如果您已经熟悉了区块链和 Hyperledger Fabric,那么可以访问 入门,然后在那里查看示例、技术规范和 API 等。
什么是区块链?¶
一个分布式账本
区块链网络的核心是一个分布式账本,记录网络上发生的所有交易。
区块链账本通常被描述为 去中心化的,因为它被复制到许多网络参与者中,每个参与者共同 协作 来维护它。我们将看到,去中心化和协作是反映现实世界中企业交换商品和服务方式的重要属性。

除了去中心和协作之外,记录到区块链的信息只可以追加,使用了加密技术来保证一旦将交易添加到账本中,就不能对其进行修改。这种“不可篡改”的属性使确定信息来源变得很简单,因为参与者可以确定信息在事后没有被更改。这就是为什么区块链有时被描述为 证明系统。
智能合约
为了支持信息的一致更新——并启用所有账本功能(交易、查询等)——区块链网络使用**智能合约**提供对账本的受控访问。

智能合约不仅是一种封装信息并使其在网络上保持简单的关键机制,还可以通过编写它们来允许参与者自动执行特定方面的交易。
例如,可以编写一个智能合约来规定货物的运输成本,其中运费根据货物到达的速度而变化。根据双方同意的条款并写在账本上,当收货时相应的资金自动转手。
共识
整个网络中保持账本交易同步的过程被称为 共识,该过程确保只有交易被适当的参与者批准时才更新账本,并且当账本确实更新时,它们以相同的交易顺序更新。

以后你会学到更多关于账本、智能合约和共识的知识。现在,只要将区块链看作是一个共享和复制交易的系统就足够了,它通过智能合约进行更新,并通过一个称为共识的协作过程保持一致。
区块链为什么有用?¶
当今的记录系统
今天的交易网络只不过是自保留业务记录以来就存在的网络的稍加更新的版本。一个 业务网络 的成员彼此进行交易,但是他们维护各自的交易记录。无论是16世纪的佛兰德挂毯,还是今天的证券,他们所交易的东西都必须在每次出售时确定其来源,以确保出售物品的企业拥有一系列产权,来验证其所有权。
现在的商业网络看起来像这样:

现代技术已经将这一过程从石碑和文件夹带到硬盘和云平台,但其底层结构是相同的。由于没有统一的系统来管理网络参与者的身份,因而建立追溯体系非常费力,通常需要数天清理证券交易(全世界有数万亿美元),合同必须手动签署和执行,每个数据库系统中都包含唯一的信息,因此代表了单点故障。
尽管对可见性和信任的需求是很明显的,今天的信息和流程共享方法也不可能构建一个跨越业务网络的记录系统。
区块链的不同之处
如果商业网络有标准的方法在网络上建立身份、执行交易和存储数据,而不是用“现代”交易系统来表示效率低下的老鼠洞,那会怎么样?如果可以通过查看一列交易来确定资产的来源,而这些交易一旦写入,就不能更改,因此可以信任,那会怎么样?
该业务网络看起来更像是这样的:

这是一个区块链网络,每个参与者都有自己复的账本副本。除了共享账本信息,更新账本的过程也被共享。不像今天的系统,参与者的 私有 程序用于更新他们的 私有 账本,区块链系统有 共享 程序来更新 共享 账本。
区块链网络能够通过共享账本协调其业务网络,可以减少和私有信息相关的处理时间、成本和风险,同时提高信任和可见性。
您现在知道了区块链是什么以及它为什么有用。还有很多其他的重要细节,基本思想都与共享信息和过程有关。
什么是 Hyperledger Fabric?¶
Linux 基金会于 2015 年创建了 Hyperledger 项目,以推进跨行业的区块链技术。它不是用以宣布单一区块链标准,而是鼓励通过社区流程协作开发区块链技术的方法,其中包括鼓励开放式开发和随着时间的推移采用关键标准的知识产权。
Hyperledger Fabric 是超级账本中的区块链项目之一。和其他区块链技术一样,它有一个账本,使用智能合约,是一个参与者管理交易的系统。
Hyperledger Fabric 与其他区块链系统的不同之处在于,它是 私有的 和 许可的。与允许匿名参与网络的开放无许可系统(需要像“工作证明”这样的协议来验证交易并保护网络)不同,Hyperledger Fabric 网络成员通过一个受信任的 成员服务提供者(MSP) 来注册。
Hyperledger Fabric 还提供多种可插拔选项。账本数据可以以多种格式存储,共识机制可以在内部和外部交换,并且支持不同的 MSP。
Hyperledger Fabric 还提供了创建 通道 的功能,支持构成通道的一组参与者创建不同于通道外成员的交易账本。对于某些网络而言,这是一个特别重要的选项。这些网络中,一些参与者可能是竞争对手,并且不希望他们做出的每笔交易被每个参与者知晓, 例如,他们向某些特定参与者提供的特殊价格。如果两个参与者组成一个通道,那么这两个参与者就拥有该通道的账本副本,而其他参与者没有。
共享账本
Hyperledger Fabric 有一个账本子系统,包括两个组件:世界状态 和 交易日志。对于所属的 Hyperledger Fabric 网络,每个参与者都有一份账本的副本。
世界状态组件描述在给定时间点的账本状态。它是账本的数据库。交易日志组件记录产生当前世界状态的所有交易;它是世界状态的更新历史。因此,账本是世界状态数据库和交易日志历史记录的组合。
账本为世界状态提供了一个可替换的数据存储。默认情况下,这是一个 LevelDB 键值存储数据库。交易日志是不需要可插拔的。它只简单地记录区块链网络使用的账本数据库的之前和之后的值。
智能合约
智能合约是用 链码 编写的,当区块链网络之外的某个应用程序需要与账本交互时,该应用程序就会调用此网络中的智能合约。多数情况下,链码仅与账本的数据库组件,即世界状态,进行交互(例如,查询世界状态),而不与交易日志交互。
链码可以用多种编程语言实现。目前支持 Go 和 Node。
隐私
根据网络的需要,企业对企业(B2B)网络中的参与者可能对他们共享的信息量非常敏感。对于其他网络来说,隐私不会是他们最关心的问题。
Hyperledger Fabric所支持的网络有:将隐私作为一项关键运行要求的网络;相对开放的网络。
共识
交易必须按照发生的顺序写在账本上,即使它们可能位于网络中不同的参与者之间。为了做到这一点,必须建立交易的顺序,且必须采用一种方法来拒绝错误(或恶意)插入到账本中的错误交易。
这是计算机科学深入研究的一个领域,有许多方法可以实现它,每种方法都有不同的权衡。例如,PBFT(实用拜占庭容错)可以为文件副本提供一种相互通信的机制,使其能够保持各个副本的一致性,即使在发生损坏的情况下也是如此。或者,在比特币中,通过称为挖掘的过程进行排序,在这个过程中,相互竞争的计算机竞相解决一个密码谜题,这个谜题定义了所有过程随后建立的顺序。
Hyperledger Fabric 的设计允许网络启动者选择一种最能代表参与者之间关系的共识机制。就像隐私一样,有一系列的需求:从关系中高度结构化的网络到更加对等的网络。
我们将学习更多关于 Hyperledger Fabric 的共识机制,目前包括 SOLO 、Kafka 和 Raft。
我在哪里可以了解更多?¶
- 身份 (概念文档)
概念文档,将带您了解身份在 Fabric 网络中扮演的关键角色(使用已建立的 PKI 结构和 x.509 证书)。
- 成员 (概念文档)
讨论成员服务提供者(MSP)的角色,该角色将身份转换为 Fabric 网络中的角色。
- 节点 (概念文档)
节点为组织所有,承载着账本和智能合约,构成了 Fabric 网络的物理结构。
- 构建你的第一个网络 (教程)
了解如何使用示例脚本下载 Fabric 二进制文件并引导你自己的示例网络。然后拆解网络,了解它是如何一步一步构建的。
- 编写你的第一个应用 (教程)
部署一个非常简单的网络,甚至比构建第一个网络还要简单,使用一个简单的智能合约和应用程序。
整体看一个简单的交易流程。
本文整体介绍了一些组件和概念,还有一些其他内容并描述了他们如何在示例交易流中协同工作。
Hyperledger Fabric 功能¶
Hyperledger Fabric 是分布式账本技术(DLT)的一种实现,它在模块化的区块链架构中提供企业级的网络安全、可伸缩性、保密性和性能。Hyperledger Fabric 提供了以下区块链网络功能:
身份管理¶
为了启用许可网络,Hyperledger Fabric 提供了一个成员身份服务,用于管理用户 ID 并认证网络上的所有参与者。访问控制列表可以通过授权特定的网络操作来提供额外的权限层。例如,可以允许特定的用户 ID 调用链码应用程序,但是不能部署新的链码。
隐私和保密¶
Hyperledger Fabric 使竞争商业利益以及任何需要私人机密交易的群体能够在同一个许可网络上共存。私有 通道 是受限制的消息传递路径,可用于为网络成员的特定子集提供交易隐私和机密性。任何未正式受权访问某通道的网络成员都无法看到、也无法访问该通道上的任何数据,包括交易、成员和通道信息在内。
高效的处理¶
Hyperledger Fabric 按节点类型分配网络角色。为了向网络提供并发性和并行性,交易执行与交易排序和提交是分离的。在对交易进行排序之前执行交易可以使每个 Peer 节点同时处理多个交易。这种并发执行提高了每个 Peer 节点上的处理效率,并加速了交易提交到排序服务的过程。
除了支持并行处理之外,分工还为排序节点减轻了交易执行和账本维护的负担,而 Peer 节点则不需要执行排序(共识)。这种角色的划分还限制了授权和身份验证所需的处理;所有 Peer 节点不必信任所有排序节点,反之亦然,因此一个节点上的进程可以独立于另一个节点的验证运行。
链码功能¶
链码应用程序是供通道上特定类型的交易调用的逻辑。例如,为资产所有权变更定义参数的链码,确保所有转移所有权的交易都遵循相同的规则和要求。系统链码 与一般链码不同,它为整个通道定义了操作参数。生命周期和配置系统链码定义了通道的规则;背书和验证系统链码定义了背书和验证交易的需求。
模块化设计¶
Hyperledger Fabric 实现了模块化架构,为网络设计者提供了功能选择。例如,身份、排序(共识)和加密的特定算法可以插入任何 Hyperledger Fabric 网络。这会产生任何行业或公共领域都可以采用的通用区块链架构,并确保其网络可跨市场、监管和地理边界进行互操作。
Hyperledger Fabric 模型¶
本节概述了 Hyperledger Fabric 的关键设计特性,实现了对全面又可定制的企业区块链解决方案的承诺:
- 资产 —— 资产的定义使得几乎任何具有货币价值的东西都可以通过网络进行交换,从所有食品、到古董车、到货币期货。
- 链码 —— 链码的执行与交易排序是分开的,这限制了跨节点类型所需的信任和验证级别,并优化了网络可扩展性和性能。
- 账本特征 —— 不可篡改的共享账本为每个通道编码整个交易历史,并包含类似 SQL 的查询功能,用于有效的审计和解决争议。
- 隐私 —— 通道和私有数据收集支持私有和机密的多边交易,这通常是在公共网络上交换资产的竞争企业和受监管的行业所需要的。
- 安全和成员服务 —— 被许可的成员资格提供了一个可信的区块链网络,参与者知道所有的交易都可以被授权的监管机构和审计人员检测和跟踪。
- 共识 —— 达成共识的独特方法支持企业所需的灵活性和可扩展性。
资产¶
资产的范围可以从有形的(房地产和硬件)到无形的(合同和知识产权)资产。Hyperledger Fabric 提供了使用链码交易修改资产的能力。
资产在 Hyperledger Fabric 中表示为键值对的集合,状态更改记录为在 Channel 账本上的交易。资产可以用二进制或 JSON 格式表示。
在 Hyperledger Fabric 应用程序中,您可以使用 Hyperledger Composer 工具轻松定义和使用资产。
链码¶
链码是定义一项或多项资产的软件,以及修改资产的交易指令;换句话说,它就是业务逻辑。链码强制执行读取或更改键值对或其他状态数据库信息的规则。链码函数针对账本的当前状态数据库执行,并通过交易提案发起。链码执行产生一组键值写集,可以提交给网络并应用于所有节点上的账本。
账本特征¶
账本是 Fabric 中所有状态转变的有序的、防篡改的记录。状态转换是参与方提交的链码调用(“交易”)的结果。每笔交易都会产生一组资产键值对,这些键值对在创建、更新或删除时提交到账本。
账本由一个区块链(“链”)组成,它以区块的形式存储不可变的、有序的记录,以及一个状态数据库来维护当前的 Fabric 状态。每个通道有一个账本。每个节点为其所属的每个通道维护一份账本副本。
Fabric 账本的一些特点:
- 查询和使用基于键的查找更新账本、范围查询和组合键查询
- 使用富查询语言的只读查询(如果使用 CouchDB 作为状态数据库)
- 只读历史查询——查询某一个键的账本历史记录,支持数据溯源场景
- 交易由链码中读取的键/值(读集)版本和链码中写入的键/值(写集)的版本组成
- 交易包含每个背书节点的签名,并提交给排序服务
- 交易排序后进入区块,并从排序服务“交付”到通道上的节点
- 节点根据背书策略验证交易并执行这些策略
- 在添加到区块之前,执行版本控制检查,以确保所读取的资产的状态自链码执行时间以来没有更改
- 一旦交易被验证和提交,就不可改变
- 通道的账本包含一个定义了策略、访问控制列表和其他相关信息的配置区块
- 通道包含 成员服务提供者 实例,允许从不同的证书颁发机构派生加密材料
有关数据库、存储结构和“查询能力”的更深入研究,请参阅 Ledger 。
隐私¶
Hyperledger Fabric 在每个通道的基础上使用一个不可篡改的账本,以及可以操作和修改资产当前状态(即更新键值对)的链码。账本存在于通道的范围内——它可以在整个网络中共享(假设每个参与者都在一个公共通道上操作)——或者它可以私有化,只包含一组特定的参与者。
在后一种情况下,这些参与者将创建一个单独的通道,从而隔离他们的交易和账本。为了解决跨越透明度和隐私之间的差距,链码只可以安装于 Peer 节点上,需要访问执行读取和写入的资产状态(换句话说,如果一个链码不是安装在 Peer 节点上,它将无法正确地与账本链接)。
当该通道上的一个组织子集需要对其交易数据保密时,将使用一个私有数据集将该数据隔离在一个私有数据库中,从逻辑上与通道账本分离,只有经过授权的组织子集才能访问该数据。
因此,通道使交易对更广泛的网络保持私有,而集合使通道上组织的子集之间的数据保持私有。
为了进一步模糊数据,可以使用常见的加密算法(如 AES)对链码中的值进行加密(部分或全部),然后再将交易发送给排序服务并将区块添加到账本中。一旦加密数据被写入账本,就只能由拥有用于生成密码文本的相应密钥的用户解密。有关链码加密的详细信息,请参阅 链码开发者教程 。
有关如何在区块链网络上实现隐私的更多细节,请参阅 私有数据 。
安全和成员服务¶
Hyperledger Fabric 支撑着一个所有参与者都知道身份的交易网络。公钥基础设施用于生成与组织、网络组件、最终用户或客户端应用程序绑定的加密证书。因此,可以在更广泛的网络和通道级别上操纵和控制数据访问控制。这种“许可”概念的 Hyperledger Fabirc,加上通道的存在和功能,有助于解决隐私和机密性是最重要的问题。
请参阅 成员服务提供者 (MSP) 主题,以更好地理解加密实现,以及在 Hyperledger Fabric 中使用的签名、验证和身份验证方法。
共识¶
在分布式账本技术中,共识已成为单个函数内特定算法的同义词。然而,共识不仅包括简单地就交易顺序达成一致,而且 Hyperledger Fabric 通过其在整个交易流程中的基本角色,从提案和背书,到排序,验证和提交,突出了这种区别。简而言之,共识被定义为包含在一个区块中的一组交易的正确性的全循环验证。
当区块的交易的顺序和结果满足显式策略标准检查时,最终就会达成共识。这些检查和平衡发生在交易的生命周期中,包括使用背书策略来规定哪些特定成员必须背书某个交易类,以及系统链码来确保这些策略得到执行和维护。在提交之前,节点将使用这些系统链码来确保存在足够的背书,并且背书来自适当的实体。此外,在将任何包含交易的区块添加到账本之前,还将进行版本检查,在此期间对账本的当前状态达成共识。最后的检查提供了对双花操作和其他可能损害数据完整性的威胁的保护,并允许对非静态变量执行功能。
除了进行大量的背书、有效性和版本检查之外,还在交易流的各个方向进行身份验证。访问控制列表是在网络的层次结构层上实现的(从排序服务到通道),当交易提案通过不同的架构组件时,有效负载被重复签名,确认和验证。总而言之,共识并不仅仅局限于一批交易的商定顺序;相反,它是一种总体特征,是在交易从提议到担保的过程中进行的验证的附属产物。
查看 交易流程 以获得共识的直观表示。
区块链网络¶
本主题将在概念层次上描述 Hyperledger Fabric 是如何允许组织间以区块链网络的形式进行合作的。如果您是架构师、管理员或开发人员,您可以使用这个主题来深入了解 Hyperledger Fabric 区块链网络中的主要结构和流程组件。本主题将使用一个可管理的工作示例介绍区块链网络中的所有主要组件。理解了这个示例之后,您可以在文档的其他地方阅读关于这些组件的更详细的信息,或者尝试构建一个示例网络。
阅读本主题并理解策略的概念之后,你就能够很好的理解组织在控制一个部署好的 Hyperledger Fabric 网络时所需要建立的决策。您还将了解组织如何使用声明性策略(Hyperledger Fabric的一个关键特性)管理网络演化。简而言之,您将了解 Hyperledger Fabric 的主要技术组件以及组织需要对此做出的决策。
什么是区块链网络?¶
区块链网络是为应用程序提供账本和智能合约(链码)服务的技术基础设施。首先,智能合约用于生成交易,这些交易随后被分发到网络中的每个节点,并在每个节点的账本副本上记录下来并且是不可篡改的。这个应用程序的用户可能是使用客户端应用的最终用户,或者是一个区块链网络的管理员。
在大多数情况下,多个组织作为一个联盟聚集在一起形成网络,它们的权限由一组策略决定,这些策略在最初配置网络时由联盟商定。此外,网络策略可以随着时间的推移而变化,这取决于联盟中的组织的协议,我们将在讨论修改策略的概念时发现这一点。
示例网络¶
在开始之前,让我们先展示一下我们最终要做的东西!这是一个表示示例网络最终状态的图表。
不要担心这个看起来很复杂!在学习本主题的过程中,我们将逐步构建网络,以便您了解 R1、R2、R3 和 R4 是如何为网络中提供基础设施并且从中获益。这个基础设施实现了区块链网络,并且它是按照网络中的组织都同意的规则来管理的——比如,谁可以添加新的组织。你会发现应用程序是如何使用账本以及区块链网络所提供的智能合约服务。
network.structure
R1、R2、R3 和 R4 这四个组织已经共同决定,并签署了一份协议,他们将建立和开发一个 Hyperledger Fabric 网络。 R4 被指定为网络初始者,它有权利来设置网络的初始版本,但 R4 不会在网络中去进行任何的业务交易。R1 和 R2 在整体网络中有进行私自的沟通的需求, R2 和 R3 也是。组织 R1 有一个客户端应用程序,可以在通道 C1 中执行业务交易。组织 R2 有一个客户端应用程序,它可以在通道 C1 和 C2 中执行相似的工作。组织 R3 有一个客户端应用程序可以在通道 C2 上实现这一点。节点 P1 维护与 C1 关联的账本 L1 的副本。节点 P2 维护与 C1 关联的账本 L1 和与 C2 关联的账本 L2 的副本。节点 P3 维护与 C2 关联的账本 L2 的副本。网络根据网络配置 NC4 中指定的策略规则进行治理,网络受组织 R1 和 R4 的控制。通道 C1 根据通道配置 CC1 中指定的策略规则进行管理;通道由组织 R1 和 R2 控 制。通道 C2 按照通道配置 CC2 中指定的策略规则进行管理;由组织 R2 和 R3 控制。有一个排序服务 O4 作为 N 的网络管理节点,并使用系统通道。排序服务还支持应用程序通道 C1 C2,来对交易进行排序、加入区块然后分发。这四个组织都有一个证书颁发机构。
创建网络¶
让我们从创建网络的基础开始:
network.creation
当一个排序节点启动后网络就形成了。在我们的示例网络 N 中,包含单个节点 O4 的排序服务根据网络配置 NC4 进行配置,NC4 赋予组织 R4 管理权限。在网络层面,证书颁发机构 CA4 用于向组织 R4 的管理员和网络节点分配身份信息。
我们可以看到,定义网络 N 的第一个东西是排序服务 O4。将排序服务看作是网络的初始管理点是很有用的。如前所述,O4 最初由组织 R4 中的管理员配置和启动,并托管在 R4 中。配置 NC4 包含描述网络初始管理功能集的策略。最初,在网络上只为 R4 授予这样的权限。我们稍后会看到这将会发生变化,但是现在 R4 是网络中唯一的成员。
证书认证机构¶
你还可以看到一个证书认证机构 CA4,它用于向管理员和网络节点颁发证书。CA4 在我们的网络中扮演着关键角色,因为它分发的 X.509 证书可用于标识属于组织 R4 的组件。CA 签发的证书也可以用于签署交易,以表明一个组织对交易结果部署,这是该笔交易可以被接受并记录到账本上的一个前提条件。让我们更详细地研究一下 CA 的这两个方面。
首先,区块链网络的不同组件使用证书相互标识自己来自特定组织。这就是为什么支持区块链网络的 CA 通常不止一个(不同的组织经常使用不同的 CA)。我们将在网络中使用四个 CA,每个组织一个。事实上,CA是非常重要的,所以 Hyperledger Fabric 提供给你一个内置的 CA (称为 Fabric-CA)来帮助您开始工作,尽管在实践中,组织会选择使用它们自己的 CA。
将证书同成员组织进行匹配是通过一个称为成员服务提供者(Membership Service Provider, MSP)的结构来实现的。网络配置 NC4 使用一个命名的 MSP 来标识 CA4 分发的证书的属性,CA4 将证书持有者与组织 R4 关联起来。NC4 接下来会使用这个在规则中的 MSP 名字来分配在网络资源上的特殊权利。这种策略的一个例子是识别 R4 中的管理员,管理员可以向网络添加新的成员组织。我们没有在这些图上显示 MSP,因为他们会很杂乱,但是它们非常重要。
其次,我们将在稍后看到 CA 颁发的证书如何成为交易生成和验证过程的核心的。具体来说,X.509 证书用于客户端应用程序交易提案和智能合约交易响应中,以便对交易进行数字签名。随后,持有账本副本的网络节点在接受账本上的交易之前验证交易签名是否有效。
让我们回顾一下示例区块链网络的基本结构。有一个由证书颁发机构 CA4 定义的一组用户可访问的资源——网络 N,这些用户对网络 N 中的资源拥有一组权限,这些权限由网络配置 NC4 中包含的策略描述。当我们配置并启动排序服务节点 O4 时,所有这些都变为现实。
添加网络管理员¶
NC4 最初配置为只允许 R4 用户在网络上拥有管理权限。在下一阶段,我们将允许组织 R1 用户管理网络。让我们看看网络是如何演变的:
network.admins
组织 R4 更新网络配置,使组织 R1 也成为管理员。在这一点之后,R1 和 R4 对网络配置具有同等的权限。
我们看到添加了一个新的组织 R1 作为管理员,R1 和 R4 现在在网络上拥有相同的权限。我们还可以看到已经添加了证书颁发机构 CA1,它可以用来标识来自 R1 组织的用户。在此之后,R1 和 R4 的用户都可以管理网络。
虽然排序节点 O4 运行在 R4 的基础设施上,但是 R1 拥有同样的管理权限。这意味着 R1 或 R4 可以更新网络配置 NC4,使 R2 组织也可以操作网络。这样的话,虽然 R4 运行着排序服务, R1 在网络中具有全部的管理员权限,但是R2拥有的创建新联盟的权利十分有限。
在这个最简单的模式中,排序服务在网络中是一个独立的节点,就像你在例子中看到的。排序服务通常是多节点的,并且可以配置为在不同的组织中拥有不同的节点。例如,我们可以在 R4 中运行 O4 并将其连接到 O2,这是组织 R1 中的一个单独的排序节点。这样,我们就有了一个多站点、多组织的管理结构。
我们将在本主题的稍后部分进一步讨论排序服务,但是现在只将排序服务看作一个管理节点,它给不同的组织提供了对于网络的管理的权限。
定义联盟¶
虽然现在网络可以由 R1 和 R4 管理,但是这样还是不够的。我们需要做的第一件事是定义一个联盟。这个词表示“具有着共同命运的一个群组”,所以这个是在一个区块链网络中合理地选择出来的一些组织。
让我们来看看联盟是如何定义的:
network.consortium
一个网络管理员定义了一个包含两个成员的联盟 X1,包含组织 R1 和 R2。这个联盟定义存储在网络配置 NC4 中,将在网络开发的下一阶段使用。CA1 和 CA2 分别是这些组织的证书认证机构。
由于 NC4 的配置方式,只有 R1 或 R4 可以创建新的联盟。这个图显示了一个新的联盟 X1 的添加,它将 R1 和 R2 定义为组成它的组织。我们也看到了 CA2 也被添加进来来标识来自于 R2 的用户。请注意,一个联盟可以有任意数量的组织成员,我们仅仅包含了两个组织作为一个最简单的配置。
为什么联盟很重要?我们可以看到,一个联盟定义了网络中的一部分组织,它们都需要彼此进行交易——在本例中是 R1 和 R2。如果组织有一个共同的目标,那么将它们组织在一起是很有意义的,而这正是正在发生的事情。
这个网络虽然最初仅包含一个组织,但现在由一组更大的组织控制。我们可以这样开始,R1、R2 和 R4 共享控制权,但是这种构建使它更容易理解。
现在,我们将使用联盟 X1 创建一个非常重要的 Hyperledger Fabric 区块链的部分——通道。
为联盟创建一个通道¶
让我们创建 Fabric 区块链网络的这个关键部分——通道。通道是一个主要的通信机制,通过它联盟的成员可以彼此通信。网络中可以有多个通道,但是现在,我们先从一个开始。
让我们看看第一个通道是如何添加到网络的:
network.channel
使用联盟定义 X1 为 R1 和 R2 创建了通道 C1。通道由通道配置 CC1 控制,完全独立于网络配置。CC1 由 R1 和 R2 管理,它们对 C1 拥有相同的权限。R4 在 CC1 中没有任何权限。
通道 C1 为联盟 X1 提供了一个私有通信机制。我们可以看到通道 C1 已经连接到排序服务 O4,但是没有附加任何其他内容。在网络开发的下一个阶段,我们将连接组件,比如客户端应用程序和 Peer 节点。但在这一点上,通道代表了未来连接的潜力。
尽管通道 C1 是网络 N 的一部分,但它与网络 N 是有很大区别的。还要注意,组织 R3 和 R4 不在这个通道中——它用于 R1 和 R2 之间的交易处理。在前面的步骤中,我们了解了 R4 如何授予 R1 创建新联盟的权限。值得一提的是,R4 也允许 R1 创建通道!在这个图中,创建通道 C1 的可能是组织 R1 或 R4。同样,请注意,通道可以有任意数量的组织连接到它——我们目前只包含了两个组织,因为它是最简单的配置。
同样,请注意通道 C1 如何具有与网络配置 NC4 完全独立的配置 CC1。CC1 包含控制 R1 和 R2 对通道 C1 拥有的权限的策略,正如我们已经看到的,R3 和 R4 在这个通道中没有权限。R3 和 R4 只有在由 R1 或 R2 添加到通道配置 CC1 中的适当策略时才能与 C1 交互。一个例子是定义谁可以向通道添加新组织。特别需要注意的是, R4 不能将自己添加到通道 C1 中——它只能由 R1 或 R2 授权。
为什么通道如此重要?通道非常有用,因为它提供了一个联盟成员之间进行私有沟通和私有数据的机制。通道提供区别于其他通道和网络的隐私。在这方面,Hyperledger Fabric 非常强大,因为它允许组织共享基础设施,同时又保持私有。这里没有矛盾——网络中的不同联盟需要适当共享不同的信息和流程,而通道提供了一种有效的机制。通道提供高效的基础设施共享,同时维护数据和通信隐私。
我们还可以看到,一旦创建了通道,它会真正地代表了“自由于网络”。从现在开始 和未来,只有在通道配置中显式指定的组织才能控制它。同样,此后对网络配置 NC4 的任何更新都不会对通道配置 CC1 产生直接影响;例如,如果更改了联盟定义 X1,则不会影响通道 C1 的成员。因此,通道是有用的,因为它们允许组成通道的组织之间进行私人通信。此外,通道中的数据与网络的其他部分完全隔离,包括其他通道。
另外,还定义了一个特殊的系统通道供排序服务使用。它的运行方式与常规通道完全相同,因此常规通道有时称为应用程序通道。我们通常不需要担心这个通道,但是我们将在本主题的后面对此进行更多的讨论。
节点和账本¶
现在让我们开始使用通道将区块链网络和组织组件连接在一起。在网络开发的下一个阶段,我们能够看到我们的网络 N 又新增了两个组件,即节点 P1 和账本实例 L1。
network.peersledger
节点 P1 已加入通道 C1。P1 物理上保存着账本 L1 的副本。P1 和 O4 可以使用通道 C1 进行通信。
节点是保存区块链账本副本的网络组件!现在,我们已经开始看到了一些区块链标志性的组件了!P1 在网络中的目的单纯地是存放 L1 账本的一个副本供其他人访问。我们可以将 L1 看作物理上托管在 P1 上,但是逻辑上托管在通道 C1 上。当我们向通道添加更多的节点时,我们会更清楚地看到这个想法。
P1 配置的一个关键部分是由 CA1 发布的 X.509 身份信息,它将 P1 与组织 R1 关联起来。一旦 P1 启动,它就可以使用排序节点 O4 连接通道 C1。当 O4 接收到这个连接请求时,它使用通道配置 CC1 来确定 P1 在这个通道上的权限。例如,CC1 确定 P1 是否可以读写账本 L1 的信息。
注意节点是如何由拥有它们的组织连接到通道的,尽管我们只添加了一个 Peer 节点,但是我们将会看到网络中的多个通道上如何具有多个 Peer 节点的。稍后我们将看到 Peer 节点可以扮演的不同角色。
应用程序和智能合约¶
现在通道 C1 拥有了一个账本,我们可以连接客户端应用来使用由 Peer 节点提供的服务了。
注意网络是如何变化的:
network.appsmartcontract
一个智能合约 S5 已经安装到 P1 上。组织 R1 中的客户端应用程序 A1 可以使用 S5 通过 peer 节点 P1 访问账本。A1、P1 和 O4 都加入了通道 C1,即它们都可以使用该通道提供的通信设施。
在网络开发的下一个阶段,我们可以看到客户端应用程序 A1 可以使用通道 C1 连接到特定的网络资源——在这种情况下,A1 可以连接到 Peer 节点 P1 和排序节点 O4。同样,看一下通道对于网络和组织组件之间的通信是如何起中心作用的。就像 Peer 节点和排序节点一样,一个客户端应用也会有一个使它和一个组织相关联的身份信息。在我们的示例中,客户端应用程序 A1 与组织 R1 相关联;虽然它在 Fabric 区块链网络之外,但是它通过通道 C1 与之连接。
现在我们能够清楚地看到 A1 能够通过 P1 直接访问账本 L1,但是事实上,所有的访问都是通过一个名为智能合约链码的特殊程序 S5 来管理的。可以认为 S5 定义了对账本的所有通用访问模式;S5 定义了一组方法,通过这些方法可以查询或更新账本 L1。简而言之,客户端应用程序 A1 必须通过智能合约 S5 才能访问账本L1!
每个组织中的应用程序开发人员都可以创建智能合约链码,以实现联盟成员共享的业务流程。智能合约用于帮助生成交易,这些交易随后可以分发到网络中的每个节点。我们稍后会讨论这个想法;当网络更大时,就更容易理解了。现在,需要理解的重要事情是,要达到这一点,必须对智能合约执行两个操作;它必须先安装,然后实例化。
安装一个智能合约¶
在一个智能合约 S5 被开发完之后,组织 R1 中的管理员必须将其安装到 Peer 节点 P1 上。这是一个简单的操作。安装完成后,P1 对 S5 有了充分的了解。具体来说, P1 可以看到 S5 的实现逻辑(一种程序代码),它可用该程序代码来访问账本 L1 。我们将此与 S5 接口进行对比, S5 接口只描述 S5 的输入和输出,而不考虑它的实现。
当一个组织在一个通道中有多个 Peer 节点时,它可以选择哪个节点可以安装智能合约;它不需要在每个节点上都安装智能合约。
实例化一个智能合约¶
但是,仅仅为 P1 安装了 S5,其他连接到通道 C1 的组件并不知道它;它必须首先在通道 C1 上实例化。在我们的示例中,只有一个 Peer 节点 P1,组织 R1 中的管理员必须使用 P1 在通道 C1 上实例化 S5。实例化后,通道 C1 上的每个组件都知道存在 S5;在我们的示例中,这意味着 S5 现在可以由客户端应用程序 A1 调用了!
注意,尽管通道上的每个组件现在都可以访问 S5,但是它们不能看到它的程序逻辑。这对安装了它的节点仍然是私有的,在我们的例子中,这意味着 P1。从概念上讲,这意味着实例化的是智能合约接口,而不是安装的智能合约实现。为了强调这一观念,安装智能合约显示了我们如何认为它是物理上托管在 Peer 节点上的,而实例化智能合约则显示了我们如何从逻辑上考虑它是由通道托管的。
背书策略¶
实例化时提供的最重要的附加信息是背书策略。它描述了哪些组织必须批准交易,然后才会被其他组织接受到他们的账本上。在我们的示例网络中,只有在 R1 或 R2 背书的情况下,交易才能被接受并存储到账本 L1 上。
实例化行为将背书策略放置在通道配置 CC1 中;它使通道的任何成员都可以访问它。您可以在交易流程主题中阅读更多关于背书策略的信息。
调用一个智能合约¶
一旦在 Peer 节点上安装了智能合约并在通道上实例化了它,客户端应用程序就可以调用它了。客户端应用程序通过向智能合约背书策略指定的组织所属的节点发送交易提案来实现这一点。交易提议作为智能合约的输入,智能合约使用它生成一个经过背书的交易响应,该响应由 Peer 节点返回给客户端应用程序。
这些交易响应与交易提案打包在一起,形成一个完整背书的交易,他们会被分发到整个网络。稍后我们将更详细地讨论这个问题,现在了解应用程序如何调用智能合约来生成经过背书的交易就足够了。
在网络开发的这个阶段,我们可以看到组织 R1 完全参与了网络。它的应用程序从 A1 开始,通过智能合约 S5 访问账本 L1,生成将由 R1 背书的交易,最后会被接受并添加到账本中,因为这满足了背书策略。
网络设置完成¶
回想一下,我们的目标是为联盟 X1(由组织 R1 和 R2构成)创建一个通道。网络开发的下一阶段将看到组织 R2 将它的基础设施添加到网络中。
让我们看看网络是如何演化的:
network.grow
网络是通过增加 R2 组织的基础设施而发展起来的。具体地说, R2 添加了 Peer 节点 P2,它会存有账本 L1 的一个副本和链码 S5。P2 也加入了通道 C1,应用程序 A2 也是如此。A2 和 P2 是使用 CA2 的证书识别的。所有这些意味着应用程序 A1 和 A2 都可以使用 Peer 节点 P1 或 P2 在 C1 上调用 S5。
我们可以看到组织 R2 在通道 C1 上添加了一个 peer 节点 P2。P2 还包含账本 L1 和智能合约 S5 的副本。我们可以看到 R2 还添加了客户端应用程序 A2,它可以通过通道 C1 连接到网络。为了实现这一点,R2 组织中的管理员创建了 peer 节点 P2 并将其连接到通道 C1,与 R1 中的管理员的方法相同。
我们创建了我们的第一个可运行的网络!在网络开发的这个阶段,我们有一个通道,组织 R1 和 R2 可以在这个通道中彼此交易。具体来说,这意味着应用程序 A1 和 A2 可以使用通道 C1 上的智能合约 S5 和账本 L1 生成交易。
生成并接受交易¶
相比较于经常会存有账本副本的 Peer 节点,我们能够看到两种类型的 Peer 节点,一类是存储智能合约而另一类则不存。在我们的网络中,每个 Peer 节点都会存储智能合约的副本,但是在更大的网络中,将有更多的 Peer 节点不存储智能合约的副本。Peer 节点运行安装在它上边的智能合约,但是它可以通过连接到通道来了解智能合约的接口。
您不应该认为没有安装智能合约的 Peer 节点在某种程度上是较差的。更重要的是,拥有智能合约的 Peer 节点具有一种特殊的能力——帮助生成交易。注意,所有 Peer 节点都可以在其账本 L1 副本上验证并随后接受或拒绝交易。然而,只有安装了智能合约的 Peer 节点才能参与交易背书过程,而交易背书是生成有效交易的核心。
在这里,我们不需要担心交易生成、分发和接受的具体细节——只要了解我们有一个区块链网络,其中的组织 R1 和 R2能够共享由账本记录的交易信息和流程就够了。我们将在其他章节中中学习更多关于交易、账本、智能合约的知识。
节点类型¶
在 Hyperledger Fabric 中,虽然所有 Peer 节点都是相同的,但它们可以根据网络的配置方式承担多个角色。现在,我们对典型的网络拓扑结构有了足够的了解,可以描述这些角色了。
*记账节点。在一个通道中的每个 Peer 节点都是一个记账节点。他们会接收生成的交易的区块,在这些区块被 Peer 节点验证之后会提交到它维护的账本副本中。
*背书节点。每一个带有一个智能合约的 peer 节点都可以作为一个背书节点。然而,想要成为一个真正的背书节点,在 Peer 节点上的智能合约必须要被一个客户端应用使用,来生成包含数字签名的响应。背书节点这个术语就是对这个事实的真实参考。
对于一个智能合约的一个背书策略明确了在交易能够被接受并且记录到记账节点的账本副本之前,哪些组织的 节点需要为交易提供数字签名。
这是 Peer 节点的两种主要类型,Peer 节点还可以扮演另外两个角色:
*领导节点。当一个组织在一个通道中具有多个节点的时候,会有一个领导节点,它是一个负责将交易从排序节点分发到该组织中其他记账节点的节点。一个节点可以选择参与静态的或者动态的领导节点的选择。
这个是很有用的,从管理者的角度来考虑的话会有两套节点:一套是具有静态主导节点选择的,另一套是带有动态的领导节点选择的。对于静态的,0个或者多个节点可以被配置为领导节点。对于动态的,一个节点会被选举成为领导节点。另外,在动态选择领导节点的过程中,如果一个领导节点出错了,那么剩下的节点将会重新选举一个领导节点。
这意味着一个组织的节点可以有一个或者多个领导节点连接到排序服务。这个对于一个处理大量的交易的大型网络有助于提升弹性以及可扩展性。
*锚节点。如果一个节点需要同另外一个组织的一个节点进行通信的话,那么它可以使用对方组织的通道配置中定义的锚节点。一个组织可以拥有0个或者多个锚节点,并且一个锚节点能够帮助很多不同的跨组织间的通信场景。
需要注意的是,节点可以同时是提交节点、背书节点、领导节点和锚节点!只有锚节点是可选的,但总是会有一个领导节点、至少一个背书节点和至少一个提交节点。
安装但没有实例化¶
与组织 R1 类似,组织 R2 必须将智能合约 S5 安装到其节点 P2 上。这很明显——如果应用程序 A1 或 A2 希望在节点 P2 上使用 S5 来生成交易,那么它必须先存在;安装是实现此目的的机制。此时,节点 P2 拥有智能合约和账本的物理副本;与 P1 一样,它可以在账本 L1 的副本上生成和接受交易。
然而,与组织 R1 相反,组织 R2 不需要在通道 C1 上实例化智能合约 S5。这是因为组织 R1 已经在通道上实例化了 S5。实例化只需要发生一次;随后加入通道的任何节点都知道通道可以使用智能合约 S5。这一事实反映了账本 L1 和智能合约确实以物理方式存在于节点上,并以逻辑方式存在于通道上;R2 只是向网络添加了 L1 和 S5 的另一个物理实例。
在我们的网络中,我们可以看到通道 C1 连接两个客户端应用程序、两个 Peer 节点和一个排序服务。由于只有一个通道,所以这些组件只与一个逻辑账本交互。节点 P1 和 P2 具有与账本 L1 相同的副本。智能合约 S5 的副本通常使用相同的编程语言以相同的方式实现,但如果没有,它们必须在语义上等价。
我们可以看到,谨慎地向网络添加节点可以帮助支持增加的吞吐量、稳定性和弹性。例如,网络中更多的节点将允许更多的应用程序连接到它;组织中的多个节点将在计划内或计划外停机的情况下提供额外的弹性。
这一切都意味着,可以配置支持各种操作目标的复杂拓扑——网络的大小没有理论上的限制。此外,单个组织中的节点高效地发现和彼此通信的技术机制——gossip 协议——将容纳大量节点,以支持此类拓扑。
谨慎地使用网络和通道策略,甚至可以对大型网络进行良好的治理。组织可以自由地向网络添加节点,只要它们符合网络商定的策略。网络及通道的策略在对于描绘一个去中心化网络中的自主和受管控之间创建了平衡。
简化视觉词汇表¶
现在我们要简化表示示例区块链网络的可视化词汇表。随着网络规模的增长,最初用来帮助我们理解通道的线路将变得很麻烦。想象一下,如果我们添加另一个节点或客户端应用程序或另一个通道,我们的图表将会多么复杂?
这就是我们马上要做的,在我们做之前,让我们先简化一下视觉词汇表。下面是我们目前开发的网络的一个简化表示:
network.vocabulary
图中显示了网络 N 中与通道 C1 相关的事实如下:客户端应用程序 A1 和 A2 可以使用通道 C1 与节点 P1 和 P2 通信,以及排序节点 O4。节点 P1 和 P2 可以使用通道 C1 的通信服务。排序服务 O4 可以使用通道 C1 的通信服务。通道配置 CC1 适用于通道 C1。
注意,通过用连接点替换通道线,简化了网络图,连接点显示为包含通道号的蓝色圆圈。没有任何信息丢失。这种表示更具有可伸缩性,因为它消除了交叉行。这使我们能够更清晰地表示更大的网络。通过关注组件和通道之间的连接点,而不是通道本身,我们实现了这种简化。
添加另外一个联盟定义¶
在网络开发的下一阶段,我们将介绍组织 R3。我们要给组织 R2 和 R3 一个单独的应用通道让它们可以互相进行交易。这个应用程序通道将完全独立于前面定义的通道,因此 R2 和 R3 交易可以对它们保持私有。
让我们回到网络层,为 R2 和 R3 定义一个新的联盟 X2:
network.consortium2
来自组织 R1 或 R4 的网络管理员添加了一个新的联盟定义 X2,其中包括组织 R2 和 R3。这将用于为 X2 定义一个新通道。
注意,网络现在定义了两个联盟:X1 表示组织 R1 和 R2, X2 表示组织 R2 和 R3。引入联盟 X2 是为了能够为 R2 和 R3 创建一个新的通道。
新通道只能由网络配置策略 NC4 中指定的具有适当权限的组织创建,即 R1 或 R4。这是一个策略的例子,它将能够在网络级别管理资源的组织与能够在通道级别管理资源的组织分开。看到这些策略的作用,有助于我们理解为什么 Hyperleder Fabric 具有复杂的分层策略结构。
实际上,联盟定义 X2 被添加到网络配置 NC4 中。我们将在文档的其他部分讨论此操作的确切机制。
添加一个新通道¶
现在让我们使用这个新的联盟定义 X2 来创建一个新的通道 C2。为了帮助你加深对更简单的通道符号的理解,我们使用了两种视觉样式——通道 C1 用蓝色的圆形端点表示,而通道 C2 用红色连接线表示:
network.channel2
使用联盟定义 X2 为 R2 和 R3 创建了一个新的通道 C2。通道有一个通道配置 CC2,完全独立于网络配置 NC4 和通道配置 CC1。通道 C2 由 R2 和 R3 管理,R2 和 R3 对 CC2 中的策略定义的 C2 拥有相同的权限。R1 和 R4 在 CC2 中没有定义任何权限。
通道 C2 为联盟 X2 提供了一种私有通信机制。同样,请注意组织如何在一个联盟中联合起来就形成了什么形式的通道。通道配置 CC2 现在包含了管理通道资源的策略,通道 C2 的管理权被分配给组织 R2 和 R3。它完全由 R2 和 R3 管理; R1 和 R4 在 C2 通道没有权力。例如,通道配置 CC2 可以被更新,以添加支持网络增长的组织,但这只能由 R2 或 R3 完成。
要注意的是通道配置 CC1 和 CC2 是如何彼此完全分离的,以及如何完全独立于网络配置 NC4。我们再次看到了Hyperledger Fabric 网络的去中心化本质,一旦创建了通道 C2,它就由组织 R2 和 R3 独立于其他网络元素进行管理。通道策略始终保持彼此独立,并且只能由授权在通道中这样做的组织更改。
随着网络和通道的发展,网络和通道的配置也将发生变化。有一个能够以可控的形式实现这些的流程——引入能够获得对于这些配置的改动的配置交易。每个配置更改都会生成一个新的配置区块交易,在本主题的后面,我们将看到如何验证和接受这些区块来更新网络和通道配置。
网络和通道配置¶
在我们的示例网络中,我们看到了网络和通道配置的重要性。这些配置非常重要,因为它们封装了网络成员同意的策略,这些策略为控制对网络资源的访问提供了一个共享的参考。网络和通道配置还包含关于网络和通道组成的事实,例如联盟及其组织的名称。
例如,当使用排序服务节点 O4 首次形成网络时,其行为由网络配置 NC4 控制。NC4 的初始配置只包含允许组织 R4 管理网络资源的策略。NC4 随后被更新,以允许 R1 管理网络资源。一旦进行了此更改,连接到 O4 的任何组织 R1 或 R4 的管理员都将拥有网络管理权限,因为这是网络配置 NC4 中的策略所允许的。在内部,排序服务中的每个节点记录网络配置中的每个通道,以便在网络级别创建每个通道的记录。
这意味着,尽管排序服务节点 O4 是创建联盟 X1 和 X2 以及通道 C1 和 C2 的参与者,但是网络的智慧包含在 O4 遵守的网络配置 NC4 中。只要 O4 表现得像一个好的参与者,并且在处理网络资源时正确地实现 NC4 中定义的策略,我们的网络就会像所有组织已经同意的那样运行。在许多方面, NC4 可以被认为比 O4 更重要,因为它最终控制网络访问。
与节点同样的概念也可以应用到通道配置。在我们的网络中, P1 和 P2 也是很好的参与者。当节点 P1 和 P2 与客户端应用程序 A1 或 A2 交互时,它们各自使用通道配置 CC1 中定义的策略来控制对通道 C1 资源的访问。
例如,如果 A1 想访问节点 P1 或 P2 上的智能合约链码 S5,每个节点使用其 CC1 的副本来确定 A1 可以执行的操作。例如, A1 可以根据 CC1 中定义的策略从账本 L1 中读取或写入数据。稍后我们将看到通道及其通道配置 CC2 中的角色的相同模式。同样,我们可以看到,虽然节点和应用程序是网络中的关键参与者,但是它们在通道中的行为更多地是由通道配置策略决定的,而不是其他因素。
最后,了解网络和通道配置是如何在物理上实现的是很有帮助的。我们可以看到网络和通道的配置在逻辑上是单一的——网络有一个,每个通道也有一个。这是很重要的,访问网络或通道的每个组件,对授予不同组织的权限必须共享理解。
即使存在逻辑上的单一配置,但它实际上被构成网络或通道的每个节点复制并保持一致。例如,在我们的网络节点 P1 和 P2 都有通道配置 CC1 的副本,当网络完全完成时,节点 P2 和 P3 都有通道配置 CC2 的副本。类似地,排序服务节点 O4 具有网络配置的副本,但是在多节点配置中,每个排序服务节点都有自己的网络配置副本。
网络和通道配置在使用相同区块链技术上都保持一致,无论用于用户交易,还是用于配置交易。要更改网络或通道配置,管理员必须提交一个配置交易来更改网络或通道配置。它必须由在适当策略中标识为负责配置更改的组织签署。这个策略称为 mod_policy*,我们稍后将讨论它。
事实上,排序服务节点操作一个小型区块链,通过我们前面提到的系统通道连接。使用系统通道排序服务节点分发网络配置交易。这些交易用于在每个排序服务节点上协同维护网络配置的一致副本。以类似的方式,应用程序通道中的 Peer 节点可以分发通道配置交易。同样,这些交易用于在每个 Peer 节点上维护通道配置的一致副本。
物理上分布而逻辑上单一,这种对象之间的平衡是 Hyperledger Fabric 中的一种常见模式。例如,像网络配置这种对象,它逻辑上是单一的,但最终会在一组排序服务节点之间进行物理复制。我们还可以在通道配置、账本和某种程度上的智能合约中看到它,这些合约安装在多个位置,但是它们的接口逻辑上存在于通道级别。您可以在 Hyperledger Fabric 中多次看到这种模式,它使 Hyperledger Fabric 能够同时去中心化和可管理。
添加另一个节点¶
既然组织 R3 能够完全参与通道 C2,让我们将其基础设施组件添加到通道中。我们不是一次只做一个组件,而是同时添加一个节点、它的本地账本副本、一个智能合约和一个客户端应用程序!
让我们看看添加了组织 R3 组件的网络:
network.peer2
图中显示了网络 N 中通道 C1 和 C2 的相关事实如下:客户端应用程序 A1 和 A2 可以使用通道 C1 与节点 P1 和 P2 通信,使用排序服务 O4;客户端应用程序 A3 可以使用通道 C2 与节点 P3 通信,并使用排序服务 O4。排序服务 O4 可以使用通道 C1 和 C2 的通信服务。通道配置 CC1 适用于通道 C1, CC2 适用于通道 C2。
首先,请注意,由于节点 P3 连接到通道 C2,它与使用通道 C1 的节点具有不同的账本 L2。账本 L2 的有效范围是通道 C2。账本 L1 是完全独立的;它的作用域是通道 C1。这是有意义的——通道 C2 的目的是在联盟 X2 的成员之间提供私有通信,而账本 L2 是他们交易的私有存储。
类似地,安装在节点 P3 上并在通道 C2 上实例化的智能合约 S6用于提供对账本 L2 的受控访问。应用程序 A3 现在可以使用通道 C2 调用智能合约 S6 提供的服务,来生成可以被网络中的每个账本 L2 副本接受的交易。
此时,我们有一个单独的网络,其中定义了两个完全独立的通道。这些通道为组织之间的交易提供了独立管理的设施。这就是工作中的去中心化;我们在控制和自主之间取得了平衡。这是通过应用于由不同组织控制和影响的通道的策略来实现的。
把一个节点添加到多个通道中¶
在网络开发的最后阶段,让我们将重点放在组织 R2 上。我们可以利用 R2 是联盟 X1 和 X2 的成员这一事实,将其加入多个通道:
network.multichannel
图中显示了网络 N 中通道 C1 和通道 C2 的相关事实如下:客户端应用程序 A1 可以使用通道 C1 与节点 P1 和 P2通信,还有排序服务O4;客户端应用程序 A2 可以使用通道 C1 与节点 P1 和 P2 通信,通道 C2 与节点 P2 和 P3 通信,以及排序服务 O4;客户端应用程序 A3 可以使用通道 C2 与节点 P3 和 P2 通信,并使用排序服务O4。排序服务 O4 可以使用通道 C1 和 C2 的通信服务。通道配置 CC1 适用于通道 C1, CC2 适用于通道 C2。
我们可以看到 R2 是网络中的一个特殊组织,因为它是两个应用程序通道中唯一的成员组织!它可以在通道 C1 上与组织 R1 进行交易,同时也可以在不同的通道 C2 上与组织 R3 进行交易。
请注意节点 P2 如何为通道 C1 安装了智能合约 S5,为通道 C2 安装了智能合约 S6。节点 P2 同时是两个通道的成员,通过不同的智能合约为不同的账本服务。
这是一个非常强大的概念——通道既提供了组织分离的机制,也提供了组织间协作的机制。一直以来,这个基础设施都是由一组独立的组织提供并在它们之间共享的。
同样重要的是,节点 P2 的行为控制非常不同,这取决于它交易所在的通道。具体来说,通道配置 CC1 中包含的策略规定了 P2 在通道 C1 中进行交易时可用的操作,而通道配置 CC2 中的策略控制了 P2 在通道 C2 中的行为。
这是我们所希望的—— R2 和 R1 同意通道 C1 的规则,而 R2 和 R3 同意通道 C2 的规则。这些规则是在各自的通道策略中适用——它们可以而且必须被通道中的每个组件使用,以强制执行正确的行为。
类似地,我们可以看到客户端应用程序 A2 现在能够在通道 C1 和 C2 上进行交易。同样,它也会按照在相关的通道配置中的策略来进行管理。另外,请注意客户端应用程序 A2 和节点 P2 使用的是混合的可视化词汇表——包括行和连接。可以看到它们是等价的;它们是视觉上的同义词。
排序服务¶
细心的读者可能会注意到,排序服务节点似乎是一个集中的组件;它最初用于创建网络,并连接到网络中的每个通道。尽管我们将 R1 和 R4 添加到控制排序节点的网络配置策略 NC4 中,该节点仍然运行在 R4 的基础设施上。在一个去中心化的世界里,这看起来是错误的!
别担心!我们的示例网络展示了最简单的排序服务配置,以帮助您理解网络管理点的概念。事实上,排序服务本身也可以完全去中心化!我们在前面提到,排序服务可以由不同组织拥有的许多单独节点组成,所以让我们看看如何在示例网络中实现这一点。
让我们来看看一个更真实的排序服务节点配置:
network.finalnetwork2
一个多组织排序服务。排序服务包括排序服务节点 O1 和 O4。 O1 由组织 R1 提供,节点 O4 由组织 R4 提供。网络配置 NC4 为来自组织 R1 和 R4 的参与者定义了网络资源权限。
我们可以看到这个排序服务完全去中心化了——它在组织 R1 中运行,也在组织 R4 中运行。网络配置策略 NC4 允许 R1 和 R4 对网络资源拥有相同的权限。来自组织 R1 和 R4 的客户端应用程序和节点可以通过连接到节点 O1 或节点 O4 来管理网络资源,因为这两个节点的行为方式相同,这是由网络配置 NC4 中的策略定义的。在实践中,来自特定组织的参与者倾向于使用由其母组织提供的基础设施,但情况并非总是如此。
去中心化的交易分发¶
除了作为网络的管理点之外,排序服务还提供了另一个关键功能——交易的分发点。排序服务是一个组件,它从应用程序中收集经过背书的交易并将其排序到交易区块中,然后将这些交易区块分发到通道中的每个 Peer 节点。在每一个记账节点,交易都会被记录下来,不管交易是有效的还是无效的,它们的本地账本副本也会被适当地更新。
注意排序服务节点 O4 为通道 C1 执行的角色与为网络 N 执行的角色非常不同。当在通道级别执行操作时,O4 的角色是收集交易务并在通道 C1 中分发区块。它根据通道配置 CC1 中定义的策略执行此操作。相反,当在网络级别执行操作时,O4 的角色是根据网络配置 NC4 中定义的策略为网络资源提供管理点。再次注意,这些角色是如何分别由通道和网络配置中的不同策略定义的。这将增强基于声明策略的配置在 Hyperledger Fabric 中的重要性。策略定义并用于控制联盟中每个成员的一致行为。
我们可以看到,与 Hyperledger Fabric中的其他组件一样,排序服务也是一个完全去中心的组件。无论是作为网络管理点,还是作为通道中区块的分发器,其节点都可以根据需要分布到网络中的多个组织中。
修改策略¶
通过对示例网络的探索,我们已经看到了控制系统中参与者行为的策略的重要性。我们只讨论了一些可用的策略,但是可以声明性地定义许多策略来控制行为的各个方面。这些单独的策略将在文档的其他部分讨论。
最重要的是,Hyperledger Fabric 提供了一个独特的功能强大的策略,允许网络和通道管理员管理策略的变更!其基本理念是,无论策略变化发生在组织内部或组织之间,还是由外部监管机构强制实施,策略变化都是持续的。例如,新组织可以加入通道,或者现有组织的权限可以增加或减少。让我们进一步研究一下更改策略是如何在 Hyperledger Fabric 中实现的。
理解的关键点是策略变更是由策略本身内部的策略管理的。修改策略,简称 mod_policy,是管理更改的网络或通道配置中的首类策略。让我们举两个简单的例子,说明我们如何已经使用了 mod_policy 管理网络中的变化!
第一个例子是网络最初建立的时候。此时,只有组织 R4 被允许管理网络。实际上,这是通过使 R4 成为网络配置 NC4 中定义的唯一具有网络资源权限的组织来实现的。此外,NC4 的 mod_policy 只提到了组织 R4,说明只允许 R4 更改此配置。
然后,我们将网络 N 改进为允许组织 R1 管理网络。R4 通过在通道创建和联盟创建策略中添加 R1 实现了这一点。由于这个更改, R1 能够定义联盟 X1 和 X2,并创建通道 C1 和 C2。 R1 在网络配置中对通道和联盟策略具有同等的管理权限。
然而,R4 可以通过网络配置为 R1 授予更多的权限!R4 可以将 R1 添加到 mod_policy 中,这样 R1 也可以管理网络策略的更改。
第二种功能比第一种功能强大得多,因为现在 R1 完全控制了网络配置 NC4!这意味着 R1 原则上可以从网络中删除 R4 的管理权限。实际上,R4 将配置 mod_policy,以便 R4 也需要批准更改,或者 mod_policy 中的所有组织都必须批准更改。mod_policy 具有很大的灵活性,可以使其尽可能复杂,以支持所需的任何更改过程。
这就是 mod_policy 的作用——它允许将基本配置优雅地演化为复杂的配置。所有这一切都是在所有有关组织的同意下发生的。mod_policy 的行为与网络或通道配置中的其他策略相同;它定义了一组允许更改 mod_policy 本身的组织。
在本小节中,我们只讨论了策略和 mod_policy 的功能的皮毛。在策略主题中会详细讨论这个问题,但是现在让我们回到已经完成的网络!”
网络完成了¶
让我们用一致的视觉词汇来概括一下我们的网络。我们稍微重新组织了它使用我们更紧凑的视觉语法,因为它更好地适应更大的拓扑:
network.finalnetwork2
在这个图中,我们看到 Fabric 区块链网络由两个应用程序通道和一个排序通道组成。组织 R1 和 R4 负责排序通道,R1 和 R2 负责蓝色的应用程序通道,R2 和 R3 负责红色的应用程序通道。客户端应用程序 A1 是组织 R1 的一个元素,而 CA1 是它的证书认证机构。注意 R2 组织中的节点 P2 可以使用蓝色和红色应用程序通道的通信设施。每个应用程序通道都有自己的通道配置,在本例中是 CC1 和 CC2。系统通道的通道配置是网络配置(NC4)的一部分。
我们已经完成了构建一个示例 Hyperledger Fabric 区块链网络的概念之旅。我们创建了一个包含两个通道和三个节点、两个智能合约和一个排序服务的四个组织网络。它由四个证书认证机构支持。它为三个客户端应用程序提供账本和智能合约服务,客户端应用程序可以通过这两个通道与它交互。花点时间浏览一下图表中网络的细节,并随时回顾主题以加强您的知识,或者转到更详细的主题。
总结¶
在本主题中,我们了解了不同的组织如何共享它们的基础设施来提供一个集成的 Hyperledger Fabric 区块链网络。我们看到了如何将集体基础设施组织成提供独立管理的私有通信机制的通道。我们也已经看到了如何通过使用来自各自证书认证机构的证书来识别来自不同组织的参与者,比如客户端应用程序、管理员、节点和排序服务。反过来,我们也看到了策略的重要性,它定义了这些组织参与者在网络和通道资源上拥有的一致同意的权限。
身份¶
什么是身份?¶
区块链网络中的不同参与者包括 Peer 节点、排序节点、客户端应用程序、管理员等等。这些参与者都有一个封装在 X.509 数字证书中的数字身份。这些身份非常重要,因为它们决定了参与者在区块链网络中对资源的确切权限和对信息的访问权限。
此外,数字身份还具有一些额外的属性,Fabric 使用这些属性来确定权限,并为身份和相关属性的联合提供了一个特殊的名称—主体。主体与 userid 或 groupid 类似,但是更加灵活,因为它们可以包含参与者身份的广泛属性,比如参与者的组织、组织单元、角色,甚至参与者的特定身份。当我们讨论主体时,它们是决定其权限的属性。
要使身份可验证,它必须来自受信的权威机构。成员服务提供者(MSP)就是Fabric中的受信机构。更具体地说,MSP 是一个为组织定义有效身份管理规则的组件。Fabric 中的默认 MSP 实现使用 X.509 证书作为身份,采用了传统公钥基础设施(PKI)层次模型(稍后将详细介绍 PKI)。
一个简单的场景来解释身份的使用¶
假设你去超市买一些杂货。在收银台,你会看到一个牌子,上面写着只接受 Visa、Mastercard 和 AMEX 卡。如果你想用另一张卡支付——我们称之为 “ImagineCard”——不管这张卡是否真实,你的账户里是否有足够的资金。我们不会接受的。
Scenario
拥有一张有效的信用卡是不够的——它还必须被商店接受!PKI 和 MSP 以相同的方式协同工作——PKI 提供了一个身份列表,MSP 表示这些身份中哪些是参与网络的给定组织的成员。
PKI 证书授权中心和 MSP 提供了类似的功能组合。PKI 就像一个卡提供商——它提供许多不同类型的可验证身份。另一方面,MSP 类似于商店接受的卡片提供者列表,确定哪些身份是商店支付网络的可信成员(参与者)。MSP 将可验证的身份转换为区块链网络的成员。
让我们更详细地研究一下这些概念。
什么是PKI?¶
公钥基础设施(PKI)是一组互联网技术,它在网络中提供安全的通信。将 S 放入 HTTPS 的是 PKI——如果您在 web 浏览器上阅读本文,您可能正在使用 PKI 来确保它来自经过验证的来源。
PKI
公钥基础设施(PKI)的元素。PKI 由证书授权中心组成,这些授权中心向各方(例如服务的用户、服务提供者)颁发数字证书,然后在与环境交换的消息中使用这些证书对自己进行身份验证。CA 的证书撤销列表(CRL)构成不再有效的证书的引用。撤销证书的原因有很多。例如,因为与证书关联的加密私有材料已被公开,证书可能会被吊销。
虽然区块链网络不仅仅是一个通信网络,它还依赖 PKI 标准来确保各个网络参与者之间的安全通信,并确保在区块链上发布的消息得到正确的身份验证。因此,了解 PKI 的基础知识以及为什么 MSP 如此重要是很重要的。
PKI 有四个要素:
- 数字证书
- 公钥和私钥
- 证书授权中心
- 证书撤销列表
让我们快速描述一下这些 PKI 基础知识,如果您想了解更多细节,可以从 Wikipedia 开始。
数字证书¶
数字证书是包含一组与证书持有者相关的属性的文档。最常见的证书类型是符合 X.509 标准的证书,该标准允许在其结构中编码某参与方的身份细节。
例如,密歇根州底特律市 Mitchell Cars 公司制造部门的玛丽莫里斯可能拥有一个 SUBJECT 属性为 C=US, ST=Michigan, L=Detroit, O=Mitchell Cars, OU=Manufacturing, CN=Mary Morris /UID=123456 的数字证书。玛丽的证件类似于她的政府身份证——它提供了有关玛丽的信息,她可以用这些信息来证明有关她的关键事实。在 X.509 证书中还有许多其他属性,但是现在让我们只关注这些属性。
DigitalCertificate
描述一个叫玛丽·莫里斯的参与方的数字证书。玛丽是证书的 SUBJECT ,突出显示的主题文本显示了关于玛丽的关键事实。如您所见,证书还包含更多的信息。最重要的是,玛丽的公钥分布在她的证书中,而她的私钥没有。此签名密钥必须保持私有。
重要的是,玛丽的所有属性都可以使用一种称为密码学的数学技术(字面意思是“秘密写作”)记录下来,这样篡改会使证书失效。密码学允许玛丽向其他人提供她的证书,以证明她的身份,只要另一方信任证书颁发者,即证书颁发机构(certificate Authority, CA)。只要 CA 安全地保存某些加密信息(即它自己的私有签名密钥),任何阅读证书的人都可以确保关于玛丽的信息没有被篡改——它始终具有玛丽莫里斯的那些特定属性。将玛丽的 X.509 证书看作是无法更改的数字身份证。
认证,公钥和私钥¶
身份认证和消息完整性是安全通信中的重要概念。身份认证要求交换消息的各方确信创建特定消息的身份。要使信息具有“完整性”,就意味着信息在传输期间不可能发生过修改。例如,您可能想要搞清楚正与自己通信的到底是真的玛丽还是冒充者。或者,如果玛丽给你发了一条信息,你可能想要确保它在传输过程中没有被任何人篡改过。
传统的身份认证机制依赖于数字签名,顾名思义,就是允许一方对其消息进行数字签名。数字签名还为签名消息的完整性提供了保证。
从技术上讲,数字签名机制要求每一方都持有两个加密连接的密钥:一个公开密钥和一个私有秘钥。公开密钥高度可获取,充当身份验证锚;私有秘钥用于在消息上生成数字签名。数字签名消息的接收方可以通过检查消息中的附加签名在预期发送方的公钥下是否有效来验证所接收消息的来源和完整性。
私钥与其对应公钥之间的独特关系是使安全通信成为可能的密码术。密钥之间独特的数学关系在于私钥可用于对消息生成签名,只有对应的公钥能够、且仅能在相同消息上与之匹配。AuthenticationKeys
在上面的示例中,玛丽使用她的私钥对消息签名。任何使用玛丽的公钥查看此签名消息的人都可以验证该签名。
证书授权¶
如您所见,参与者或节点可通过数字身份的方式参与区块链网络,该数字身份是由系统信任的机构为其颁布的。多数情况下,数字身份(或简称身份)具有密码验证的数字证书形式,符合 X.509 标准,由证书授权中心(CA)颁发。
CA 是互联网安全协议的一个常见部分,您可能听说过一些比较流行的协议:赛门铁克(最初是 Verisign)、GeoTrust、DigiCert、GoDaddy 和 Comodo 等等。
CertificateAuthorities
证书授权中心将证书分发给不同的参与者。这些证书由 CA 进行数字签名,证书把参与者与其公钥绑定在一起(也可以和一整套属性绑定)。因此,如果一方信任 CA (并且知道CA的公钥),那么通过验证参与者证书上的 CA 签名,它就可以信任以下事实:证书中的公钥是该参与者的;该参与者拥有证书中所包含的属性。
证书既不包含参与者的私钥,也不包含 CA 的私钥,因此可以广泛传播。证书可作为验证不同参与者所发送的信息的信任锚。
CA 也有一个证书,可以广泛使用。这样的话,若某参与方想要使用一指定CA颁布的证书,可以通过检查证书只能由相应私钥(CA)的持有者生成来验证该证书。
在区块链设置中,希望与网络交互的每个参与者都需要一个身份。在这种情况下,您可能会说可以使用一个或多个 CA 从数字的角度定义组织的成员。CA为组织的参与者拥有可验证的数字身份奠定了基础。
根 CA,中间 CA 和信任链¶
CA 有两种类型:根 CA 和中间 CA。由于根 CA(赛门铁克、Geotrust等)必须向互联网用户安全颁发数亿个证书,因此将此过程分散到所谓的中间 CA 是有意义的。中间 CA 的证书由根 CA 或其他中间机构颁发,这就为链上所有 CA 颁发的全部证书建立起一条“信任链”。追溯到根 CA 的这种能力不仅使得 CA在保证安全的同时扩展了自身功能——让使用证书的组织在运用中间 CA的时候有足够信心——而且还限制了根 CA 的暴露。根CA一旦遭到破坏,将会危及整个信任链。而如果受到损害的是中间CA,则曝光量会小得多。
ChainOfTrust
在根 CA 和一组中间 CA 之间建立信任链的前提是,每个中间 CA 的证书颁发机构 要么是根 CA 本身,要么是与根CA有信任链关系的其他中间CA。
当涉及到跨多个组织颁发证书时,中间 CA 提供了巨大的灵活性,这在许可区块链系统(如 Fabric)中非常有用。例如,您将看到不同的组织可能使用不同的根 CA,或者使用相同的根 CA 和不同的中间 CA——这完全取决于网络的需要。
Fabric CA¶
因为 CA 非常重要,所以Fabric 内置了一个的CA 组件,支持用户在自己搭建的区块链网络中创建 CA。该组件——称为 Fabric CA,是一个私有根 CA 的提供者,能够管理具有 X.509 证书的 Fabric 参与者的数字身份。因为 Fabric CA 是针对 Fabric 根 CA 需求的一种自定义 CA,所以它本质上不能为浏览器中的常规/自动使用提供 SSL 证书。但是,由于必须使用一些 CA 来管理身份(即使是在测试环境中),因此可以用Fabric CA 来提供和管理证书。使用公共/商业根CA或中间 CA 来提供身份也是可以的,而且是完全合适的。
如果您感兴趣,可以在 CA 文档部分阅读更多关于 Fabric CA 的内容。
证书撤销列表¶
证书撤销列表(Certificate Revocation List, CRL)很容易理解——它就是一个证书的参照列表,有了它,CA 就知道列表上的这些证书是因为这样或那样的原因被撤销了的。回忆一下上文中我们谈到的商店场景,其中CRL 就好比一个被盗信用卡列表。
当第三方想要验证另一方的身份时,它首先会检查颁发其证书的 CA 的 CRL,以确保该证书未被撤销。验证者不是必须要检查 CRL,但是如果他们不检查 ,就将冒着接受被盗身份的风险。
CRL
使用 CRL 检查证书是否依然有效。如果虚假身份者试图将一个已受损的数字证书传递给验证方,可以根据证书颁发CA 的 CRL 检查它,以确保它未被列为不再有效。
注意,被撤销的证书与到期的证书相差甚远。被撤销的证书还没有过期——按其他任何标准衡量,它们都是完全有效的证书。有关 CRL 的更多信息,请单击这里。
现在您已经了解了 PKI 如何通过信任链来提供可验证的身份,下一步就来看如何使用这些身份来代表区块链网络的受信成员。这就是成员服务提供者(MSP)发挥作用的地方——它能识别出区块链网络中给定组织的成员。
要了解更多关于成员身份的信息,请参阅关于 MSP 的概念文档。
成员¶
如果你已经阅读了身份文档,那么你应该已经知道了 PKI 是如何能够通过信任链条来提供可验证身份的。现在我么来看一下如何用这些身份代表区块链网络中的受信任成员。
这就需要成员服务提供者(MSP)来发挥作用了,它能识别出哪些根 CA 和中间 CA 是受到信任来定义某个信任领域的成员的,例如,一个组织。MSP发挥作用的方式要么是将成员的身份进行列表,要么是识别出哪些CA获权为其成员颁发有效证书,要么(最常见的方式)将二者结合使用。
MSP 的能力绝非仅仅罗列谁是网络参与者或通道成员那么简单。MSP 可以识别参与者在 该MSP 代表的组织范围内可能扮演的特定角色(例如,管理员或作为子组织小组的成员),同时MSP还为网络或通道环境内访问权限的定义奠定了基础(例如,通道管理员、读取者、写入者)。
MSP 的配置会(以通道 MSP 的形式)被广播到相应组织成员所参与的全部通道中。除了通道 MSP之外,Peer 节点、排序节点以及客户端也都维护着一个本地 MSP,用来验证通道环境外的成员信息,并定义对某特定组件的权限(比如,谁能够在节点上安装链码)。
此外,MSP 还支持将已撤销的一组身份识别出来(如身份文档中所讨论的),但我们马上要讨论该过程是如何扩展到 MSP的。
稍后我们将继续讨论本地和通道 MSP。现在,让我们看看 MSP 通常做些什么。
将 MSP 映射到组织¶
组织是被管理的成员小组。组织可大可小,可以像一家跨国公司那么大,也可以像一个花店那么小。通常,组织由一个单个MSP来代表。注意,这与我们稍后将讨论的 X.509 证书中定义的组织概念不同。
由于组织和其MSP之间存在排他关系,因此以组织名称来命名MSP是很明智的,这是大多数策略配置中都会采用的约定。例如,组织ORG1
可能想将某个MSP命名为ORG1-MSP
。在有些情况中,一个组织可能需要多个成员小组,比如,需要用通道来在组织间执行完全不同的业务功能时。在这些情况中,拥有多个MSP并且相应地对这些MSP命名是很有道理的,例如,ORG2-MSP-NATIONAL
和ORG2-MSP-GOVERNMENT
反映了NATIONAL
销售通道上ORG2
内与GOVERNMENT
管理通道不同的成员信任根源。
由于组织及其 MSP 之间的排他性关系,所以最好使用组织的名称命名 MSP,您会发现在大多数策略配置中都采用了这种约定。例如,组织 ORG1
可能有一个名为 ORG1-MSP
之类的 MSP。在某些情况下,组织可能需要多个成员组,例如,通道用于在组织之间执行完全不同的业务功能。在这些情况下,有多个 MSP 并相应地命名是有意义的,例如,ORG2-MSP-NATIONAL
和 ORG2-MSP-GOVERNMENT
,这反映了 ORG2
在全国
销售通道中与政府
监管通道中成员的信任根是不同的。
MSP1
一个组织有两种不同的 MSP 配置。第一个配置显示了 MSP 和组织之间的典型关系——单个 MSP 定义了组织的一组成员。在第二种配置中,用不同的 MSP 来表示具有国家、国际和政府关联的不同组织小组。
组织的单位以及 MSP¶
一个组织通常被划分为多个组织单元(OU),每个单元都有一组特定的职责。例如,ORG1
组织可能同时具有 ORG1-MANUFACTURING
和 ORG1-DISTRIBUTION
OU 来反映这些独立的业务线。当 CA 发出 X.509 证书时,证书中的 OU
字段会指明身份所属的业务线。
稍后我们将看到,OU 怎样帮忙控制组织中被认为是区块链网络成员的部分。例如,只有来自 ORG1-MANUFACTURING
OU 的身份才能访问通道,而来自 ORG1-DISTRIBUTION
的不能。
最后,尽管这是对 OU 的轻微误用,但有时它们可以被联盟中的不同组织用来区分彼此。在这种情况下,不同的组织使用相同的根 CA 和中间 CA 作为它们的信任链,但是分配 OU
字段来识别各组织的成员。稍后我们还将看到如何配置 MSP 来实现这一点。
本地和通道 MSP¶
在区块链网络中,MSP 出现在两个位置:
- 在通道配置中(通道 MSP)
- 在参与者节点本地(本地 MSP)
本地 MSP 是为客户端(用户)和节点(Peer 节点和排序节点)定义的。节点本地 MSP 为该节点定义权限(例如,节点管理员是谁)。用户的本地 MSP 允许用户端在其交易中作为通道的成员(例如在链码交易中)或作为系统中特定角色的所有者(例如在配置交易中的某组织管理员)对自己进行身份验证。
每个节点和用户都必须定义一个本地 MSP,因为它定义了谁拥有该级别的管理或参与权(节点管理员不一定是通道管理员,反之亦然)。
相反,通道 MSP 在通道级别定义管理和参与权。每个参与通道的组织都必须为其定义一个 MSP。通道上的 Peer 节点和排序节点将共享通道 MSP,因此能够正确地对通道参与者进行身份验证。这意味着,如果某个组织希望加入通道,则需要在通道配置中加入一个包含该组织成员信任链的 MSP。否则,由该组织身份发起的交易将被拒绝。
本地和通道 MSP 之间的关键区别不在于它们如何工作(它们都将身份转换为角色)而在于它们的作用域。
MSP2
本地和通道 MSP。每个节点的信任域(例如组织)由节点的本地 MSP(例如 ORG1 或 ORG2)定义。通过将组织的 MSP 添加到通道配置中,可以表示该组织加入了通道。例如,此图中的通道由 ORG1 和 ORG2 管理。类似的原则也适用于网络、排序节点和用户,但是为了简单起见,这里没有显示这些原则。
你可能会发现通过查看区块链管理员安装和实例化智能合约时所发生的情况,能帮助你理解如何使用本地和通道 MSP ,如上图所示。
管理员 B
使用身份连接到节点上,该身份是由RCA1
颁发的,存储在本地 MSP中。当 B
试图在节点上安装智能合约时,该节点检查其本地 MSP ORG1-MSP
,以验证 B
的身份确实是 ORG1
的成员。验证成功后才会允许安装。随后,B
希望在通道上实例化智能合约。因为这是一个通道操作,所以通道上的所有组织都必须同意该操作。因此,节点在成功提交此命令之前必须检查该通道的 MSP。(其他事情也必须发生,但现在先专注于上面的事情。)
本地 MSP 只被定义在它们应用到的节点或用户的文件系统上。因此,从物理和逻辑上来说,每个节点或用户只有一个本地 MSP。但是,由于通道 MSP 对通道中的所有节点都可用,因此在逻辑上他们只在通道配置中定义一次。然而,通道 MSP 也在通道中每个节点的文件系统上实例化,并通过共识来保持同步。因此,虽然每个节点的本地文件系统上都有各通道 MSP 的副本,但从逻辑上讲,通道 MSP 驻留在通道或网络上并由通道或网络进行维护。
MSP 层级¶
通道MSP和本地 MSP 之间的区别反映了组织管理其本地资源(如 Peer 节点或排序节点)和通道资源(如账本、智能合约和联盟)的需求,这些资源在通道或网络级别运行。将这些 MSP 看作是不同级别的是有帮助的,其中处于较高级别的 MSP 与网络管理相关,而处于较低级别的 MSP 处理私有资源管理的身份。MSP 在每个管理级别都是强制性的——必须为网络、通道、Peer 节点、排序节点和用户定义它们。
MSP3
MSP 的级别。Peer 节点和排序节点的 MSP 是本地的,而通道(包括网络配置通道)的 MSP 是对该通道的所有参与者共享的。在这个图中,网络配置通道由 ORG1 管理,但是另一个应用程序通道可以由 ORG1 和 ORG2 管理。Peer 节点是 ORG2 的成员,由 ORG2 管理,而 ORG1 管理图中的排序节点。ORG1 信任来自 RCA1 的身份,而 ORG2 信任来自RCA2 的身份。注意,这些是管理员身份,反映了谁可以管理这些组件。所以当 ORG1 管理网络时,ORG2.MSP 也要在网络定义中。
- 网络 MSP :一个网络的配置文件,通过定义参与组织的 MSP 定义了谁是这个网络中的成员,并且定义了这些成员中的哪些被授权来执行相关管理任务 (比如,创建一个通道)。
- 通道 MSP:对于一个通道来说,保持其成员MSP的不同至关重要。通道为一群特定组织提供了一个彼此间私有的通信方式,这些组织又对这个通道进行管理。该通道 MSP 中所解释的通道策略定义了谁有能力参与该通道上的某些操作,比如,添加组织或者实例化链码。注意,管理通道的权限和管理网络配置通道(或任何其他通道)的能力之间没有必然联系。管理权仅存在于被管理的范围内(除非已在规则中明确——查看下边对于
ROLE
属性的讨论)。 - Peer 节点 MSP:这个本地 MSP 是在每个节点的文件系统上定义的,并且每个节点都有一个单独的 MSP 实例。从概念上讲,该MSP和通道 MSP 执行着完全一样的操作,但仅适用于其被定义的节点。在节点上安装一个链码,这个就是使用节点的本地 MSP 来判定谁被授权进行某个操作的例子。
- 排序节点 MSP: 就和Peer 节点 MSP 一样,排序节点的本地 MSP 也是在节点的文件系统上定义的,并且只会应用于这个节点。同时,排序节点也是由某个单独的组织所有,因此具有一个单独的 MSP 用于罗列它所信任的操作者或者节点。
MSP 结构¶
目前为止,你所看到的 MSP 最为重要的元素就是对于根或者中间 CA 的声明,这些 CA 被用来在对应的组织中建立一个参与者或者节点的成员身份。然而这里还有更多的元素同这两个元素一起来辅助成员相关的功能。
MSP4
上面的图显示了本地 MSP 在本地文件系统中的储存方式。尽管通道 MSP 的物理结构不是完全按照这种方式构建的,但是这样考虑它们仍然是一种有用的方式。
正如您所看到的,MSP 有九个元素。在目录结构中考虑这些元素最简单,其中 MSP 名称就是根文件夹名称,而每个子文件夹代表 MSP 配置的不同元素。
让我们深入探讨一下这些文件夹,看看它们为什么重要。
根 CA:该文件夹包含了根CA自主签名的X.509证书的列表,其中的根CA是受MSP代表的组织所信任的。在这个 MSP 文件夹中至少要有一个根 CA X.509 证书。
这是最重要的一个文件夹,因为它指出了所有可用于证明成员属于对应组织的其他证书的来源 CA。
中间 CA:该文件夹包含了受这个组织信任的中间 CA 对应的 X.509 证书列表。其中每个证书的签发方必须是MSP中的某个根CA或中间CA,若是中间CA,则该中间 CA 的证书签发 CA 信任链必须最终能够连上一个受信任的根 CA。
中间 CA 可能代表了组织中不同的一个分支(就像
ORG1
有ORG1-MANUFACTURING
和ORG1-DISTRIBUTION
一样), 也可能代表了这个组织自身(比如当一个商业 CA 被用来管理组织的身份时)。在后边这个情况中,中间 CA 可以被用来代表组织的分支。从这里你或许能看到更多关于 MSP 配置最佳实践的信息。注意,对于一个工作网络来说,也有可能没有任何的中间 CA,这种情况下,这个文件夹就是空的。就像根 CA 文件夹一样,中间CA文件夹定义了可用于证明组织成员身份的证书的签发CA。
组织单元 (OU):组织单元被列在
$FABRIC_CFG_PATH/msp/config.yaml
文件中,包含了组织单元的一个列表,其中的成员被认为是由这个 MSP 所代表的组织的一部分。当你想要把一个组织的成员限定为持有一个其中包含某特定组织单元的身份(由MSP指定的某个CA签发)的成员时,它是很有用的。指定 OU 不是必须的。如果没有列出任何 OU,MSP中的所有身份(由根 CA 和中间 CA 文件夹指出)都会被认为是组织的成员。
管理员:该文件夹包含了一个身份列表,其中的身份为该组织定义了哪些操作者担任管理员。对于标准的 MSP 类型来说,在这个列表中应该有一个或者多个 X509 证书。
值得注意的是,仅凭借某操作者担任管理员这一点并不能说明它可以管理某些特定的资源。对于一个给定身份来说,它在管理系统方面的实际能力是由管理系统资源的相关策略决定的。比如,一个通道策略可能会指明
ORG1-MANUFACTURING
管理员有权利来向通道中添加新的组织,然而ORG1-DISTRIBUTION
管理员却没有这个权利。虽然 X.509 证书具有
ROLE
属性(比如,明确规定一个操作者是一个admin
),但是该属性指的是操作者在其组织内所扮演的角色,而不是在区块链网络上。这一点与OU
属性的目的类似,若已经定义OU
属性,则其指的是操作者在组织中的位置。如果某个通道策略允许来自一个(或某些)组织的任何管理员执行某些通道功能的话,那么
ROLE
属性就能够用来授予该通道级别上的管理权力。这样的话,一个组织层面的角色可以授予一个网络级别的角色。撤销证书:如果一个参与者的身份被撤销,那么该身份的识别信息(不是指身份本身)就会被储存在这个文件夹中。对基于 X.509 的身份来说,这些标识符就是主体密钥标识符 (Subject Key Identifier,SKI) 和权限访问标识符(Authority Access Identifier,AKI)的字符串对,并且无论何时使用 X.509 证书,都会检查这些标识符,以确保证书未被撤销。
虽然这个列表在概念上跟 CA 的证书撤销列表 (CRL) 是一样的,但是它还和从组织中撤销成员有关。这样一来,本地或通道 MSP 的管理员通过广播被撤销证书的发行CA的最新CRL,就可以迅速将这个参与者或者节点从组织中撤销。罗列这一列表并不是必须的,只有在证书要撤销的时候才会用到。
节点身份:这个文件夹包含了节点的身份,比如,与
KeyStore
内容结合使用的加密材料将允许节点在向同通道或同网络上其他参与者发送的信息中验证自己的身份。对基于 X.509 的身份来说, 该文件夹包含了一个 X.509 证书。Peer节点会把这一证书放置到交易提案的响应中,比如,来表明该节点已经为此交易背书,在接下来的验证阶段会根据交易的背书策略来验证这一背书。本地 MSP 中必须拥有”节点身份“文件夹,节点中有且仅有一个X.509 证书。而通道 MSP中不使用该文件夹 。
私钥的
KeyStore
:这个文件夹是为 Peer 节点或者排序节点(或者在客户端的本地 MSP 中) 的本地 MSP 定义的,其中包含了节点的签名秘钥。这个秘钥与节点身份文件夹里的节点身份能以密码方式匹配,可用来对数据进行签名,比如在背书阶段对一个交易提案的响应进行签名。本地 MSP 必须有这个文件夹,而且该文件夹必须包含且仅包含一个私钥。很显然,这个文件夹的访问权限必须限定在对这个节点有管理权限的用户的身份。
因为通道 MSP 的目标只是提供身份验证功能,而不是签名的能力,所以通道 MSP 的配置中不包含这个文件夹。
TLS 根 CA:该文件夹包含了根CA的自主签名X.509证书的列表,其中的根CA是受该组织信任来进行TLS通信的。一个 TLS 通信的例子就是,Peer 节点为接受到更新过的账本, 需要连接到一个排序节点,这时就会发生TLS通信。
MSP TLS 信息和网络内部的节点(Peer 节点和排序节点)有关联,换句话说,它与使用该网络的应用程序和管理员无关。
这个文件夹中必须有至少一个 TLS 根 CA X.509 证书。
TLS 中间 CA:这个文件夹包含了在通信时这个 MSP 所代表的的组织所信任的中间 CA 证书列表。当商业 CA 被用于一个组织的 TLS 证书时,该文件夹特别有用。跟成员的中间 CA 类似,指定中间 TLS CA 是可选项。
关于 TLS 的更多信息,点击这里。
如果你读过这个文档以及我们关于身份的文档,你应该对身份和成员在 Hyperledger Fabric 中的作用有了很好的理解。您了解了如何使用 PKI 和 MSP 来识别在区块链网络中协作的参与者。您学习了证书、公钥、私钥和信任根的工作原理,以及 MSP 的物理和逻辑结构。
节点¶
区块链网络主要由一组 Peer 节点(或者简称节点)组成。节点托管着账本和智能合约,因此节点是网络的基本成分。回想一下,账本记录着由智能合约( Hyperledger Fabric 的智能合约储存在链码中*,稍后将详细介绍)生成的所有交易,并且不可篡改。智能合约和账本分别用于封装网络中的共享进程和共享信息。因此节点是了解 Fabric 网络的良好开端。
区块链网络的其他元素也很重要:账本和智能合约、排序节点、策略、通道、应用程序、组织、身份和成员,您可以在各元素的专属章节中阅读更多相关信息。本节主要讨论节点及其与 Fabric 网络中其他元素的关系。
Peer1
区块链网络由节点组成,每个节点都可以保存账本副本和智能合约副本。在本例中,网络 N 由节点 P1、P2 和 P3 组成,它们各自维护分布式账本 L1 的副本。P1、P2 和 P3 使用相同的链码 S1 来访问各自的分布式账本L1的副本。
节点可以被创建、启动、停止、重新配置甚至被删除。节点公开了一组 API(Application Programming Interface 应用程序编码端口),以供管理员和应用程序与 API 提供的服务进行交互。下文中将详细谈到这些服务。
术语¶
Fabric 通过一种叫做链码的技术概念来实现智能合约,简单来说,链码就是一段访问账本的代码,它是用网络支持的编程语言编写的。在本主题中,我们将使用链码这个术语,但是如果您更习惯智能合约这个名字也可以把它看成智能合约。它们其实是一回事!如果您想了解更多关于链码和智能合约的信息,请查看我们关于智能合约和链码的文档。
账本和链码¶
让我们来更仔细地研究一下节点。我们可以看到,节点托管账本和链码。更准确地说,节点实际上托管着账本和链码的副本。注意,这是故意在 Fabric 网络中造成副本冗余,它避免了单点故障。下文中我们将了解更多关于区块链网络分布式和去中心的特性。
Peer2
节点托管账本和链码的副本。在本例中,P1 托管了账本 L1 的副本和链码 S1 的副本。单个节点上可以托管许多账本和链码。
因为节点是账本和链码的宿主,所以应用程序和管理员必须与节点进行交互才能访问账本和链码。这就是为什么节点被视为构成 Fabric 网络最基本的构件。首次创建节点时,节点上既没有账本也没有链码。稍后我们将看到如何在节点上创建账本和安装链码。
多账本¶
一个节点能够保存多个账本,这使得灵活的系统设计成为可能,因此非常有帮助。最简单的节点配置是一个节点只管理一个账本,但是在有需要时,一个节点保存两个或多个账本是绝对合适的。
Peer3
一个节点保存多个账本。节点保存一个或多个账本,并且每个账本都具有零个或多个适用于它们的链码。在这个例子中,我们可以看到节点 P1 保存着账本 L1 和 L2。要访问账本 L1需使用链码 S1 。而要访问账本 L2 可使用链码 S1 和 S2 。
虽然节点只保存账本副本而不保存访问账本的链码副本的情况也存在,但是很少有节点是以这种方式配置的。绝大多数节点都会安装至少一个链码,以用来查询或更新该节点上的账本副本。值得一提的是,无论用户是否安装了供外部应用程序使用的链码,节点始终都有特殊的系统链码。本主题不详细讨论这些。
多链码¶
一个节点所拥有的账本数量和能够访问这些账本的链码数量之间没有固定的关系。一个节点可能拥有许多链码以及许多可供这些链码访问的账本。
Peer4
上面这个例子显示的是一个节点保存了多个链码。每个账本可被多个链码访问。在这个示例中,我们可以看到节点 P1 保存着账本 L1 和 L2,其中 L1 由链码 S1 和 S2 访问,而 L2 由 S1 和 S3 访问。我们可以看到 S1 既可以访问 L1 也可以访问 L2。
稍后我们将简单谈到为什么 Fabric 中的通道概念对于在节点上保存多个账本或多个链码来说非常重要。
应用程序和节点¶
现在我们将展示的是应用程序如何与节点交互来访问账本。账本查询交互涉及了应用程序和节点之间简单的三步对话;账本更新交互稍微复杂一些,需要两个额外的步骤。为了帮助您开始使用 Fabric,我们稍微简化了这些步骤,但是不要担心——需要你理解的最关键点在于应用程序和节点之间进行账本查询和账本更新的交易类型之间的区别。
当应用程序需要访问账本和链码时,它们总是连接到节点上。Fabric 软件开发工具包(SDK)让程序员的上述工作变得简单,首先,SDK 的应用程序编程接口(API) 让应用程序连接到节点上,随后调用链码来生成交易,接着向网络提交交易,网络将对这些交易进行排序并将其提交到分布式账本上,最后当这一过程完成时,应用程序将接收到对应的事件。
通过与一个节点连接,应用程序可以执行链码来查询或更新账本。账本查询交易的结果会立即返回,而账本更新交易涉及了应用程序、节点和排序服务之间更复杂的交互。让我们更详细地研究一下。
Peer6
节点与排序服务一起发挥作用,保证每个节点上的账本都是最新的。在本例中,应用程序 A 连接到账本 P1 上并调用链码 S1 来查询或更新 L1。 P1 调用 S1 来生成一个包含查询结果或者账本拟更新的提案响应。应用程序 A 接收到提案响应,对于查询来说,流程到这里就已经完成了。而对于更新来说,A 会基于所有响应生成一个交易,并将其发送到 排序服务 O1 进行排序。O1 将网络上的所有交易收集到区块中并分发给包括 P1 在内的所有节点。P1 对交易进行验证,随后将其提交到账本 L1 上。一旦 L1 更新完毕,P1 会生成一个事件,随后 A 收到这一事件,标志着整个过程结束。
因为节点的本地账本副本中包含了账本查询所需的全部信息,所以节点可以立即将查询结果返回给应用程序。节点从不通过与其他节点协商来响应应用程序发出的查询。但是,应用程序可以连接到一个或多个节点上来执行查询;例如,在多个节点之间验证结果,或者如果怀疑信息不是最新的,可以从另一个节点上检索最新的结果。在图中,您可以看到账本查询是只需要三个步骤,非常简单。
更新交易的开始方式和查询交易的一样,但是多了两个额外的步骤。虽然账本更新应用程序和账本查询应用程序一样,也是连接到节点上来调用链码的,但不同的是,单个节点无法执行账本更新,这是由于其他节点必须首先同意这一改变,我们把这个过程称为共识。因此,节点向应用程序返回一个拟更新——该节点将在其他节点事先同意的情况下应用该更新。第一个额外步骤(步骤4)要求应用程序向全网络中的所有节点发送一组适当的拟更新,以作为向各自账本提交的交易。该步骤是由应用程序实现的,排序服务将交易打包成区块,并将这些交易区块分发到网络中所有的节点上,节点在将这些交易应用到各自本地账本副本之前会对其进行验证。如步骤5所示,由于整个排序服务需要一些时间(几秒钟)才能完成,因此应用程序不会同时收到通知。
下文中将继续讨论排序服务的详细特性,想要深入了解此流程,请参阅交易流程主题。
节点和通道¶
虽然本节讨论的是节点而不是通道,但还有必要花点时间来了解一下节点是如何通过通道来彼此交互以及与应用程序交互的,通道是区块链网络的一种机制,有了它区块链网络中的一组组件就能够私下通信和交易。
这些组件通常是节点、排序节点和应用程序,通过加入一个通道,这些组件同意协作共享和管理与该通道关联的相同的账本副本。从概念上讲,您可以将通道看作类似于朋友组(尽管通道的成员并不需要是朋友!)一个人可能有几组朋友,每组朋友都有他们一起做的活动。这些群体可能是完全独立的(一群工作上的朋友对比于一群爱好上的朋友),或者他们之间可能有交叉。但不管怎样,每个组都是自己的实体,具有某种“规则”。
Peer5
通道允许一组特定的节点和应用程序在区块链网络中彼此通信。在本例中,应用程序 A 可以使用通道 C 直接与节点 P1和 P2通信。你可以把通道看成特定应用程序和节点之间通信的通道。(为了简单起见,此图中没有显示排序节点,但是必须在一个正常运行的网络中显示排序节点。)
我们看到通道的存在方式与节点不同——将通道看作由物理节点集合形成的逻辑结构更合适。节点为通道的访问和管理提供控制点,理解这一点非常重要。
节点和组织¶
既然您已经清楚了节点及其与账本、链码和通道的各自关系,那您将能够看到多个组织是如何组合在一起形成区块链网络的。
区块链网络的管理者是一系列组织,而不是单个组织。因为节点属于这些组织,并且是这些组织与网络的连接点,所以它对于这种分布式网络的构建至关重要。
区块链网络中的节点和多个组织。区块链网络是由不同组织所拥有和提供的节点构建而成的。在这个例子中,我们看到四个组织贡献了八个节点来组成一个网络。通道 C 连接了网络 N 中的五个节点—— P1、P3、P5、P7 和 P8。这些组织拥有的其他节点尚未连接到此通道,但它们通常至少连接到一个其他通道上。由特定组织开发的应用程序将连接到他们自己组织的节点以及不同组织的节点上。同样,为了简单起见,此图中没有显示排序节点。
务必了解区块链网络的形成过程,这一点十分重要。区块链网络是由多个向其提供资源的组织形成和管理的。节点是我们在本主题中讨论的资源,但是组织提供的资源不仅仅是节点。这里有一个原则——如果组织不向网络贡献自己的资源,那么网络根本就不会存在。此外,区块链网络随着协作组织所提供资源的增减而增长和收缩。
您可以看到(除了排序服务之外)没有集中的资源——在上面的示例中,如果组织没有将自己的节点贡献给网络 N ,那么N将不存在。这反映了以下事实:除非各组织把构成网络的资源贡献出来,否则网络在任何意义上都不存在。此外,网络并不依赖于任何一个单独的组织——不管其他组织加入或离开,只要还有一个组织,网络就还会继续存在。这就是网络去中心化的核心。
就像上边的例子,不同的组织中的应用程序可能相同也可能不同。这是因为一个组织完全取决于它的应用程序如何处理其节点的账本副本。这意味着虽然各组织的节点保存着完全相同的账本数据,但是应用程序和呈现逻辑都可能会因组织而异。
应用程序可以连接到其所在组织上的节点,也可以连接到另一个组织上的节点,这取决于所需账本交互的性质。对于账本查询交互来说,应用程序通常连接到它们自己组织的节点上。对于账本更新交互来说,下文中我们将谈到为什么应用程序需要连接到代表每个必须对账本更新背书的组织的节点上。
节点和身份¶
既然您已经清楚了来自不同组织的节点是如何聚集在一起形成区块链网络的,那么有必要花一些时间来了解组织管理员是如何将节点分配到组织中去的。
节点会被分配一个身份,这一身份是由特定证书授权中心颁发给它们的数字证书赋予的。本指南的其他部分讲解了更多关于 X.509 数字证书工作原理的信息,但是就目前而言,可以将数字证书看作是一个 ID 卡,它提供了大量关于节点的可验证信息。网络中的每个节点都由所属组织的管理员分配一个数字证书。
Peer9
当节点连接到通道时,节点的数字证书通过该通道的 MSP 识别其所属组织。在这个例子中,P1 和 P2 的身份由证书授权中心 CA1 颁布。通道 C 在其通道配置的策略中规定由 CA1 颁发的身份应该使用组织1的 MSP (ORG1.MSP) 来与 Org1 关联。类似的,P3 和 P4 由 ORG2.MSP 识别为 Org2 的一部分。
当一个节点使用通道连接到区块链网络时,通道配置中的策略使用节点的身份来确定其权限。身份与组织之间的映射是由一个名为成员服务提供者(MSP)的组件建立的——它规定了如何在特定组织中将一个特定角色分配给一个节点,并让节点相应地获得对区块链资源的适当访问权。此外,因为节点只能由单个组织拥有,所以它与单个 MSP 关联。稍后我们将谈到更多关于节点访问控制的内容,本指南的其他部分有一整节是关于 MSP 和访问控制策略的。但是现在,可以将 MSP 看作是在区块链网络中提供个人身份和特定组织角色之间的链接。
稍微讨论一个额外的话题,节点以及所有与区块链网络交互的东西都从它们的数字证书和 MSP 中获得它们的组织身份。如果节点、应用程序、最终用户、管理员和排序服务想要与区块链网络进行交互,那么它们必须具有身份和关联的 MSP。我们为使用身份与区块链网络交互的每个实体提供了一个名称——主体(principal)。在其他章节中,您可以了解更多关于主体和组织的信息,但是现在你已经有足够的知识来继续学习节点了!
最后,要注意的是节点位于哪里并不重要——它可以位于云端,或者位于一个组织的数据中心中,又或者位于一个本地机器上-——决定节点所述组织的是与该节点相关的数字证书。在我们上面的例子中,P3 可以位于 Org1 的数据中心,但是只要与 P3 相关联的数字证书是由 CA2 颁发,那么 P3 就属于 Org2。
节点和排序节点¶
我们已经看到,节点构成了区块链网络的基础,它承载着账本和智能合约,与节点相连接的应用程序可以对账本和智能合约进行查询和更新。然而,应用程序和节点使用一种机制进行彼此交互以确保每个节点上的账本始终都彼此一致,这一机制是由由名为排序节点的特殊节点来协调的,现在我们就来讨论这些节点。
更新交易与查询交易有很大的不同,这是因为光靠单个节点无法完成账本更新(更新需要网络中其他节点的同意)。一个节点需要等到其他节点对一项账本更新交易表示同意之后才能将该交易实行到自己的本地账本上。这个过程称叫做共识,比起简单的查询,共识需要的时间更长。但是,当所有需要批准某交易的节点都已批准该交易,并且已将该交易提交到账本上时,那么各节点会把账本已经更新的消息通知给与其连接的应用程序。下文将继续详细谈论节点和排序节点是如何管理共识过程的。
具体来说,想要更新账本的应用程序会涉及到三个步骤,这些步骤确保了区块链网络中的所有节点能保持彼此账本始终一致。
- 第一步,应用程序与一组背书节点合作,每个背书节点都向该应用程序提供对拟账本更新的背书,但是并不会把该拟更新应用到自己的账本副本上。
- 第二步,把各背书节点做出的背书收集起来作为交易打包成区块。
- 最后一步,将这些区块分发给每个节点,节点会对每项交易进行验证,随后将交易提交到各自账本副本上。
正如下文中你将看到的,排序节点是上述过程的核心,所以让我们来深入研究一下应用程序和节点如何使用排序节点生成账本更新,而这些更新可被持续应用到一个分布式、可复制的账本上。
第一阶段:提案¶
交易流程的第一阶段涉及的是一个应用程序和一组节点之间的交互,并不涉及排序节点。阶段一发生的只是:应用程序请求不同组织的背书节点对拟链码调用的结果做出同意。
要开始第一阶段,应用程序必须先生成一个交易提案,并将该提案发送给每个需要对交易做出背书的节点进行背书。随后,每个背书节点都使用该交易提案独立地执行一个链码,以生成交易提案响应。背书节点不会将此更新应用到账本上,它只是简单地对交易进行签名并将其返回给应用程序。一旦应用程序收到足够数量的签名提案响应,那么交易流程的第一阶段就完成了。让我们来更详细地研究一下这个阶段。
Peer10
返回已背书提案响应的各个节点会独立执行交易提案。在本例中,应用程序 A1 生成交易 T1 提案 P,并将其发送给通道 C 上的节点 P1 和 P2。P1 使用交易 T1 提案 P 调用链码 S1,以生成 交易 T1 响应 R1,并且 P1 通过 E1 对该响应进行背书。P2 独立使用交易 T1 提案 P 来执行 S1,以生成交易 T1 响应 R2,该响应是由 E2 背书的。应用程序 A1 收到交易 T1 发来的两个已背书的响应,即 E1 和 E2。
最初,应用程序选择一组节点来生成一些拟账本更新。应用程序会选择哪些节点呢?这取决于背书策略(为链码定义的),背书策略定义了一群需要在网络接受某项拟账本更新之前对其作出背书的组织。这就是达成共识的真正含义——在任何节点的账本接受拟账本更新之前,所有相关组织必须对该拟账本更新进行背书。
节点通过添加自己的数字签名来对提案响应进行背书,并使用自己的私钥为整体进行签名。该背书随后可被用于证明此组织的节点生成了一个特定响应。在我们的示例中,如果节点 P1 属于组织 Org1,则背书 E1 对应了一个数字证明——“账本 L1 上的交易 T1 响应 R1 是由 Org1 的 节点 P1 提供的!”。
节点通过添加其数字签名,并使用其私钥对整个有效负载签名,来背书提案响应。此背书随后可用于证明该组织的节点生成了特定的响应。在我们的示例中,如果节点 P1 属于组织 Org1,则背书 E1 对应一个数字证明——“在账本 L1 上的交易 T1 的反馈 R1 已经被 Org1 的 peer P1 提供了!”。
当应用程序收到足够数量节点发来的签名提案响应时,第一阶段就结束了。我们注意到,对于相同的交易提案,不同的节点可能会向应用程序返回不同的,因此也就不一致的交易响应。这或许只是因为各节点生成交易响应的时间不同,账本的世界状态也不同,在这种情况下,应用程序只需请求一个更新版本的提案响应即可;又或许是因为链码是不确定的,这种情况发生的可能性不太大,但是一旦发生,后果极其严重。不确定性是链码和账本的敌人,如果发生这种情况,则表明提案的交易存在严重问题,因为不一致的结果显然不能适用于账本。单个节点无法知道自己的交易结果是非确定性的——只有将所有交易响应收集在一起进行比较才能发现不确定性。(严格来说,这些依然不够,不过我们暂且不继续往下谈,后续在交易模块会深入探讨非确定性的问题。)
在第一阶段的最后,应用程序可以按照自己的意愿丢弃不一致的交易响应,从而有效地提前终止交易流程。稍后我们将看到,如果应用程序试图使用一组不一致的交易响应来更新账本,它将被拒绝。
第二阶段:排序和把交易打包成区块¶
交易流程的第二阶段是打包阶段。排序节点是该流程的关键——多个应用程序向排序节点发送交易,其中包含了已背书的交易提案响应,随后排序节点将这些交易排序进区块。有关排序和打包阶段的更多细节,请查看排序阶段的概念信息。
第三阶段:验证和提交¶
在阶段二的末尾,我们看到排序节点负责的是对提案的交易更新进行收集、排序、打包进区块,以便分发给各节点,这些任务虽简单但却十分关键。
交易流程的最后一个阶段涉及到从排序节点到 Peer 节点的区块的分发和随后的验证,节点可将这些交易应用到账本上。具体来说,所有节点都会对一个区块内的每一笔交易进行验证,以确保这些交易在应用到账本上之前已经得到所有相关组织的背书。未成功的交易会被留下来进行审计,但不会提交到账本上。
Peer12
排序节点的第二个角色是将区块分发给 peer 节点。在本例中,排序节点 O1 将区块 B2 分配给节点 P1 和 P2。节点 P1 处理区块 B2,最终将一个新的区块添加到节点 P1 的账本 L1 上。同时,节点 P2 处理区块 B2,最终将一个新的区块添加到节点 P2 的账本 L1 上。一旦这个过程结束,节点 P1和 P2 各自的账本 L1 就已经一致更新了,并且 P1 和 P2 都会把交易已处理完毕的消息通知给与它们连接的应用程序。
阶段三从排序节点将区块分发给与其连接的所有 peer 节点开始。peer 节点和通道上的排序节点相连接,这样一来,当生成一个新区块时,连接到排序节点的所有 Peer 节点都将收到这个新区块的副本。随后每个 Peer 节点独立地处理该新区块,不过通道上各节点的处理方式完全相同。因此,我们将看到各节点的账本依然保持一致。同样值得注意的是,并不是每个 peer 节点都需要连接到一个排序节点上——peer 节点可以使用 gossip 协议将区块信息发送给其他(未连接到排序节点的) Peer 节点,这些 Peer 节点在收到信息后也可以独立地处理新区块。不过我们下次再讨论这个问题!
当接收到一个区块时,节点将按照各交易在区块中出现的顺序依次进行处理。各节点将根据生成交易的链码中的背书策略对每项交易进行验证,以确保所有交易都已被相关组织背书。例如,某些交易可能只需要一个组织的背书,而另一些交易可能需要多个组织的背书才能被视为有效。该验证过程将验证所有相关组织是否都已经产生了相同的结果。还要注意的是,此验证与阶段一中的背书检查不同,在背书检查中,是应用程序接收了背书节点发送的响应,并作出发送提案交易的决定。如果应用程序发送错误的交易,违反了背书策略,那么节点在阶段三的验证环节依然能够拒绝该交易。
如果一笔交易被正确地背书,节点将试图把它应用于账本。为此,节点必须执行账本一致性检查,以验证账本的当前状态与生成提案更新时的账本状态是否兼容。两个状态可能不总是兼容的,即便交易已经被所有相关组织背书了。例如,另一个交易可能更新了账本中的相同资产,这就导致前一个交易更新不再有效,因此无法将其应用到账本上。通过这种方式,每个节点的账本副本在整个网络中保持一致,这是因为它们都遵循相同的验证规则。
在节点成功地验证每一笔交易之后,它将更新账本。节点不会把失败的交易应用到账本上,但为了审计会保留这些交易,就像成功的交易一样。这意味着节点中的区块与从排序节点接收到的区块几乎完全相同,唯一不同的是节点区块中的每个交易上都存在有效或无效指示符。
我们还注意到,阶段三不需要运行链码,链码运行只发生在阶段一,这一点很重要。它意味着链码只需存在于背书节点上,并不要求在整个区块链网络上都能使用链码。这样一来,只有参与背书的组织才能看到链码的逻辑,因此这一点通常很有帮助。而链码(交易提案响应)的输出则与之相反,链码输出会在通道上的各节点间共享,无论这些节点是否参与了该交易的背书。这种背书节点的特殊设计旨在提升区块链网络的可扩展性和机密性。
最后,每次将一个区块提交到节点的账本上时,该节点都会生成一个适当的事件。区块事件包括完整的区块内容,而区块交易事件只包含总结信息,例如区块中的每个交易经验证显示有效还是无效。链码执行产生的链码事件也可以在这个时候发布出去。应用程序可以注册这些事件的类型,以便在这些事件发生时,它可以得到通知。这些通知标志着交易流程最后一步结束了。
总之,第三阶段中,排序节点生成的区块都被一致地应用于账本。将交易严格排序到区块中,这使得每个节点都可以验证交易更新是否在整个区块链网络中得到一致应用。
排序节点和共识¶
上述的整个交易流程叫做共识,这是因为所有节点都已就交易的顺序和内容达成一致,排序节点在这个流程中扮演调节者的角色。共识是一个多步骤的过程,只有当流程完成时,应用程序才会收到账本更新的通知——在不同的节点上,更新完成的时间可能略有不同。
我们将在以后的排序节点主题中更详细地讨论它,但是现在我们可以先这样理解排序节点:它从应用程序那里收集和分发提案的账本更新,以供节点进行验证并最终应用到账本上。
就是这样!现在我们已经完成了节点以及与 Fabric 相关的其他组件的学习。我们已经看到,节点在很多方面都是最基本的元素——它们组成网络、托管链码和账本、处理交易提案和响应,并且与其他节点一致地把交易更新应用到账本上,从而使账本的状态保持最新。
智能合约和链码¶
受众 :架构师、应用程序和智能合约开发者、管理员。
从应用程序开发人员的角度来看,智能合约与账本一起构成了 Hyperledger Fabric 区块链系统的核心。账本包含了与一组业务对象的当前和历史状态有关的事实,而智能合约定义了生成这些被添加到账本中的新事实的可执行逻辑。管理员通常使用链码将相关的智能合约组织起来进行部署,但链码也可以用于Fabric的低级系统编程。在本主题中,我们将重点讨论为什么智能合约和链码都存在,以及如何和何时使用它们。
在本主题中,我们将讨论:
智能合约¶
在各业务彼此进行交互之前,必须先定义一套通用的合约,其中包括通用术语、数据、规则、概念定义和流程。将这些合约放在一起,就构成了管理交易各方之间所有交互的业务模型。
smart.diagram1
智能合约用可执行的代码定义了不同组织之间的规则。应用程序调用智能合约来生成被记录到账本上的交易。
使用区块链网络,我们可以将这些合约转换为可执行程序(业内称为智能合约),从而实现了各种各样的新可能性。这是因为智能合约可以为任何类型的业务对象实现治理规则,以便在执行智能合约时自动执行这些规则。例如,一个智能合约可能会确保新车在指定的时间内交付,或者根据预先安排的条款释放资金,前者可改善货物流通,而后者可优化资本流动。然而最重要的是,智能合约的执行要比人工业务流程高效得多。
在上面的图中,我们可以看到组织ORG1
和 ORG2
是如何定义了一个 car
智能合约来query
、transfer
和update
汽车的。来自这些组织的应用程序调用此智能合约以执行业务流程中已商定的步骤,例如将特定汽车的所有权从 ORG1
转移到 ORG2
。
术语¶
Hyperledger Fabric 用户经常交替使用智能合约和链码。通常,智能合约定义的是控制世界状态中业务对象生命周期的交易逻辑,随后该交易逻辑被打包进链码,紧接着链码会被部署到区块链网络中。可以将智能合约看成交易的管理者,而链码则管理着如何将智能合约打包用于部署。
smart.diagram2
一个智能合约定义在一个链码中。而多个智能合约也可以定义在同一个链码中。当一个链码部署完毕,该链码中的所有智能合约都可供应用程序使用。
从上图中我们可以看到,vehicle
链码包含了以下三个智能合约:cars
、boats
和 trucks
;而 insurance
链码包含了以下四个智能合约:policy
、liability
、syndication
和securitization
。以上每种智能合约都涵盖了与车辆和保险有关的业务流程的一些关键点。在本主题中,我们将以car
智能合约为例。我们可以看到,智能合约是一个特定领域的程序,它与特定的业务流程相关,而链码则是一组相关智能合约的技术容器。
账本¶
以最简单的方式来说,区块链记录着更新账本状态的交易,且记录不可篡改。智能合约以编程方式访问账本两个不同的部分: 一个是区块链(记录所有交易的历史,且记录不可篡改),另一个是世界状态(保存这些状态当前值的缓存,是一个经常需要用到的对象的当前值)。
智能合约主要在世界状态中将状态写入(put)、读取(get)和删除(delete),还可以查询不可篡改的区块链交易记录。
- 读取(get) 操作一般代表的是查询,目的是获取关于交易对象当前状态的信息。
- 写入(put) 操作通常生成一个新的业务对象或者对账本世界状态中现有的业务对象进行修改。
- 删除(delete) 操作代表的是将一个业务对象从账本的当前状态中移除,但不从账本的历史中移除。
智能合约有许多可用的 API(应用程序编码接口)。但关键的是,在所有情况下,无论交易创建、读取、更新还是删除世界状态中的业务对象,区块链都包含了这些操作的记录,且记录不可更改 。
开发¶
智能合约是应用程序开发的重点,正如我们所看到的,一个链码中可定义一个或多个智能合约。将链码部署到网络中以后,网络上的组织就都可以使用该链码中的所有智能合约。这意味着只有管理员才需要考虑链码;其他人都只用考虑智能合约。
智能合约的核心是一组 transaction
定义。例如,在 fabcar.js
中,你可以看到一个创建了一辆新车的智能合约交易:
async createCar(ctx, carNumber, make, model, color, owner) {
const car = {
color,
docType: 'car',
make,
model,
owner,
};
await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car)));
}
在编写您的第一个应用程序 教程中,您可以了解更多关于 Fabcar 智能合约的信息。
智能合约几乎可以描述所有与多组织决策中数据不可变性相关的业务案例。智能合约开发人员的工作是将一个现有的业务流程(可能是管理金融价格或交付条件)用 JavaScript、GOLANG 或 Java 等编程语言来表示成一个智能合约。将数百年的法律语言转换为编程语言需要法律和技术方面的技能,智能合约审核员们不断地实践着这些技能。您可以在开发应用程序主题中了解如何设计和开发智能合约。
背书¶
每个链码都有一个背书策略与之相关联,该背书策略适用于此链码中定义的所有智能合约。背书策略非常重要,它指明了区块链网络中哪些组织必须对一个既定智能合约所生成的交易进行签名,以此来宣布该交易有效。
smart.diagram3
每个智能合约都有一个与之关联的背书策略。这个背书策略定义了在智能合约生成的交易被认证为有效之前,哪些组织必须同意该交易。
一个示例背书策略可能这样定义:参与区块链网络的四个组织中有三个必须在交易被认为有效之前签署该交易。所有的交易,无论是有效的还是无效的,都会被添加到分布式账本中,但仅有效交易会更新世界状态。
如果一项背书策略指定,必须有不止一个组织来签署交易,那么只有当足够数量的组织都执行了智能合约,才能够生成有效交易。在上面的示例中,要使用于车辆 transfer
的智能合约交易有效,需要 ORG1
和 ORG2
都执行并签署该交易。
背书策略是 Hyperledger Fabric 与以太坊(Ethereum)或比特币(Bitcoin)等其他区块链的区别所在。在这些区块链系统中,网络上的任何节点都可以生成有效的交易。而 Hyperledger Fabric 更真实地模拟了现实世界;交易必须由 Fabric 网络中受信任的组织验证。例如,一个政府组织必须签署一个有效的 issueIdentity
交易,或者一辆车的 buyer
和 seller
都必须签署一个 car
转移交易。背书策略的设计旨在让 Hyperledger Fabric 更好地模拟这些真实发生的交互。
最后,背书策略只是 Hyperledger Fabric 中策略的一个例子。还可以定义其他策略来确定谁可以查询或更新账本,或者谁可以在网络中添加或删除参与者。总体来说,虽然区块链网络中的组织联盟并非一成不变,但是它们需要事先商定好策略。实际上,策略本身可以定义对自己进行更改的规则。虽然现在谈论这个主题有点早,但是在 Fabric 提供的规则基础上来定义自定义背书策略的规则也是可能实现的。
有效交易¶
当智能合约执行时,它会在区块链网络中组织所拥有的节点上运行。智能合约提取一组名为交易提案的输入参数,并将其与程序逻辑结合起来使用以读写账本。对世界状态的更改被捕获为交易提案响应(或简称交易响应),该响应包含一个读写集,其中既含有已读取的状态,也含有还未书写的新状态(如果交易有效的话)。注意,在执行智能合约时世界状态没有更新!
smart.diagram4
所有的交易都有一个识别符、一个提案和一个被一群组织签名的响应。所有交易,无论是否有效,都会被记录在区块链上,但仅有效交易会更新世界状态。
检查 car transfer
交易。您可以看到 ORG1
和 ORG2
之间为转移一辆车而进行的交易 t3
。看一下交易是如何通过输入{CAR1,ORG1,ORG2}
和输出 {CAR1.owner=ORG1,CAR1.owner=ORG2}
来表示汽车的所有者从 ORG1
变为了 ORG2
。注意输入是如何由应用程序的组织 ORG1
签名的,输出是如何由背书策略标识的两个组织( ORG1
和 ORG2
)签名的。这些签名是使用每个参与者的私钥生成的,这意味着网络中的任何人都可以验证网络中的所有参与者是否在交易细节上达成了一致。
一项交易被分发给网络中的所有节点,各节点通过两个阶段对其进行验证。首先,根据背书策略检查交易,确保该交易已被足够的组织签署。其次,继续检查交易,以确保当该交易在受到背书节点签名时它的交易读集与世界状态的当前值匹配,并且中间过程中没有被更新。如果一个交易通过了这两个测试,它就被标记为有效。所有交易,不管是有效的还是无效的,都会被添加到区块链历史中,但是仅有效的交易才会更新世界状态。
在我们的示例中,t3
是一个有效的交易,因此 CAR1
的所有者已更新为 ORG2
。但是 t4
(未显示)是无效的交易,所以当把它记录在账本上时,世界状态没有更新,CAR2
仍然属于 ORG2
所有。
最后,要了解如何通过世界状态来使用智能合约或链码,请阅读链码命名空间主题。
通道¶
Hyperledger Fabric 允许一个组织利用通道同时参与多个、彼此独立的区块链网络。通过加入多个通道,一个组织可以参与一个所谓的网络的网络。通道在维持数据和通信隐私的同时还提供了高效的基础设施共享。通道是足够独立的,可以帮助组织将自己的工作流量与其他组织的分开,同时它还具有足够的协调性,在必要时能够协调各个独立的活动。
smart.diagram5
通道在一群组织之间提供了一种完全独立的通信机制。当链码定义被提交到通道上时,该通道上所有的应用程序都可以使用此链码中的智能合约。
虽然智能合约代码被安装在组织节点的链码包内,但是只有等到链码被定义在通道上之后,该通道上的成员才能够执行其中的智能合约。链码定义是一种包含了许多参数的结构,这些参数管理着链码的运行方式,包含着链码名、版本以及背书策略。各通道成员批准各自组织的一个链码定义,以表示其对该链码的参数表示同意。当足够数量(默认是多数)的组织都已批准同一个链码定义,该定义可被提交至这些组织所在的通道。随后,通道成员可依据该链码定义中指明的背书策略来执行其中的智能合约。这个背书策略可同等使用于在相同链码中定义的所有智能合约。
在上面的示例中,car
智能合约被定义在 vehicle
通道上,insurance
智能合约被定义在 INSURANCE
通道上。car
的链码定义明确了以下背书策略:任何交易在被认定为有效之前必须由 ORG1
和 ORG2
共同签名。insurance
智能合约的链码定义明确了只需要 ORG3
对交易进行背书即可。ORG1
参与了 VEHICLE
通道和 INSURANCE
通道这两个网络,并且能够跨网络协调与 ORG2
和 ORG3
的活动。
将汽车
合约部署到车辆
通道,并将保险
合约部署到保险
通道。汽车合约有一个背书策略,要求 ORG1
和 ORG2
在交易被认为有效之前签署该交易,而保险合约也有一个背书策略,只要求 ORG3
签署有效交易。ORG1
参与车辆
通道和保险
网络两个网络,分别与 ORG2
和 ORG3
协调这两个网络之间的活动。
互通¶
一个智能合约既可以调用同通道上的其他智能合约,也可以调用其他通道上的智能合约。这样一来,智能合约就可以读写原本因为智能合约命名空间而无法访问的世界状态数据。
这些都是智能合约彼此通信的限制,我们将在链码命名空间主题中详细描述。
系统链码¶
链码中定义的智能合约为一群区块链组织共同认可的业务流程编码了领域相关规则。然而,链码还可以定义低级别程序代码,这些代码符合无关于领域的系统交互,但与业务流程的智能合约无关。
以下是不同类型的系统链码及其相关缩写:
_lifecycle
在所有 Peer 节点上运行,它负责管理节点上的链码安装、批准组织的链码定义、将链码定义提交到通道上。你可以在这里阅读更多关于_lifecycle
如何实现 Fabric 链码生命周期的内容。- 生命周期系统链码(LSCC)负责为1.x版本的 Fabric 管理链码生命周期。该版本的生命周期要求在通道上实例化或升级链码。你可以阅读更多关于LSCC如何实现这一过程。如果你的 V1_4_x 或更低版本设有通道应用程序的功能,那么你也可以使用LSCC来管理链码。
- **配置系统链码(CSCC)**在所有 Peer 节点上运行,以处理通道配置的变化,比如策略更新。你可以在这里阅读更多 CSCC 实现的内容。
- **查询系统链码(QSCC)**在所有 Peer 节点上运行,以提供账本 API(应用程序编码接口),其中包括区块查询、交易查询等。你可以在交易场景主题中查阅更多这些账本 API 的信息。
- **背书系统链码(ESCC)**在背书节点上运行,对一个交易响应进行密码签名。你可以在这里阅读更多 ESCC 实现的内容。
- **验证系统链码(VSCC)**验证一个交易,包括检查背书策略和读写集版本。你可以在这里阅读更多 LSCC 实现的内容。
底层的 Fabric 开发人员和管理员可以根据自己的需要修改这些系统链码。然而,系统链码的开发和管理是一项专门的活动,完全独立于智能合约的开发,通常没有必要进行系统链码的开发和管理。系统链码对于一个 Hyperledger Fabric网络的正常运行至关重要,因此必须非常小心地处理系统链码的更改。例如,如果没有正确地开发系统链码,那么有可能某个 Peer 节点更新的世界状态或区块链备份与其他 peer 节点的不同。这种缺乏共识的现象是账本分叉的一种形式,是极不理想的情况。
账本¶
受众:架构师、应用程序开发者和智能合约开发者、管理员
账本是 Hyperledger Fabric 中的一个重要概念,它存储了有关业务对象的重要事实信息,其中既包括对象属性的当前值,也包括产生这些当前值的交易的历史。
在这个主题中,我们将谈到:
什么是账本?¶
账本记录着业务的当前状态,它就像一个交易日记。欧洲和中国最早的账本可以追溯到近 1000 年前,苏美尔人在 4000 年前就已经有石制账本了,不过我们还是从离我们最近的例子开始讲吧!
你可能已经习惯查看你的银行账户了。对你来说,最重要的是账户余额,它是你当时就能花的钱。如果你想看看你的余额是如何产生的,可以浏览一下相关的交易收入和支出。这是现实生活中的一个账本的示例——一个状态(您的银行余额)和一组促成该状态的有序交易(收入和支出)。Hyperledger Fabric 也致力于这两个方面,它旨在呈现一组账本状态的当前值,同时记录下促成了以上账本状态的交易的历史。
账本、事实和状态¶
账本储存的其实并不是业务对象本身,而是与业务对象相关的事实信息。当我们说“我们在账本中存储一个业务对象”时,其实是说我们正在记录与一个业务对象当前状态有关的事实,以及与促成这一当前状态的交易历史相关的事实。在一个日益数字化的世界里,我们感觉自己正在看的是一个物体本身,而不是关于这个物体的一些事实。对于数字对象来说,它可能位于一个外部数据库,但通过我们储存在账本中有关该对象的事实就能够识别出该数字对象的所在位置以及其他与之相关的关键信息。
虽然与业务对象当前状态相关的事实可能会发生改变,但是与之相关的事实历史是不可变的,我们可以在事实历史上增加新的事实,但无法更改历史中已经存在的事实。我们将看到,如果把区块链看作是与业务对象有关的事实历史,且该历史是不可更改的,那么我们就能够很轻松、高效地理解区块链。
现在我们来深入探讨一下 Hyperledger Fabric 的账本结构!
账本¶
Hyperledger Fabric 中的账本由“世界状态“和”区块链“这两部分组成,它们彼此不同但却相互关联。二者都代表了与业务对象有关的一些事实。
首先,世界状态是一个数据库,它存储了一组账本状态的当前值。通过世界状态,程序可以直接访问一个账本状态的当前值,不需要遍历整个交易日志来计算当前值。默认情况下,账本状态是以键值对的方式来表示的,稍后我们将看到 Hyperledger Fabric 如何提供这一方面的灵活性。因为我们可以创建、更新和删除状态,所以世界状态能够频繁更改。
其次,区块链是交易日志,它记录了促成当前世界状态的所有改变。交易被收集在附加到区块链的区块中,能帮助我们理解所有促成当前世界状态的改变的历史。区块链数据结构与世界状态相差甚远,因为一旦把数据写入区块链,就无法修改,它是不可篡改的。
ledger.ledger
账本 L 由区块链 B 和世界状态 W组成,其中世界状态 W 由区块链 B 决定。我们也可以说世界状态 W 是源自区块链 B。
为帮助理解,可以这样认为: Hyperledger Fabric 网络中存在一个逻辑账本。实际上,Fabric 网络维护着一个账本的多个副本,这些副本通过名为共识的过程来与其他副本保持一致。分布式账本技术(DLT)这个术语经常与这种账本联系在一起,这种账本在逻辑上是单个的,但是在整个网络中却分布着许多彼此一致的副本。
现在让我们更细致地研究一下世界状态和区块链数据结构。
世界状态¶
世界状态将业务对象属性的当前值保存为唯一的账本状态。这很有用,因为程序通常需要对象的当前值,如果遍历整个区块链来计算对象的当前值会很麻烦——从世界状态中可以直接获取当前值。
ledger.worldstate
一个账本世界状态包含两个状态。第一个状态是: key=CAR1 和 value=Audi。第二个状态中有一个更复杂的值:key=CAR2 和 value={model:BMW, color=red, owner=Jane} 。两个状态的版本都是0。
账本状态记录了一组与特定业务对象有关的事实。我们的示例展示的是 CAR1 和 CAR2 这两辆车的账本状态,二者都各有一个值和一个键。应用程序可以调用智能合约,该合约使用简单的账本 API 来获取、写入和删除状态。注意状态值可以是简单值(Audi…),也可以是复合值(type:BMW…)。经常会通过查询世界状态来检索具有某些特定属性的对象,例如查找所有红色宝马。
世界状态被作为数据库来实现。这一点很有意义,因为数据库为有效存储和状态检索提供了充分的算子。稍后我们将看到,我们可以将 Hyperledger Fabric 配置为使用不同的世界状态数据库来满足以下需求:不同类型的状态值,应用程序所需的访问模式,例如,当遇到复杂查询的情况时。
应用程序提交那些会更改世界状态的交易,这些交易最终被提交到账本区块链上。应用程序无法看到 Hyperledger Fabric SDK(软件开发工具包) 设定的共识机制的细节内容,它们能做的只是调用智能合约以及在交易被收进区块链时收到通知(所有被提交的交易,无论有效与否,都会被收进区块链)。Hyperledger Fabric 的关键设计在于,只有那些受到相关背书组织签名的交易才会更新世界状态。如果一个交易没有得到足够背书节点的签名,那么它不会更新世界状态。您可以阅读更多关于应用程序如何使用智能合约以及如何开发应用程序的信息。
您还会注意到,每个状态都有一个版本号,在上面的图表中,状态 CAR1 和 CAR2 都处于它们的初始版本 0。版本号是供 Hyperledger Fabric 内部使用的,并且每次状态更改时版本号会发生递增。每当更新状态时,都会检查该状态的版本,以确保当前状态与背书时的版本相匹配。这就确保了世界状态是按照预期进行更新的,没有发生并发更新。
最后,首次创建账本时,世界状态是空的。因为区块链上记录了所有代表有效世界状态更新的交易,所以任何时候都可以从区块链中重新生成世界状态。这样一来就变得非常方便,例如,创建节点时会自动生成世界状态。此外,如果某个节点发生异常,重启该节点时能够在接受交易之前重新生成世界状态。
区块链¶
现在让我们把注意力从世界状态转移到区块链上。世界状态存储了与业务对象当前状态相关的事实信息,而区块链是一种历史记录,它记录了这些业务对象是如何到达各自当前状态的相关事实。区块链记录了每个账本状态之前的所有版本以及状态是如何被更改的。
区块链的结构是一群相互链接的区块的序列化日志,其中每个区块都包含一系列交易,各项交易代表了一个对世界状态进行的查询或更新操作。我们在其他地方讨论了排序交易的确切机制;其中重要的是区块排序以及区块内的交易排序,这一机制是在 Hyperledger Fabric 的排序服务组件首次创建区块时被建立起来的。
每个区块的头部都包含区块交易的一个哈希,以及前一个区块头的哈希。这样一来,账本上的所有交易都被按序排列,并以密码方式连接在一起。这种哈希和链接使账本数据变得非常安全。即使某个保存账本的节点被篡改了,该节点也无法让其他节点相信自己拥有“正确的”区块链,这是因为账本被分布在一个由独立节点组成的网络中。
区块链总是被作为一个文件来实现,而与之相反的是,世界状态被作为一个数据库来实现。这是一个明智的设计,因为区块链数据结构高度偏向于非常小的一组简单操作。第一项操作被放在区块链的末尾,就目前来说,查询操纵相对少见。
让我们更细致地看看区块链的结构。
ledger.blockchain
区块链 B 包含了 B0、B1、B2、B3这四个区块。B0 是该区块链的第一个区块,也叫创世区块。
在上面的图中我们可以看到,区块 B2 有一个区块数据 D2,该数据包含了 B2 的所有交易:T5、T6、T7。
最重要的是,B2 有一个区块头 H2,H2 包含了 D2 中所有交易的加密哈希以及前一个区块中 H1 的一个哈希。这样一来,所有区块彼此紧密相连,不可篡改,术语区块链很好地描述了这一点!
最后,如图所示,区块链中的第一个区块被称为创始区块。虽然它并不包含任何用户交易,但却是账本的起始点。相反的,创世区块包含了一个配置交易,该交易含有网络配置(未显示)的初始状态。我们将会在讨论区块链网络和通道 时更详细地探讨初始区块。
区块¶
让我们仔细看看区块的结构。它由三个部分组成
区块头
这个部分包含三个字段,这些字段是在创建一个区块时候被写入的。
- 区块编号:编号从0(初始区块)开始,每在区块链上增加一个新区块,编号的数字都会加1。
- 当前区块的哈希值:当前区块中包含的所有交易的哈希值。
- 前一个区块头的哈希值:区块链中前一个区块头的哈希值。
这些字段是通过在内部对区块数据进行加密哈希而生成的。它们确保了每一个区块和与之相邻的其他区块紧密相连,从而组成一个不可更改的账本。
ledger.blocks
区块头详情:区块 B2 的区块头 H2 包含了区块编号 2,当前区块数据 D2 的哈希值 CH2,以及前一个区块头 H1的哈希值 PH1。
区块数据
这部分包含了一个有序的交易列表。区块数据是在排序服务创建区块时被写入的。这些交易的结构很复杂但也很直接,我们会在后边进行讲解。
区块元数据
这个部分包含了区块被写入的时间,还有区块写入者的证书、公钥以及签名。随后,区块的提交者也会为每一笔交易添加一个有效或无效的标记,但由于这一信息与区块同时产生,所以它不会被包含在哈希中。
交易¶
正如我们所看到的,交易记录了世界状态发生的更新。让我们来详细了解一下这种把交易包含在区块中的区块数据结构。
ledger.transaction
交易详情:交易 T4 位于区块 B1 的区块数据 D1 中,T4包括的内容如下:交易头 H4,一个交易签名 S4,一个交易提案 P4,一个交易响应 R4 和一系列背书 E4。
在上面的例子中,我们可以看到以下字段:
(Header)交易头
这部分用 H4 表示,它记录了关于交易的一些重要元数据,比如,相关链码的名字以及版本。
(Signature)交易签名
这部分用 S4 表示,它包含了一个由客户端应用程序创建的加密签名。该字段是用来检查交易细节是否未经篡改,因为交易签名的生成需要用到应用程序的私钥。
(Proposal)交易提案
这部分用 P4 表示,它负责对应用程序供给智能合约的输入参数进行编码,随后该智能合约生成提案账本更新。在智能合约运行时,这个提案提供了一套输入参数,这些参数同当前的世界状态一起决定了新的账本世界状态。
(Response)交易响应
这部分用 R4 表示,它是以读写集 (RW-set)的形式记录下世界状态之前和之后的值。交易响应是智能合约的输出,如果交易验证成功,那么该交易会被应用到账本上,从而更新世界状态。
(Endorsements)交易背书
就像 E4 显示的那样,它指的是一组签名交易响应,这些签名都来自背书策略规定的相关组织,并且这些组织的数量必须满足背书策略的要求。你会注意到,虽然交易中包含了多个背书,但它却只有一个交易响应。这是因为每个背书都对组织特定的交易响应进行了有效编码,那些不完全满足背书的交易响应肯定会遭到拒绝、被视为无效,而且它们也不会更新世界状态,所以没必要放进交易中。
在交易中只包含一个交易响应,但是会有多个背书。这是因为每个背书包含了它的组织特定的交易响应,这意味着不需要包含任何没有有效的背书的交易响应,因为它会被作为无效的交易被拒绝,并且不会更新世界状态。
以上总结了交易的一些主要字段,其实还有其他字段,但是上述几种是您需要了解的基本字段,便于您对账本数据结构有一个很好的了解。
世界状态数据库选项¶
世界状态是以数据库的形式实现的,旨在提供简单有效的账本状态存储和检索。正如我们所看到的,账本状态可包含简单值或复合值,为了适应这一点,世界状态数据库可以多种形式实现,从而对这些值进行有效实现。目前,世界状态数据库的选项包括 LevelDB 和 CouchDB 。
LevelDB 是世界状态数据库的默认选项,当账本状态是简单的键值对时,使用 LevelDB 非常合适。LevelDB 数据库与 peer 节点位于相同位置,它被嵌入与 peer 节点相同的操作系统进程中。
当账本状态结构为 JSON 文档时,以 CouchDB 来实现世界状态非常合适,这是因为业务交易涉及的数据类型通常十分丰富,而 CouchDB 可支持对这些数据类型进行各种形式的查询和更新。在实现方面,CouchDB 是在单独的操作系统进程中运行的,但是节点和 CouchDB 实例之间仍然存在1:1的关系。智能合约无法看到上述任何内容。有关 CouchDB 的更多信息,请参见 CouchDB 作为状态数据库。
在 LevelDB 和 CouchDB 中,我们看到了 Hyperledger Fabric 的一个重要方面——它是可插拔的。世界状态数据库可以是关系数据存储、图形存储或时态数据库。这极大提升了可被有效访问的账本状态类型的灵活性,使得 Hyperledger Fabric 能够处理多种不同类型的问题。
示例账本:fabcar¶
关于账本的讨论即将结束,让我们来看一个示例账本。如果您已经运行了 fabcar 示例应用程序,那么您就已经创建了这个账本。
fabcar 示例应用程序创建了 10 辆车,每辆车都有独一无二的身份;它们有不同的颜色,制造商,型号和拥有者。以下是前四辆车创建后的账本。
ledger.transaction
账本 L包含了一个世界状态 W 和一个区块链 B。其中 W 包含了四个状态,各状态的键分别是:CAR0,CAR1,CAR2 和 CAR3 。而 B 包含了两个区块 0和 1。区块1包含了四笔交易:T1,T2,T3,T4。
我们可以看到世界状态包含了对应于 CAR0、CAR1、CAR2 和 CAR3 的状态。CAR0 中包含的值表明了这是一辆蓝色的丰田普锐斯(Toyota Prius),目前车主是 Tomoko,其他车辆的的状态和值也与此类似。此外,我们还可以看到所有车辆状态的版本号都是0,这是它们的初始版本号,也就是说这些车辆状态自创建以来一直没有被更新过。
我们还可以看到区块链包含两个区块。其中区块0是创世区块,但它并不包含任何与汽车相关的交易。而区块1包含交易 T1、T2、T3、T4,这些交易与生成世界状态中 CAR0 到 CAR3 这四辆车初始状态的交易相符。同时区块1与区块0是相连的。
我们没有介绍区块或交易中的其他字段,特别是(区块/交易)头和哈希,如果你对这部分内容感兴趣的话,可以阅读文件中的其他部分。读完后你会对整个区块和交易有更加透彻的认识,但现在,你对 Hyperledger Fabric 账本的概念已经足够了解了。恭喜!
命名空间¶
上文中我们讨论账本时,似乎它只包括一个世界状态和一条区块链,但这显然过于简单化了。实际上,每个链码都有自己的世界状态,并且与所有其他链码的世界状态分离。世界状态位于一个命名空间中,因此只有位于同一链码中的智能合约才能访问一个给定的名称空间。
区块链没有命名空间。它包含来自许多不同智能合约命名空间的交易。您可以在此主题中阅读更多关于链码命名空间的信息。
现在让我们看看命名空间的概念是如何被应用到 Hyperledger Fabric 通道中的。
通道¶
在 Hyperledger Fabric 中,每个通道都有一个完全独立的账本。这意味着完全独立的区块链和完全独立的世界状态,包括名称空间。应用程序和智能合约可以在通道之间通信,以便在通道间访问账本信息。
在本主题中,您可以阅读更多关于账本如何与通道一起工作的信息。
更多信息¶
要深入了解交易流程、并发控制和世界状态数据库,请查阅交易流程、读写集语义和 CouchDB 作为状态数据库主题。
排序服务¶
受众:架构师、排序服务管理员、通道创建者
本主题将概念性的介绍排序的概念、排序节点是如何与 Peer 节点交互的、它们在交易流程中如何所发挥作用以及当前可用的排序服务的实现方式,尤其关注 Raft 排序服务实现。
什么是排序?¶
许多分布式区块链,如以太坊(Ethereum)和比特币(Bitcoin),都是非许可链的,这意味着任何节点都可以参与共识过程,在共识过程中,交易被排序并捆绑成区块。因为这个事实,这些系统依靠概率共识算法最终保证账本一致性高的概率,但仍容易受到不同的账本(有时也称为一个账本“分叉”),在网络中不同的参与者对于交易顺序有不同的观点。
Hyperledger Fabric 的工作方式不同。它有一种称为排序节点的节点使交易有序,并与其他排序节点一起形成一个排序服务。因为 Fabric 的设计依赖于确定性的共识算法,所以 Peer 节点所验证的排序服务生成的任何区块都是最终的和正确的。账本不会像其他分布式区块链中那样产生分叉。
除了促进确定性之外,排序节点还将链码执行的背书(发生在节点)与排序分离,这在性能和可伸缩性方面给 Fabric 提供了优势,消除了由同一个节点执行和排序时可能出现的瓶颈。
排序节点和通道配置¶
除了排序角色之外,排序节点还维护着允许创建通道的组织列表。此组织列表称为“联盟”,列表本身保存在“排序节点系统通道”(也称为“排序系统通道”)的配置中。默认情况下,此列表及其所在的通道只能由排序节点管理员编辑。请注意,排序服务可以保存这些列表中的几个,这使得联盟成为 Fabric 多租户的载体。
排序节点还对通道执行基本访问控制,限制谁可以读写数据,以及谁可以配置数据。请记住,谁有权修改通道中的配置元素取决于相关管理员在创建联盟或通道时设置的策略。配置交易由排序节点处理,因为它需要知道当前的策略集合,并根据策略来执行其基本的访问控制。在这种情况下,排序节点处理配置更新,以确保请求者拥有正确的管理权限。如果有权限,排序节点将根据现有配置验证更新请求,生成一个新的配置交易,并将其打包到一个区块中,该区块将转发给通道上的所有节点。然后节点处理配置交易,以验证排序节点批准的修改确实满足通道中定义的策略。
排序节点和身份¶
与区块链网络交互的所有东西,包括节点、应用程序、管理员和排序节点,都从它们的数字证书和成员服务提供者(MSP)定义中获取它们的组织身份。
有关身份和 MSP 的更多信息,请查看我们关于身份和成员的文档。
与 Peer 节点一样,排序节点属于组织。也应该像 Peer 节点一样为每个组织使用单独的证书授权中心(CA)。这个 CA 是否将作为根 CA 发挥作用,或者您是否选择部署根 CA,然后部署与该根 CA 关联的中间 CA,这取决于您。
排序节点和交易流程¶
阶段一:提案¶
从我们对 Peer 节点的讨论中,我们已经看到它们构成了区块链网络的基础,托管账本,应用程序可以通过智能合约查询和更新这些账本。
具体来说,更新账本的应用程序涉及到三个阶段,该过程确保区块链网络中的所有节点保持它们的账本彼此一致。
在第一阶段,客户端应用程序将交易提案发送给一组节点,这些节点将调用智能合约来生成一个账本更新提案,然后背书该结果。背书节点此时不将提案中的更新应用于其账本副本。相反,背书节点将向客户端应用程序返回一个提案响应。已背书的交易提案最终将在第二阶段经过排序生成区块,然后在第三阶段分发给所有节点进行最终验证和提交。
要深入了解第一个阶段,请参阅节点主题。
阶段二:将交易排序并打包到区块中¶
在完成交易的第一阶段之后,客户端应用程序已经从一组节点接收到一个经过背书的交易提案响应。现在是交易的第二阶段。
在此阶段,应用程序客户端把包含已背书交易提案响应的交易提交到排序服务节点。排序服务创建交易区块,这些交易区块最终将分发给通道上的所有 Peer 节点,以便在第三阶段进行最终验证和提交。
排序服务节点同时接收来自许多不同应用程序客户端的交易。这些排序服务节点一起工作,共同组成排序服务。它的工作是将提交的交易按定义好的顺序安排成批次,并将它们打包成区块。这些区块将成为区块链的区块!
区块中的交易数量取决于区块的期望大小和最大间隔时间相关的通道配置参数(确切地说,是 BatchSize
和 BatchTimeout
参数)。然后将这些区块保存到排序节点的账本中,并分发给已经加入通道的所有节点。如果此时恰好有一个 Peer 节点关闭,或者稍后加入通道,它将在重新连接到排序服务节点或与另一个 Peer 节点通信之后接收到这些区块。我们将在第三阶段看到节点如何处理这个区块。
Orderer1
排序节点的第一个角色是打包提案的账本更新。在本例中,应用程序 A1 向排序节点 O1 发送由 E1 和 E2 背书的交易 T1。同时,应用程序 A2 将 E1 背书的交易 T2 发送给排序节点 O1。O1 将来自应用程序 A1 的交易 T1 和来自应用程序 A2 的交易 T2 以及来自网络中其他应用程序的交易打包到区块 B2 中。我们可以看到,在 B2 中,交易顺序是 T1、T2、T3、T4、T6、T5,但这可能不是这些交易到达排序节点的顺序!(这个例子显示了一个非常简单的排序服务配置,只有一个排序节点。)
值得注意的是,一个区块中交易的顺序不一定与排序服务接收的顺序相同,因为可能有多个排序服务节点几乎同时接收交易。重要的是,排序服务将交易放入严格的顺序中,并且 Peer 节点在验证和提交交易时将使用这个顺序。
区块内交易的严格排序使得 Hyperledger Fabric 与其他区块链稍有不同,在其他区块链中,相同的交易可以被打包成多个不同的区块,从而形成一个链。在 Hyperledger Fabric 中,由排序服务生成的区块是最终的。一旦一笔交易被写进一个区块,它在账本中的地位就得到了保证。正如我们前面所说,Hyperledger Fabric 的最终性意味着没有账本分叉,也就是说,经过验证的交易永远不会被重写或删除。
我们还可以看到,虽然节 Peer 点执行智能合约并处理交易,而排序节点不会这样做。到达排序节点的每个授权交易都被机械地打包在一个区块中,排序节点不判断交易的内容(前面提到的通道配置交易除外)。
在第二阶段的最后,我们看到排序节点负责一些简单但重要的过程,包括收集已提案的交易更新、排序并将它们打包成区块、准备分发。
阶段三:验证和提交¶
交易工作流的第三个阶段涉及到从排序节点到 Peer 节点的区块的分发和随后的验证,这些区块可能会被应用到账本中。
第三阶段排序节点将区块分发给连接到它的所有 Peer 节点开始。同样值得注意的是,并不是每个 Peer 节点都需要连接到一个排序节点,Peer 节点可以使用 gossip 协议将区块关联到其他节点。
每个节点将独立地以确定的方式验证区块,以确保账本保持一致。具体来说,通道中每个节点都将验证区块中的每个交易,以确保得到了所需组织的节点背书,也就是节点的背书和背书策略相匹配,并且不会因最初认可该事务时可能正在运行的其他最近提交的事务而失效。无效的交易仍然保留在排序节点创建的区块中,但是节点将它们标记为无效,并且不更新账本的状态。
Orderer2
排序节点的第二个角色是将区块分发给 Peer 节点。在本例中,排序节点 O1 将区块 B2 分配给节点 P1 和 P2。节点 P1 处理区块 B2,在 P1 上的账本 L1 中添加一个新区块。同时,节点 P2 处理区块 B2,从而将一个新区块添加到 P2 上的账本 L1中。一旦这个过程完成,节点 P1 和 P2 上的账本 L1 就会保持一致的更新,并且每个节点都可以通知与之连接的应用程序交易已经被处理。
总之,第三阶段看到的是由排序服务生成的区块一致地应用于账本。将交易严格地按区块排序,允许每个节点验证交易更新是否在整个区块链网络上一致地应用。
要更深入地了解阶段三,请参阅节点主题。
排序服务实现¶
虽然当前可用的每个排序服务都以相同的方式处理交易和配置更新,但是仍然有几种不同的实现可以在排序服务节点之间就严格的交易排序达成共识。
有关如何建立排序节点(无论该节点将在什么实现中使用)的信息,请参阅关于建立排序节点的文档。
Solo
排序服务的 Solo 实现就像它的名字一样:它只具有一个排序节点。因此,它不是,也永远不会是容错的。出于这个原因,Solo 实现不能用于生产环境,但对于测试应用程序和智能合约,或者创建概念证明来说,它是一个很好的选择。但是,如果您曾经希望将这个 PoC 网络扩展到生产环境中,那么您可能希望从单个节点 Raft 集群开始,因为它可以通过重新配置来添加其他节点。
Raft
作为v1.4.1的新特性,Raft 是一种基于
etcd
中 Raft 协议实现的崩溃容错(Crash Fault Tolerant,CFT)排序服务。Raft 遵循“领导者跟随者”模型,这个模型中,在每个通道上选举领导者节点,其决策被跟随者复制。Raft 排序服务会比基于 Kafka 的排序服务更容易设置和管理,它的设计允许不同的组织为分布式排序服务贡献节点。Kafka
和基于 Raft 的排序类似,Apache Kafka 是一个 CFT 的实现,它使用“领导者和跟随者”节点配置。Kafka 利用一个 ZooKeeper 进行管理。基于 Kafka 的排序服务从 Fabric v1.0开始就可以使用,但许多用户可能会发现管理 Kafka 集群的额外管理开销令人生畏或不受欢迎。
Solo¶
如上所述,在开发测试、开发或概念验证网络时,单独排序服务是一个不错的选择。出于这个原因,它是部署在我们构建第一个网络教程中默认的排序服务,从其他网络组件的角度来看, Solo 排序服务处理交易和更复杂的 Kafka 和 Raft 实现相同,同时节省了维护和升级多个节点和集群的管理开销。由于 Solo 排序服务不能容错,因此永远不应该认为它是生产区块链网络的可行替代方案。对于只希望从 Solo 排序节点开始但将来可能希望增长的网络,单节点 Raft 集群是更好的选择。
Raft¶
有关如何配置 Raft 排序服务的信息,请参阅有关配置 Raft 排序服务的文档。
对于将用于生产网络的排序服务,Fabric 实现了使用“领导者跟随者”模型的 Raft 协议,领导者是在一个通道的排序节点中动态选择的(这个集合的节点称为“批准者集合(consenter set)”),领导者将信息复制到跟随者节点。Raft 被称为“崩溃容错”是因为系统可以承受节点的损失,包括领导者节点,前提是要剩余大量的排序节点(称为“法定人数(quorum)”)。换句话说,如果一个通道中有三个节点,它可以承受一个节点的丢失(剩下两个节点)。如果一个通道中有五个节点,则可以丢失两个节点(剩下三个节点)。
从它们提供给网络或通道的服务的角度来看,Raft 和现有的基于 Kafka 的排序服务(我们将在稍后讨论)是相似的。它们都是使用领导者跟随者模型设计的 CFT 排序服务。如果您是应用程序开发人员、智能合约开发人员或节点管理员,您不会注意到基于 Raft 和 Kafka 的排序服务之间的功能差异。然而,有几个主要的差异值得考虑,特别是如果你打算管理一个排序服务:
- Raft 更容易设置。虽然 Kafka 有很多崇拜者,但即使是那些崇拜者也(通常)会承认部署 Kafka 集群及其 ZooKeeper 集群会很棘手,需要在 Kafka 基础设施和设置方面拥有高水平的专业知识。此外,使用 Kafka 管理的组件比使用 Raft 管理的组件多,这意味着有更多的地方会出现问题。Kafka 有自己的版本,必须与排序节点协调。使用 Raft,所有内容都会嵌入到您的排序节点中。
- Kafka 和 Zookeeper 并不是为了在大型网络上运行。它们被设计为 CFT,但应该在一组紧密的主机中运行。这意味着实际上,您需要有一个组织运行 Kafka 集群。考虑到这一点,在使用 Kafka 时,让不同组织运行排序节点不会给您带来太多的分散性,因为这些节点都将进入同一个由单个组织控制的 Kafka 集群。使用 Raft,每个组织都可以有自己的排序节点参与排序服务,从而形成一个更加分散的系统。
- Raft 是原生支持的。虽然基于 Kafka 的排序服务目前与 Fabric 兼容,但用户需要获得相关的镜像,并学习如何单独使用 Kafka 和 ZooKeeper。同样,对 Kafka 相关问题的支持是通过 Apache 来处理的,Apache 是 Kafka 的开源开发者,而不是 Hyperledge Fabric。另一方面,Fabric Raft 的实现已经开发出来了,并将在 Fabric 开发人员社区及其支持设备中得到支持。
- Kafka 使用一个服务器池(称为“Kafka 代理”),而且排序组织的管理员要指定在特定通道上使用多少个节点,但是 Raft 允许用户指定哪个排序节点要部署到哪个通道。通过这种方式,节点组织可以确保如果他们也拥有一个排序节点,那么这个节点将成为该通道的排序服务的一部分,而不是信任并依赖一个中心来管理 Kafka 节点。
- Raft 是向开发拜占庭容错(BFT)排序服务迈出的第一步。正如我们将看到的,Fabric 开发中的一些决策是由这个驱动的。如果你对 BFT 感兴趣,学习如何使用 Raft 应该可以慢慢过渡。
注意:与 Solo 和 Kafka 类似,在向客户发送回执后 Raft 排序服务也可能会丢失交易。例如,如果领导者和跟随者提供回执时同时崩溃。因此,应用程序客户端应该监听节点上的交易提交事件,而不是检查交易的有效性。但是应该格外小心,要确保客户机也能优雅地容忍在配置的时间内没有交易提交超时。根据应用程序的不同,在这种超时情况下可能需要重新提交交易或收集一组新的背书。
Raft 概念¶
虽然 Raft 提供了许多与 Kafka 相同的功能(尽管它是一个简单易用的软件包)但它与 Kafka 的功能却大不相同,它向 Fabric 引入了许多新的概念,或改变了现有的概念。
日志条目(Log entry)。 Raft 排序服务中的主要工作单元是一个“日志条目”,该项的完整序列称为“日志”。如果大多数成员(换句话说是一个法定人数)同意条目及其顺序,则我们认为条目是一致的,然后将日志复制到不同排序节点上。
批准者集合(Consenter set)。主动参与给定通道的共识机制并接收该通道的日志副本的排序节点。这可以是所有可用的节点(在单个集群中或在多个集群中为系统通道提供服务),也可以是这些节点的一个子集。
有限状态机(Finite-State Machine,FSM)。Raft 中的每个排序节点都有一个 FSM,它们共同用于确保各个排序节点中的日志序列是确定(以相同的顺序编写)。
法定人数(Quorum)。描述需要确认提案的最小同意人数。对于每个批准者集合,这是大多数节点。在具有五个节点的集群中,必须有三个节点可用,才能有一个法定人数。如果节点的法定人数因任何原因不可用,则排序服务集群对于通道上的读和写操作都不可用,并且不能提交任何新日志。
领导者(Leader)。这并不是一个新概念,正如我们所说,Kafka 也使用了领导者,但是在任何给定的时间,通道的批准者集合都选择一个节点作为领导者,这一点非常重要(我们稍后将在 Raft 中描述这是如何发生的)。领导者负责接收新的日志条目,将它们复制到跟随者的排序节点,并在认为提交了某个条目时进行管理。这不是一种特殊类型的排序节点。它只是排序节点在某些时候可能扮演的角色,而不是由客观环境决定的其他角色。
跟随者(Follower)。再次强调,这不是一个新概念,但是理解跟随者的关键是跟随者从领导者那里接收日志并复制它们,确保日志保持一致。我们将在关于领导者选举的部分中看到,跟随者也会收到来自领导者的“心跳”消息。如果领导者在一段可配置的时间内停止发送这些消息,跟随者将发起一次领导者选举,它们中的一个将当选为新的领导者。
交易流程中的 Raft¶
每个通道都在 Raft 协议的单独实例上运行,该协议允许每个实例选择不同的领导者。这种配置还允许在集群由不同组织控制的排序节点组成的用例中进一步分散服务。虽然所有 Raft 节点都必须是系统通道的一部分,但它们不一定必须是所有应用程序通道的一部分。通道创建者(和通道管理员)能够选择可用排序节点的子集,并根据需要添加或删除排序节点(只要一次只添加或删除一个节点)。
虽然这种配置以冗余心跳消息和线程的形式产生了更多的开销,但它为 BFT 奠定了必要的基础。
在 Raft 中,交易(以提案或配置更新的形式)由接收交易的排序节点自动路由到该通道的当前领导者。这意味着 Peer 节点和应用程序在任何特定时间都不需要知道谁是领导者节点。只有排序节点需要知道。
当排序节点检查完成后,将按照我们交易流程的第二阶段的描述,对交易进行排序、打包成区块、协商并分发。
架构说明¶
Raft 是如何选举领导者的¶
尽管选举领导者的过程发生在排序节点的内部过程中,但是值得注意一下这个过程是如何工作的。
节点总是处于以下三种状态之一:跟随者、候选人或领导者。所有节点最初都是作为跟随者开始的。在这种状态下,他们可以接受来自领导者的日志条目(如果其中一个已经当选),或者为领导者投票。如果在一段时间内没有接收到日志条目或心跳(例如,5秒),节点将自己提升到候选状态。在候选状态中,节点从其他节点请求选票。如果候选人获得法定人数的选票,那么他就被提升为领导者。领导者必须接受新的日志条目并将其复制到跟随者。
要了解领导者选举过程的可视化表示,请查看数据的秘密生活。
快照¶
如果一个排序节点宕机,它如何在重新启动时获得它丢失的日志?
虽然可以无限期地保留所有日志,但是为了节省磁盘空间,Raft 使用了一个称为“快照”的过程,在这个过程中,用户可以定义日志中要保留多少字节的数据。这个数据量将符合一定数量的区块(这取决于区块中的数据量。注意,快照中只存储完整的区块)。
例如,假设滞后副本 R1
刚刚重新连接到网络。它最新的区块是100
。领导者 L
位于第 196
块,并被配置为快照20个区块。R1
因此将从 L
接收区块180
,然后为区块101
到180
区块发送
请求。然后180
到196
的区块将通过正常 Raft 协议复制到 R1
。
Kafka¶
Fabric 支持的另一个容错崩溃排序服务是对 Kafka 分布式流平台的改写,将其用作排序节点集群。您可以在 Apache Kafka 网站上阅读更多关于 Kafka 的信息,但是在更高的层次上,Kafka 使用与 Raft 相同概念上的“领导者跟随者”配置,其中交易(Kafka 称之为“消息”)从领导者节点复制到跟随者节点。就像 Raft 一样,在领导者节点宕机的情况下,一个跟随者成为领导者,排序可以继续,以此来确保容错。
Kafka 集群的管理,包括任务协调、集群成员、访问控制和控制器选择等,由 ZooKeeper 集合及其相关 API 来处理。
Kafka 集群和 ZooKeeper 集合的设置是出了名的棘手,所以我们的文档假设您对 Kafka 和 ZooKeeper 有一定的了解。如果您决定在不具备此专业知识的情况下使用 Kafka,那么在试验基于 Kafka 的排序服务之前,至少应该完成 Kafka 快速入门指南的前六个步骤。您还可以参考这个示例配置文件来简要解释 Kafka 和 ZooKeeper 的合理默认值。
要了解如何启动基于 Kafka 的排序服务,请查看我们关于 Kafka 的文档。
私有数据¶
什么是私有数据?¶
在某个通道上的一组组织需要对该通道上的其他组织保持数据私密的情况下,它们可以选择创建一个新通道,其中只包含需要访问数据的组织。但是,在每一种情况下创建单独的通道会产生额外的管理开销(维护链码版本、策略、MSP 等),并且不允许在保持部分数据私有的同时,让所有通道参与者都看到交易。
这就是为什么从v1.2开始,Fabric就提供了创建私有数据集合的能力,这使得通道上定义的组织子集能够背书、提交或查询私有数据,而不必创建单独的通道。
什么是私有数据集?¶
集合是两个元素的组合:
- 实际的私有数据,通过 Gossip 协议点对点地发送给授权可以看到它的组织。私有数据保存在被授权的组织的节点上的私有数据库上(有时候成为“侧”数据库或者“SideDB”),它们可以被授权节点的链码访问。排序节点不能影响这里也不能看到私有数据。注意,由于 gossip 以点对点的方式向授权组织分发私有数据,所以必须设置通道上的锚节点,也就是每个节点上的 CORE_PEER_GOSSIP_EXTERNALENDPOINT 配置,以此来引导跨组织的通信。
- 该数据的哈希值,该哈希值被背书、排序之后写入通道上每个节点的账本。哈希值作为交易的证明用于状态验证,还可用于审计。
下图说明了一个被授权拥有私有数据的节点和一个未授权的节点的账本内容。
private-data.private-data
如果集合成员陷入争议,或者如果他们想将资产转让给第三方,则可以决定与其他方共享私有数据。然后,第三方可以计算私有数据的哈希,并查看它是否与通道账本上的状态匹配,从而证明在某个时间点上,集合成员之间存在该状态。
在通道内什么时候使用私有数据集,什么时候使用单独通道¶
- 当通道成员的一群组织必须对所有交易(和账本)保密时,使用通道。
- 当交易(和账本)必须在一群组织间共享,并且这些组织中只有一部分组织可以访问交易中一些(或全部)私有数据时,使用集合。此外,由于私有数据是点对点传播的,而不是通过区块传播,所以当交易数据必须对排序服务节点保密时,应该使用私有数据集合。
解释集合的一个案例¶
设想一个通道上有五个交易农产品的组织:
- 农民把产品销往国外
- 分销商将货物运往国外
- 托运商负责参与方之间的货物运输
- 批发商从分销商那里采购商品
- 零售商从托运人和批发商那里采购商品
分销商可能希望与农民和托运商之间保持私密交易,从而使得批发商和零售商无从得知这些交易条款(以免暴露他们加价的事实)。
分销商还可能希望与批发商建立单独的私有数据关系,因为它向批发商收取的价格比向零售商收取的价格低。
批发商还可能希望与零售商和托运商建立私有数据关系。
与其为这些私有数据关系定义许多小通道,不如定义多个私有数据集合(PDC)在以下各方之间共享私有数据:
- PDC1:分销商,农民和托运商
- PDC2:分销商和批发商
- PDC3:批发商、零售商和托运商
private-data.private-data
拿上述例子来说,分销商拥有的节点在其账本储存了多个私有数据库,这些数据库中包含了来自分销商、农民和托运商关系以及分销商和批发商关系的私有数据。
private-data.private-data
私有数据的交易流程¶
当在链码中引用私有数据集合时,为了保护私有数据在提案、背书和提交交易过程中的机密性,私有数据的交易流程略有不同。
关于不使用私有数据的交易流程的详细信息,请参阅我们关于交易流程的文档。
- 客户端应用程序向背书节点提交一项提案,请求调用一个链码函数(读取或写入私有数据),这些背书节点属于私有数据集合中已授权组织的一部分。私有数据,或用于在链码中生成私有数据的数据,被发送到提案的
transient
字段中。 - 背书节点模拟交易,并将私有数据存储在
transient data store
(节点的本地临时存储库)中。这些背书节点根据组织集合的策略将私有数据通过 gossip 分发给授权的节点。 - 背书节点将提案响应发送回客户端。提案响应中包含经过背书的读写集,这其中包含了公共数据,还包含任何私有数据键和值的哈希。私有数据不会被发送回客户端。更多关于带有私有数据的背书的信息,请查看这里。
- 客户端应用程序将交易(包含带有私有数据哈希值的提案响应)提交给排序服务。带有私有数据哈希的交易同样被包含在区块中。带有私有数据哈希的区块被分发给所有节点。这样,通道中的所有节点就可以在不知道真实私有数据的情况下,用同样的方式来验证带有私有数据哈希值的交易。
- 在区块提交的时候,授权的节点会根据集合策略来决定它们是否有权访问私有数据。如果节点有访问权,它们会先检查自己的本地
transient data store
,以确定它们是否在链码背书的时候已经接收到了私有数据。如果没有的话,它们会尝试从其他已授权节点那里拉取私有数据,然后对照公共区块上的哈希值来验证私有数据并提交交易和区块。当验证或提交结束后,私有数据会被移动到这些节点私有数据库和私有读写存储的副本中。随后transient data store
中存储的这些私有数据会被删除。
共享私有数据¶
在多数情况下,一个私有数据集合中的私有数据键或值可能需要与其他通道成员或者私有数据集合共享,例如,当你需要和一个通道成员或者一组通道成员交易私有数据,而初始私有数据集合中并没有这些成员时。私有数据的接收方一般都会在交易过程中对照链上的哈希值来验证私有数据。
私有数据集合的以下几个方面促成了私有数据的共享和验证:
- 首先,只要符合背书策略,尽管你不是私有数据集的成员也可以为集合写入键。可以在链码层面,键层面(用基于状态的背书)或者集合层面(始于 Fabric v2.0)上定义背书策略。
- 其次,从 v1.4.2 开始出现了链码 API(应用程序编程接口) GetPrivateDataHash() ,它支持非集合成员节点上的链码读取一个私有键的哈希值。下文中你将发现这是一个十分重要的功能,因为它允许链码对照之前交易中私有数据生成的链上哈希来验证私有数据。
当设计应用程序和相关私有数据集合时需要考虑这种共享和验证私有数据的能力。您当然可以创建出几组多边私有数据集合以供多个通道成员组合之间共享数据,但是这样做的话你就需要定义大量私有数据集合。或者你也可以考虑使用少量私有数据集合(例如,每个组织用一个集合,或者每对组织用一个集合),然后和其他通道成员共享私有数据,有需要时还可以和其他集合共享私有数据。从 Fabric v2.0 开始,隐含的组织特定集合可供所有链码使用,这样一来部署链码的时候你就不用定义每个组织中的私有数据集合。
私有数据共享模式¶
为各组织的私有数据集合构建模型时,有多种模式可被用来共享或传输私有数据,且无需费力定义多个多边集合。以下是链码应用程序中可以使用的一些共享模式:
- 使用相应的公钥来追踪公共状态:您可以选择使用一个相应的公钥来追踪特定的公共状态(例如:资产性质,当前所有权等公共状态),对于每个需要拥有资产相应私有数据访问权的组织,您可以在它们的私有数据集合中创建一个私有秘钥或值。
- 链码访问控制:您可以在您的链码中执行访问控制,指明什么客户端应用程序能够查询私有数据集合中的私有数据。例如,为一个或多个私有数据集合键存储一个访问控制列表,然后在链码中获取客户端应用程序提交者的证书(使用 GetCreator() chaincode API or CID library API GetID() or GetMSPID()来获取),并在返回私有数据之前验证这些证书。同样,为了访问私有数据,您可以要求客户端将密码短语传送到链码中,且该短语必须与存储在秘钥级别的密码短语相匹配。注意,这种模式也可用于限制客户端对公共状态数据的访问权。
- 使用带外数据来共享私有数据:这是一种链下选项,您可以同其他组织以带外数据的形式共享私有数据,这些组织可使用 GetPrivateDataHash() 链码 API 将键或值转换成哈希以验证其是否与链上哈希匹配。例如,如果一个组织想要购买一份你的资产,那么在同意购买之前它会检查链上哈希,以验证如下事项:该资产的属性;你是否为该资产的合法所有人。
- 与其他集合共享私有数据:您可以同链码在链上共享私有数据,该链码在其他组织的私有数据集合中生成一个相应的键或值。你将通过临时字段把私有数据键或值传送给链码,收到私有数据后该链码使用 GetPrivateDataHash() 验证此私有数据的一个哈希是否与您集合中的链上哈希一致,随后将该私有数据写入其他组织的私有数据集合中。
- 将私有数据传送给其他集合:您可以使用链码来”传送“私有数据,该链码把您集合中的私有数据键删除,然后在其他组织的集合中生成。与上述方法相同,这里在调用链码时使用临时字段传输私有数据,并且在链码中用 GetPrivateDataHash() 来验证你的私有数据集合中是否存在该数据,验证成功之后再把你的集合中该数据删除,并在其他组织的集合中生成该键。为确保每次操作都会从一个集合中删除一项交易并在另一个集合中添加该交易,你可能需要获得一些额外组织的背书,如监管者或审计者。
- 使用私有数据进行交易确认:如果您想在交易完成之前获得竞争对手的批准(即竞争对手同意以某价钱购买一项资产这一链上记录),链码会在竞争对手或您自己的私有数据集合中写入一个私有数据(链码随后将使用 GetPrivateDataHash() 来验证该数据),从而要求竞争对手”率先批准“这项交易。事实上,嵌入式生命周期系统链码就是使用这种机制来确保链码定义在被提交到通道之前已经获得通道上各组织的同意。这种私有数据共享模式从从 Fabric v2.0 开始凭借集合层面的背书策略变得越来越强大,确保在集合拥有者自己的信任节点上执行、背书链码。或者,您也可以使用具有秘钥层面背书策略、共同商定的秘钥,随后该秘钥被预先批准的条款更新,并且在指定组织的节点上获得背书。
- 使交易者保密:对上一种共享模式进行些许变化还可以实现指定交易的交易者不被暴露。例如,买方表示同意在自己的私有数据集合上进行购买,随后在接下来的交易中卖方会在自己的私有数据集合中引用该买方的私有数据。
结合以上几种模式,我们可以发现,私有数据的交易和普通的通道状态数据交易情况类似,特别是以下几点:
- 重要级别交易访问控制:您可以在私有数据值中加入 所有权证书,这样一来,后续发生的交易就能够验证数据提交者是否具有共享和传输数据的所有权。在这种情况下,链码会获取数据提交者的证书(例如:使用 GetCreator() chaincode API or CID library API GetID() or GetMSPID() ),将此证书与链码收到的其他私有数据合并,
- 重要级别背书策略:和正常的通道状态数据一样,您可以使用基于状态的背书策略来指明哪些组织必须对共享或转移私有数据的交易做出背书,使用 SetPrivateDataValidationParameter() chaincode API 来进行一些操作,例如,指明必须对上述交易作出背书的仅包括一个拥有者的组织节点,托管组织的节点或者第三方组织。
私有数据共享案例¶
将上述私有数据共享模式结合起来能够赋能基于链码的应用程序。例如,思考一下如何使用各组织私有数据集合来实现资产转移场景:
- 在公共链码状态使用UUID键可以追踪一项资产。所记录信息只包括资产的所属权,没有其他信息。
- 链码要求:所有资产转移请求必须源自拥有者客户端,并且该键是基于
- 资产拥有者的私有数据集合包含了有关该资产的私有信息,
- 假定管理员是各私有数据集合的一员,因此它一直维护着私有数据,即使他并没必要这么做。
一项资产交易的进行情况如下:
- 链下,资产所有者和意向买家同意以某一特定价格交易该资产。
- 卖家通过以下方式提供资产所有权证明:利用带外数据传输私有信息;或者出示证书以供买家在其自身节点或管理员节点上查询私有数据。
- 买家验证私有信息的哈希是否匹配链上公共哈希。
- 买家通过调取链码来在其自身私有数据集合中记录投标细节信息。一般在买家自己的节点上调取链码,但如果集合背书策略有相关规定,则也可能会在管理员节点上调取链码。
- 当前的资产所有者(卖家)调取链码来卖出、转移资产,传递私有信息和投标信息。在卖家、买家和管理员的节点上调用链码,以满足公钥的背书策略以及买家和买家私有数据集合的背书策略。
- 链码验证交易发送方是否为资产拥有者,根据卖家私有数据集合中的哈希来验证私有细节信息,同时还会根据买家私有数据集合中的哈希来验证投标细节信息。随后链码为公钥书写提案更新(将资产拥有者设定为买家,将背书策略设定为买家和管理员),把私有细节信息写入买家的私有数据集合中,以上步骤成功完成后链码会把卖家私有数据集合中的相关私有细节信息删除。在最终背书之前,背书节点会确保已将私有数据分布给卖家和管理员的所有授权节点。
- 卖家提交交易等待排序,其中包括了公钥和私钥哈希,随后该交易被分布到区块中的所有通道节点上。
- 每个节点的区块验证逻辑都将一致性地验证背书策略是否得到满足(买家、卖家和管理员都作出背书),同时还验证链码中已读取的公钥和私钥自链码执行以来未被任何其他交易更改。
- 所有节点提交的交易都是有效的,因为交易已经通过验证。如果买家和管理员节点在背书时未收到私有数据的话,它们将会从其他授权节点那里获取这些私有数据,并将这些数据保存在自己的私有数据状态数据库中(假定私有数据与交易的哈希匹配)。
- 交易完成后,资产被成功转移,其他对该资产感兴趣的通道成员可能会查询公钥的历史以了解该资产的来历,但是它们无法访问任何私有细节信息,除非资产拥有者在须知的基础上共享这些信息。
以上是最基本的资产转移场景,我么可以对其进行扩展,例如,资产转移链码可能会验证一项付款记录是否可用于满足付款和交付要求,或者可能会验证一个银行是否在执行资产转移链码之前已经提交了信用证。交易各方并不直接维护节点,而是通过那些运行节点的托管组织来进行交易。
清除私有数据¶
对于非常敏感的数据,即使是共享私有数据的各方可能也希望(或者应政府相关法规要求必须)定期“清除”节点上的数据,仅把这些敏感数据的哈希留在区块链上,作为私有数据不可篡改的证据。
在某些情况下,私有数据只需要在其被复制到节点区块链外部的数据库之前存在于该节点的私有数据库中。而数据或许也只需要在链码业务流程结束之前存在于节点上(交易结算、合约履行等)。
为了支持这些用户案例,如果私有数据已经持续 N 个块都没有被修改,则可以清除该私有数据,N 是可配置的。链码中无法查询已被清除的私有数据,并且其他节点也请求不到。
入门¶
准备阶段¶
在我们开始之前,如果您还没有这样做,您可能希望检查以下所有先决条件是否已安装在您将开发区块链应用程序或运行 Hyperledger Fabric 的平台上。
安装 cURL¶
如果尚未安装cURl或在服务器上运行文档中的curl命令出错时请下载最新版本的 cURL 工具。
注解
如果您使用的是Windows,请参阅下面有关Windows附加功能( `Windows extras`_ )的特定说明。
Docker 和 Docker Compose¶
您将需要在将要运行或基于Hyperledger Fabric开发(或开发Hyperledger Fabric)的平台上安装以下内容:
- MacOSX,* nix或Windows 10: 要求 Docker Docker版本17.06.2-ce及以上。
- 较旧版本的Windows:Docker Toolbox - 要求Docker版本Docker 17.06.2-ce及以上。
您可以通过执行以下终端提示符中的命令来检查已安装的Docker的版本:
docker --version
注解
在Mac或Windows以及Docker Toolbox中安装Docker,也会安装 Docker Compose。 如果您已安装 Docker,则应检查是否已安装 Docker Compose版本1.14.0或更高版本。如果没有,我们建议您安装最新版本的Docker。
您可以通过执行以下终端提示符中的命令来检查已安装的Docker Compose的版本:
docker-compose --version
Go 语言¶
Hyperledger Fabric许多组件使用Go编程语言开发。
- Go 要求 1.11.x 版本.
鉴于我们将在Go中编写链码程序,您需要正确设置两个环境变量; 您可以将这些设置永久保存在相应的启动文件中,例如您的个人 ~/.bashrc
文件(如果您在Linux下使用 bash
shell)。
首先,您必须将环境变量 GOPATH
设置为指向包含下载的Fabric代码库的Go工作空间,例如:
export GOPATH=$HOME/go
注解
您 必须 设置GOPATH变量
即使在Linux中,Go的 GOPATH
变量可以是以冒号分隔的目录列表,如果未设置,将使用默认值 $HOME/go
,当前的Fabric构建框架仍然需要您设置和输出该变量,它必须 只 包含Go工作区的单个目录名称。(此限制可能会在将来的版本中删除。)
其次,您应该(再次,在适当的启动文件中)扩展您的命令搜索路径以包含Go ``bin``目录,例如下面是Linux下的``bash``示例:
export PATH=$PATH:$GOPATH/bin
虽然此目录可能不存在于新安装的Go工作区中,但稍后由Fabric构建系统填充,其中构建系统的其他部分使用少量Go可执行文件。因此,即使您目前还没有此类目录,也可以像上面那样扩展shell搜索路径。
Node.js运行环境及NPM¶
如果你将用Node.js的Hyperledger Fabric SDK开发Hyperledger Fabric的应用程序,则需安装Node.js的8.9.x版本.
- Node.js 下载
注解
安装Node.js也会安装NPM,但建议您确认安装的NPM版本。 您可以使用以下命令升级 npm
工具:
npm install npm@5.6.0 -g
Python¶
注解
以下内容仅适用于Ubuntu 16.04用户。
默认情况下,Ubuntu 16.04附带了Python 3.5.1安装的 python3
二进制文件。Fabric Node.js SDK需要使用
Python 2.7版本才能成功完成 npm install
操作。使用以下命令安装2.7版本:
sudo apt-get install python
检查您的版本:
python --version
Windows附加功能¶
如果您在Windows 7上进行开发,则需要在使用 Git Bash 的Docker Quickstart终端中工作,它是一个比内置Windows shel更好的替代方案。
然而,经验表明这是一个功能有限的糟糕开发环境。它适合运行基于Docker的场景,如 入门,但你可能在操作包括``make``和 ``docker``命令时出现问题。
在Windows 10上,你应该使用本地Docker发行版,并且可以使用Windows PowerShell。但是你仍需要可用的 uname
命令以便成功运行 binaries
命令。
在运行任何``git clone``命令前,运行如下命令:
git config --global core.autocrlf false
git config --global core.longpaths true
你可以通过如下命令检查这些参数的设置:
git config --get core.autocrlf
git config --get core.longpaths
它们必须分别是false和true
Git和Docker Toolbox附带的 curl
命令很旧,无法正确处理 :doc:`getting_started`中使用的重定向。
因此要确保你从 `cURL downloads page <https://curl.haxx.se/download.html>`__ 安装并使用的是较新版本。
对于Node.js,你还需要必需的Visual Studio C ++构建工具,它是免费可用的并且可以使用以下命令进行安装:
npm install --global windows-build-tools
有关更多详细信息,请参阅 NPM windows-build-tools 页面 。
完成此操作后,还应使用以下命令安装NPM GRPC模块:
npm install --global grpc
你的环境现在应该已准备好实现 入门 中的示例和教程。
注解
如果你有本文档未解决的问题,或遇到任何有关教程的问题,请访问 还有问题? 页面,获取有关在何处寻求其他帮助的一些提示。
安装示例、二进制文件和 Docker 镜像¶
在我们为 Hyperledger Fabric 二进制文件开发真正的安装程序时,我们提供了一个脚本,可以下载并安装示例和二进制文件到你的系统。我们认为你会发现安装的示例应用程序可以帮助去了解 Hyperledger Fabric有关的功能和运维的更多信息。
注解
如果你在 Windows 上运行样例,则需要使用 Docker 快速启动终端来执行即将发布的终端命令。如果你之前没有安装,请访问 准备阶段 。
如果你在 Windows 7或 macOS 上使用 Docker Toolbox, 你在安装和运行示例需要使用到 C:\Users
(Windows 7) 或 /Users
(macOS)目录下的路径。
如果你在 Mac 上使用 Docker,则需要使用到 /Users
、 /Volumes
、 /private
、 或 /tmp
这些目录下的路径。要使用其他路径,请参阅 Docker 文档以获取 文件共享 。
如果你在 Windows 下使用 Docker,请参阅 Docker 文档以获取 共享驱动器 ,并使用其中一个共享驱动器下的路径。
确定计算机上要放置 fabric-samples 仓存的位置,并在终端窗口中进入该目录。 按以下步骤执行相关命令:
- 如果需要,克隆 hyperledger/fabric-samples 仓库
- 检出适当的版本标签
- 将指定版本的 Hyperledger Fabric 平台特定二进制文件和配置文件安装到 fabric-samples 下的 /bin 和 /config 目录中
- 下载指定版本的 Hyperledger Fabric docker 镜像
一旦你准备好了,在要安装 Fabric Samples 和二进制文件的目录中,继续执行以下命令:
注解
如果你想使用最新的发布版本,请省略版本号。
curl -sSL http://bit.ly/2ysbOFE | bash -s
注解
如果你想使用特定的发布版本,请加一个 Fabric、Fabric-ca 和第三方 Docker 镜像的版本号,下边的命令演示了如何下载 Fabric v1.4.2 。
curl -sSL http://bit.ly/2ysbOFE | bash -s -- <fabric_version> <fabric-ca_version> <thirdparty_version>
curl -sSL http://bit.ly/2ysbOFE | bash -s -- 1.4.2 1.4.2 0.4.15
注解
如果运行上述curl命令时出错,则可能是旧版本的 curl 不能处理重定向或环境不支持。请访问 准备阶段 页面获取有有关在哪里可以找到最新版的 curl 并获得正确环境的其他信息。或者,你可以访问完整的 URL: https://raw.githubusercontent.com/hyperledger/fabric/master/scripts/bootstrap.sh
上面的命令下载并执行一个 bash 脚本,该脚本将下载并提取设置网络所需的所有特定于平台的二进制文件,并将它们放入你在上面创建的克隆仓库中。它检索以下特定平台的二进制文件:
configtxgen
,configtxlator
,cryptogen
,discover
,idemixgen
orderer
,peer
, andfabric-ca-client
并将它们放在当前工作目录的子目录 bin
中。
你可能希望将其添加到 PATH 环境变量中,以便在不需要指定每个二进制文件的绝对路径的情况下获取这些命令。例如:
export PATH=<path to download location>/bin:$PATH
最后,该脚本会将从 Docker Hub 上下载 Hyperledger Fabric docker 镜像到本地 Docker 注册表中,并将其标记为 ‘latest’。
该脚本列出了结束时安装的 Docker 镜像。
查看每个镜像的名称;这些组件最终将构成我们的 Hyperledger Fabric 网络。你还会注意到,你有两个具有相同镜像 ID 的实例——一个标记为 “amd64-1.x.x”,另一个标记为 “latest”。在1.2.0之前,由 uname -m
命令结果来确定下载的镜像,并显示为 “x86_64-1.x.x”。
注解
在不同的体系架构中,x86_64/amd64 将替换为标识你的体系架构的字符串。
注解
如果你有本文档未解决的问题,或遇到任何有关教程的问题,请访问 还有问题? 页面,获取有关在何处寻求其他帮助的一些提示。
在我们开始之前,如果您还没有这样做,您可能希望检查您是否已经在将要开发区块链应用程序以及/或运行Hyperledger Fabric的平台上安装了所有先决条件(准备阶段)。
安装必备组件后,即可下载并安装HyperLedger Fabric。 在我们为Fabric二进制文件开发真正的安装程序时,我们提供了一个脚本,可以将样例、二进制文件和Docker镜像安装(安装示例、二进制文件和 Docker 镜像 )到您的系统中。 该脚本还将Docker镜像下载到本地注册表。
Hyperledger Fabric SDKs¶
Hyperledger Fabric提供了许多SDK来支持各种编程语言。 有支持Node.js和Java 语言的两个正式发布的SDK:
此外,还有三个尚未正式发布的SDK(适用于Python,Go和REST),但它们仍可供下载和测试:
开发应用¶
场景¶
受众:架构师, 应用和智能合约开发者, 业务专家
在本主题中,我们将会描述一个涉及六个组织的业务场景,这些组织使用基于 Hyperledger Fabric 构建的商业票据网络 PaperNet 来进行商业票据的发行,购买和兑换操作。我们将围绕该场景来概述参与组织使用的商业票据应用程序和智能合约的开发要求。
PaperNet 网络¶
PaperNet 是一个商业票据网络,它允许网络中的组织在获得适当授权的前提下执行商业票据的发行、交易、兑换和评估操作。
develop.systemscontext
上图展示的是 PaperNet 商业票据网络。其中的六个组织目前使用 PaperNet 网络发行,购买,出售,兑换和评估商业票据。其中 MagentoCorp 负责发行和兑换商业票据。 DigiBank, BigFund,BrokerHouse 和 HedgeMatic 互相交易商业票据。而 RateM 则对商业票据进行各种风险评估。
让我们来看一下 MagnetoCorp 如何使用 PaperNet 和商业票据来辅助开展自身业务的。
网络成员介绍¶
MagnetoCorp 是一家广受尊重的自动驾驶电动汽车制造商。在2020年4月初,MagnetoCorp 公司赢得了一份为 Daintree 公司制造10,000辆 Model D 汽车的大订单,后者是私家车市场的新成员。虽然这份订单意味着MagnetoCorp 公司获得重大胜利,但是在11月1日车辆交付——即双方正式确定交易后六个月——之前它还无法收到货款。
为完成生产任务,MagnetoCorp 公司需雇佣1000名员工进行至少6个月的工作。这样一来,MagnetoCorp 每月需额外支出500万美元以支付新员工的工资,给公司带来了短期的财务压力。商业票据旨在帮助 MagnetoCorp 公司解决短期融资难题,其原理在于:MagnetoCorp 公司出售商业票据获得现款以供新员工工资开支,根据预期,它将在 Daintree 公司开始支付货款时获得大量资金。
在五月底,MagnetoCorp 公司需要为月初入职的新员工支付总计500万美元的工资。为此,MagnetoCorp 发行一张面值为500万美元、期限为6个月——预计MagnetoCorp 将在6个月内收到货款——的商业票据。DigiBank 认为 MagnetoCorp 公司信誉良好,因此只需收取略高于央行2%基础利率的溢价。根据基础利率,这张到期后价值500万美元的票据目前估价495万美元。基于以上判断,DigiBank 决定以494万美元的价格从MagnetoCorp 公司购得这张期限为6个月的商业票据,与基础利率计算的495万美元估价相比略有折扣。DigiBank 非常希望在未来6个月内可以向MagnetoCorp 公司兑现500万美元,从而获利 1万美元,以作为自己承担与该票据相关的风险的报酬。这1万美元的获利意味着 DigiBank 公司此次投资收到2.4%的报酬,远高于2%的无风险报酬。
六月底时,MagnetoCorp 公司为支付新员工六月份的工资发行了另一张面值同为500万美元的商业票据, BigFund 公司以494万美元的价格购得该票据,与五月份的购价相同,这是因为六月份的相关商业情况基本未发生改变。
接下来每个月,MagnetoCorp 公司都可以通过发行商业票据来支付新员工的工资,票据购买方可能是 DigiBank 或其他任何 PaperNet 商业票据网络的参与者,即BigFund, HedgeMatic 或 BrokerHouse。票据的最终购价取决于央行基础利率和 MagnetoCorp 公司相关风险,后者受多种因素的影响,例如 Model D 汽车的生产情况,信用评级机构 RateM 给 MagnetoCorp 公司做出的信用评估等。
PaperNet 网络中各成员分别扮演不同的角色,其中 MagnetoCorp 负责发行票据,DigiBank、 BigFund、 HedgeMatic 和 BrokerHouse 负责交易票据,而 RateM 负责评估票据。角色相同的组织互为竞争关系,如 DigiBank、 BigFund、 HedgeMatic 和 BrokerHouse。角色不同的组织不一定互为竞争关系,但是可能也存在对立的商业利益,比如 MagnetoCorp 和 DigiBank,前者希望自己发行的商业票据能获得高评级,从而卖出高价,而后者却希望前者的票据获得低评级,从而可以低价买入该票据。从上文中我们可以看到,即使是 PaperNet 这种看似很简单的网络可能也存在复杂的信任关系。对此,区块链技术可以在互为竞争关系或彼此间存在可导致争端的对立商业利益的组织之间建立起信任。其中 Fabric 尤为如此,它能够抓住极其细微的信任关系。
关于 MagnetoCorp 的讨论就到此为止,现在让我们来开发客户端应用程序和智能合约( PaperNet 用来发行、购买、销售和兑换商业票据以及抓住信任关系)。稍后我们将接着讨论评级机构 RateM 所扮演的角色。
分析¶
受众: 架构师,应用和合约开发者,业务专家
接下来让我们详细探讨一下商业票据。MagnetoCorp 和 DigiBank 等 PaperNet 网络的参与者皆使用商业票据交易来实现自己的商业目的,我们来一起检验一下商业票据的结构以及逐渐影响这一结构的交易。我们还将基于各网络组织之间的信任关系来考虑哪个组织需要对交易签名。紧接着我们还将讨论票据买方和卖方之间的资金流动,不过现在让我们只关注 MagnetoCorp 发布的第一个票据。
商业票据生命周期¶
5月31号 MagnetoCorp 发行一张商业票据00001。花点时间来看看该票据的第一个状态,它具有不同的属性和值:
Issuer = MagnetoCorp
Paper = 00001
Owner = MagnetoCorp
Issue date = 31 May 2020
Maturity = 30 November 2020
Face value = 5M USD
Current state = issued
该票据的状态是发行交易的结果,它使得 MagnetoCorp 公司的第一张商业票据面世!注意一下该票据在今年晚些时候将兑换500万美元。在发行票据00001时 Issuer
和 Owner
具有相同的值。该票据可有唯一标识 MagnetoCorp00001
,它是 Issuer
属性和 Paper
属性的组合。最后,看属性 Current state = issued
如何快速识别 MagnetoCorp 票据 00001 在其生命周期中所处阶段。
发行后不久,票据被 DigiBank 买下。花点时间来看看该该票据如何因为此购买交易发生变化:
Issuer = MagnetoCorp
Paper = 00001
Owner = DigiBank
Issue date = 31 May 2020
Maturity date = 30 November 2020
Face value = 5M USD
Current state = trading
最重要的变化是 Owner
的改变,票据初始拥有者是 MagnetoCorp
而现在是 DigiBank
。我们可以想象如果后期该票据被出售给 BrokerHouse 或 HedgeMatic,则 Owner
也会相应更改为BrokerHouse 或 HedgeMatic。注意 Current state
让我们很容易识别出该票据目前状态是 trading
。
六个月后,如果 DigiBank 仍然持有该商业票据,它就可以从 MagnetoCorp 那里进行兑换:
Issuer = MagnetoCorp
Paper = 00001
Owner = MagnetoCorp
Issue date = 31 May 2020
Maturity date = 30 November 2020
Face value = 5M USD
Current state = redeemed
最终的兑换交易结束了这张商业票据的生命周期,可以认为生命周期已经终止。对已兑换的商业票据作记录通常是强制性的,redeemed
状态可以让我们能很快判断出票据是否已经兑换。通过对比 Owner
的值和交易创建者的身份,就可以利用票据的 Owner
值来执行兑换交易中的访问控制。Fabric 通过 getCreator()
链码 API 来支持这个功能。如果使用 golang 作为链码开发的语言,客户端身份链码库可用于检索交易创建者的额外属性。
交易¶
从上文中我们可以看出,票据00001的生命周期相对直接,通过发行、购买和兑换交易,其生命周期在 issued
、trading
和 redeemed
状态之间转换。
这三笔交易由 MagnetoCorp 和 DigiBank(两次)发起,使得00001票据的状态发生变化。让我们详细讨论一下对票据造成影响的交易:
发行(Issue)¶
看一下由 MagnetoCorp 发起的第一笔交易:
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
我们可以看到,发行交易的结构由属性和值组成。这个交易的结构和票据00001的结构不同但二者十分匹配。那是因为它们是不同的东西,票据00001反映的是 PaperNet 由于发行交易而产生的状态。它是包含这些属性并生成这张商业票据的发行交易背后的逻辑(这些我们看不到)。由于交易创建了票据,这也就意味着这些结构之间存在紧密联系。
发行交易唯一涉及到的组织是 MagnetoCorp。因此,MagnetoCorp 需要对这笔交易进行离线签名。一般来说票据的发行者需要在发行新票据的交易上进行离线签名。
购买(Buy)¶
接下来,看一下购买交易,该交易将票据00001的所有权从 MagnetoCorp 转移到 DigiBank:
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = MagnetoCorp
New owner = DigiBank
Purchase time = 31 May 2020 10:00:00 EST
Price = 4.94M USD
对比之前的交易我们可以看到,购买交易中的属性更少。这是因为该交易只能修改该票据,它只改变了 New owner = DigiBank
属性,其余属性未发生变化。购买交易中最重要的点在于所有权的变更,事实上这份交易中存在票据当前所有人MagnetoCorp 的承认。
你可能会奇怪为什么 Purchase time
和 Price
这两个属性没有在票据00001中体现呢?这要回到交易和票据之间的差异。494万美元的价格标签实际上是交易的属性,而不是票据的属性。花点时间来思考一下这两者的不同,它并不像看上去那么明显。稍后我们会看到账本会记录一些信息,其中包括影响票据的所有交易历史,还包括票据的最新状态。弄清楚如何区分这些信息是非常重要的。
同样值得注意的是,票据00001可能会被买卖多次。尽管我们的场景中略微跳过了一部分环节,但我们还是来检查一下票据00001的所有权发生变更的话可能会发生哪些交易。
如果 BigFund 购买:
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = DigiBank
New owner = BigFund
Purchase time = 2 June 2020 12:20:00 EST
Price = 4.93M USD
接着由 HedgeMatic 购买:
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = BigFund
New owner = HedgeMatic
Purchase time = 3 June 2020 15:59:00 EST
Price = 4.90M USD
看看票据所有者如何变化,价格如何变化。你能想到 MagnetoCorp 商业票据价格会降低的原因吗?
一项购买交易需要买卖双方的签名以作为交易两方达成共识的证据。
兑换(Redeem)¶
票据00001的兑换交易代表了它生命周期的结束。以上这个例子相对简单,其中 HedgeMatic 是将商业票据交回 MagnetoCorp 这一交易的发起方:
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Current owner = HedgeMatic
Redeem time = 30 Nov 2020 12:00:00 EST
注意,兑换交易的属性也很少;票据00001所有更改都可以通过兑换交易逻辑来进行数据计算: Issuer
将成为票据新的所有者,Current state
将变成 redeemed
。上述例子中指定了 Current owner
属性,因此可以根据当前的票据持有者来检查这一属性。
从信任的角度来说,兑换交易的逻辑也适用于购买交易:在购买交易中,买卖双方也都需要对交易进行离线签名。
账本¶
从以上讨论中我们可以看出,交易和由交易导致的票据状态是 PaperNet 中两个重要的概念。事实上,任何 Hyperledger Fabric 分布式账本中都存在这两个基本成分——其一为包含了所有物件当前值的世界状态;其二则是记录了所有导致当前世界状态的交易历史的区块链。
根据规则对交易进行离线签名是硬性要求,在把交易添加到账本上之前会判断是否已进行签名。只有存在规定签名的交易才能被 Fabric 视为有效。
现在你可以将以上想法转化为智能合约,可能你的编程技能有些生疏,但别担心,我们会提供理解程序代码的相关提示。掌握商业票据智能合约是设计自己的应用程序的第一个重要步骤。如果你是一个有编程经验的业务分析师,不要害怕继续深入挖掘!
流程和数据设计¶
受众:架构师,应用程序和智能合约开发者,业务专家
本文主要讨论 PaperNet 中商业票据流程的设计方法以及该流程的相关数据结构。我们在分析这篇文章中已经强调过,使用状态和交易对 PaperNet 网络进行建模能够准确呈现商业票据的交易流程。接下来我们将详细讲解两个关联性很高的概念,从而为我们后续设计 PaperNet 智能合约和应用程序提供帮助。
生命周期¶
正如我们所见,在处理商业票据时有两个重要的概念:状态和交易。实际上,所有区块链用例都是如此;一些以状态为模型的概念性价值对象的生命周期转换是通过交易来描述的。要想成功实施商业票据流程,必须先对状态和交易进行有效分析。
我们可以用以下状态转移图来展示商业票据的生命周期:
develop.statetransition
以上是商业票据的状态转移图。商业票据通过发行、购买和兑换交易在 issued(已发行), trading(交易中) 和 redeemed(已兑换) 状态之间进行转换。
对照上图,观察商业票据的状态变化,看特定交易是如何控制商业票据生命周期转换的。在 Hypledger Fabric 中,智能合约实现了商业票据不同生命周期转换的交易逻辑。实际上商业票据状态被储存在账本世界状态中,接下来我们就深入了解一下这些概念。
账本状态¶
回想一下商业票据的结构:
develop.paperstructure
我们可以用一组属性来代表商业票据,其中每个属性都对应一个值。通常,这些属性的组合会为每个票据提供一个独一无二的键。
观察上图可知,商业票据的 Paper
属性的值是00001
,Face value
属性的值是 5M USD
。更重要的是 Current state
属性,它体现了商业票据的具体状态(issued
、trading
或者 redeemed
)。将以上所有属性结合起来就构成了一个商业票据的状态,而将所有这些商业票据的状态集合起来就构成了账本的世界状态。
所有账本世界状态的形式皆是如此:每个状态中包含一系列属性,各属性的值都不一样。账本的这种多属性特征十分有用,我们可以把Fabric的状态看作是一个矢量而不是简单的标量。在此基础上我们可以用独立的状态来代表一个对象的全部事实信息,这些状态后续将会经历由交易逻辑控制的状态转换。Fabric 中的状态是作为键值对来实现的,其中值是以一种捕获了指定对象多个属性的格式来编码对象属性的,通常是 JSON 格式。账本数据库支持根据上述属性进行高级查询操作,这对于复杂的对象索取十分有帮助。
下图将 MagnetoCorp 的商业票据 00001
呈现为一个根据不同交易进行转换的状态矢量:
develop.paperstates
不同交易导致的结果是产生或转换商业票据状态。Hyperledger Fabric 状态拥有多个属性,所以它们是向量而不是标量。
观察上图可发现,各票据的起始状态皆为空,从技术上来说这是票据的 nil 状态,即票据不存在!发行交易生成了票据 00001
,随后购买和兑换交易使得票据状态发生改变。
观察上图可发现,各状态的每个属性都有一个名字和一个值。虽然目前所有商业票据的属性都相同,但这并非表示属性相同是必要条件,比如 Hyperledger Fabric 就支持不同的状态拥有不同的属性。这就使得相同的账本世界状态能够包含同一资产的不同形式以及不同形式的资产。同时这也使得更新状态结构成为可能;试想一下如果出现新规定要求必须有一个额外的数据字段,那么更新状态结构就十分有益。灵活的状态属性可满足数据演化的基本需求。
状态的键¶
在绝大多数实际应用程序中,每个状态都包含一系列独特属性,这些属性可以在指定情形下描述该状态,我们把这种状态称之为键。将 PaperNet 商业票据的 Issuer
和 paper
属性拼接起来就形成了它的键,也就是说MagnetoCorp 第一个票据的键是 MagnetoCorp00001
。
通过一个状态的键我们就可以描述一张票据;票据的形成是 发行 交易的结果,后续过程中,购买和兑换交易将对票据进行更新。Hyperledger Fabric 要求账本中每个状态都有一个独特的键。
当现有属性无法形成一个独特的键时,系统将指定应用程序生成的独特建来作为更新票据状态的交易的输入。应用程序生成的键通常是UUID形式,虽然可读性不强,但却是一种标准做法。账本中的所有状态都必须拥有一个独特的键,这一点很重要。
注意:不要在键中使用 U+0000 (nil 字节) 。
多种状态¶
从上文中我们了解到,PaperNet 中的商业票据作为状态向量被存储在账本中。从账本中查询不同的商业票据是合理需求;比如,查询所有由 MagnetoCorp 发行的的票据,或者查询所有由 MagnetoCorp 发行且处在 redeemed
状态的票据。
要想实现不同类型的查询操作,需要将所有相关票据组合起来放在一个逻辑列表中。PaperNet 的设计中就包含了商业票据列表的思想,该列表是一个逻辑容器,每当发行商业票据或更新商业票据的状态时,该逻辑容器就会发生更新。
逻辑描述¶
设想所有的 PaperNet 商业票据都位于同一个商业票据列表中,这样有助于我们理解:
develop.paperlist
MagnetoCorp 新创建的票据 00004 被添加到现有票据的列表中。
通过发行交易将新票据添加到上述列表中,列表中已有的票据会因购买和兑换交易而发生状态更新。观察可得,该列表有一个描述性的名称 org.papernet.papers
。使用这种DNS 名称有诸多优势,因为准确的命名可以使得区块链设计变得简便易懂,这和智能合约的命名空间是一个道理。
物理描述¶
设想 PaperNet 中存在一个单独的票据列表org.papernet.papers
是没有问题的,但是最好将该列表作为一组单独的 Fabric 状态来实现,这些状态的复合键将各状态与其列表关联起来。这样一来,每个状态的复合键都是唯一的,并且可以支持高效的列表查询操作。
develop.paperlist
如上图,用一组互不相同的 Hyperledger Fabric 状态来代表一个 PaperNet 商业票据列表
观察上图可知,列表中的每张票据都被用一个向量状态来表示,每个状态都包含一个由 org.papernet.paper
, Issuer
和 Paper
属性连接而成的复合键。这种结构具有以下两种优势:
- 支持用户检查账本中的任意状态向量以判定其所在列表,无需查询其他列表。这就好比观察体育比赛的粉丝:通过所穿队服颜色就能知道粉丝支持的是哪支队伍。无需罗列粉丝名单,因为粉丝们自己就表达了各自支持的队伍。
- Hyperlegder Fabric 内部使用了一个并发控制机制来更新账本,保证各票据处于不同的状态向量中,大大降低了出现相同状态冲突的概率。出现这种碰撞使得交易需要重新提交,应用程序设计变得复杂,系统性能大打折扣。
上述第二点是 Hyperledger Fabric 的关键点;状态向量的物理设计对于优化性能和行为至关重要。所以,一定要保证你的网络中不会出现相同票据状态的情况!
智能合约处理¶
受众:架构师、应用程序和智能合约开发人员
区块链网络的核心是智能合约。在PaperNet中,商业票据智能合约中的代码定义了商业票据的有效状态,以及将票据从一种状态状态转变为另一种状态的交易逻辑。在本主题中,我们将向您展示如何实现一个真实世界的智能合约,该合约管理发行、购买和兑换商业票据的过程。
我们将会介绍:
如果您愿意,可以下载示例,甚至可以在本地运行。它是用 JavaScript 编写的,但逻辑与语言无关,因此您可以轻松地查看正在发生的事情!(该示例也可用于 Java 和 GOLANG。)
智能合约¶
智能合约定义业务对象的不同状态,并管理对象在不同状态之间变化的过程。智能合约很重要,因为它们允许架构师和智能合约开发人员定义在区块链网络中协作的不同组织之间共享的关键业务流程和数据。
在 PaperNet 网络中,智能合约由不同的网络参与者共享,例如 MagnetoCorp 和 DigiBank。 连接到网络的所有应用程序必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。
合约类¶
PaperNet 商业票据智能合约的副本包含在 papercontract.js
中。如果您已下载,请使用浏览器查看或在您喜欢的编辑器中打开它。
您可能会从文件路径中注意到这是 MagnetoCorp 的智能合约副本。 MagnetoCorp 和 DigiBank 必须同意他们将要使用的智能合约版本。现在,你看哪个组织的合约副本无关紧要,它们都是一样的。
花一些时间看一下智能合约的整体结构; 注意,它很短!在 papercontract.js
的顶部,您将看到商业票据智能合约的定义:
class CommercialPaperContract extends Contract {...}
CommercialPaperContract
类包含商业票据的交易定义:发行、 购买和兑换。正是这些交易带来了商业票据的存在,并将它们贯穿整个生命周期。我们很快就会检查这些交易,但现在请注意 CommericalPaperContract
如何扩展 Hyperledger Fabric Contract
类。这个内置类和 Context
类开始就被导入代码区域:
const { Contract, Context } = require('fabric-contract-api');
我们的商业票据合约将使用这些类的内置功能,例如自动方法调用,每个交易上下文,交易处理器,和类共享状态。
还要注意类构造函数如何使用其超类通过一个命名空间来初始化自身:
constructor() {
super('org.papernet.commercialpaper');
}
最重要的是,org.papernet.commercialpaper
非常具有描述性——这份智能合约是所有 PaperNet 组织关于商业票据商定的定义。
通常每个文件只有一个智能合约(合约往往有不同的生命周期,这使得将它们分开是明智的)。但是,在某些情况下,多个智能合约可能会为应用程序提供语法帮助,例如 EuroBond
、DollarBond
、YenBond
但基本上提供相同的功能。在这种情况下,智能合约和交易可以消除歧义。
交易定义¶
在类中找到 issue 方法。
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}
无论何时调用此合约来发行
商业票据,都会调用该方法。回想一下如何使用以下交易创建商业票据 00001:
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
我们已经更改了编程样式的变量名称,但是看看这些属性几乎直接映射到 issue
方法变量。
只要应用程序请求发行商业票据,合约就会自动调用 issue
方法。交易属性值通过相应的变量提供给方法。使用示例应用程序,了解应用程序如何使用应用程序主题中的 Hyperledger Fabric SDK 提交一笔交易。
您可能已经注意到 issue 方法中定义的一个额外变量 ctx。它被称为交易上下文,它始终是第一个参数。默认情况下,它维护与交易逻辑相关的每个合约和每个交易的信息。例如,它将包含 MagnetoCorp 指定的交易标识符,MagnetoCorp 可以发行用户的数字证书,也可以调用账本 API。
通过实现自己的 createContext()
方法而不是接受默认实现,了解智能合约如何扩展默认交易上下文:
createContext() {
return new CommercialPaperContext()
}
此扩展上下文将自定义属性 paperList 添加到默认值:
class CommercialPaperContext extends Context {
constructor() {
super();
// All papers are held in a list of papers
this.paperList = new PaperList(this);
}
我们很快就会看到 ctx.paperList
如何随后用于帮助存储和检索所有 PaperNet 商业票据。
为了巩固您对智能合约交易结构的理解,找到购买和兑换交易定义,看看您是否可以理解它们如何映射到相应的商业票据交易。
购买交易:
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...}
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = MagnetoCorp
New owner = DigiBank
Purchase time = 31 May 2020 10:00:00 EST
Price = 4.94M USD
兑换交易:
async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...}
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Redeemer = DigiBank
Redeem time = 31 Dec 2020 12:00:00 EST
在这两种情况下,观察商业票据交易和智能合约方法定义之间的1:1对应关系。并且不用担心 async
和 await
关键字,它们允许将异步 JavaScript 函数视为其他编程语言中的同步方法。
交易逻辑¶
现在您已经了解了合约的结构和交易的定义,下面让我们关注智能合约中的逻辑。
回想一下第一个发行交易:
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
它导致issue方法被传递调用:
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
// create an instance of the paper
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
// Smart contract, rather than paper, moves paper into ISSUED state
paper.setIssued();
// Newly issued paper is owned by the issuer
paper.setOwner(issuer);
// Add the paper to the list of all similar commercial papers in the ledger world state
await ctx.paperList.addPaper(paper);
// Must return a serialized paper to caller of smart contract
return paper.toBuffer();
}
逻辑很简单:获取交易输入变量,创建新的商业票据 paper
,使用 paperList
将其添加到所有商业票据的列表中,并将新的商业票据(序列化为buffer)作为交易响应返回。
了解如何从交易上下文中检索 paperList
以提供对商业票据列表的访问。issue()
、buy()
和 redeem()
不断重新访问 ctx.paperList
以使商业票据列表保持最新。
购买交易的逻辑更详细描述:
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) {
// Retrieve the current paper using key fields provided
let paperKey = CommercialPaper.makeKey([issuer, paperNumber]);
let paper = await ctx.paperList.getPaper(paperKey);
// Validate current owner
if (paper.getOwner() !== currentOwner) {
throw new Error('Paper ' + issuer + paperNumber + ' is not owned by ' + currentOwner);
}
// First buy moves state from ISSUED to TRADING
if (paper.isIssued()) {
paper.setTrading();
}
// Check paper is not already REDEEMED
if (paper.isTrading()) {
paper.setOwner(newOwner);
} else {
throw new Error('Paper ' + issuer + paperNumber + ' is not trading. Current state = ' +paper.getCurrentState());
}
// Update the paper
await ctx.paperList.updatePaper(paper);
return paper.toBuffer();
}
在使用 paper.setOwner(newOwner)
更改拥有者之前,理解交易如何检查 currentOwner
并检查该 paper
应该是 TRADING
状态的。基本流程很简单:检查一些前提条件,设置新拥有者,
更新账本上的商业票据,并将更新的商业票据(序列化为 buffer )作为交易响应返回。
你为什么不看一下你否能理解兑换交易的逻辑?
表示对象¶
我们已经了解了如何使用 CommercialPaper
和 PaperList
类定义和实现发行、购买和兑换交易。让我们通过查看这些类如何工作来结束这个主题。
在 paper.js
文件中找到 CommercialPaper
类:
class CommercialPaper extends State {...}
此类包含商业票据状态的内存表示。了解 createInstance
方法如何使用提供的参数初始化一个新的商业票据:
static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue });
}
回想一下发行交易如何使用这个类:
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
查看每次调用发行交易时,如何创建包含交易数据的商业票据的新内存实例。
需要注意的几个要点:
这是一个内存中的表示; 我们稍后会看到它如何在帐本上显示。
CommercialPaper
类扩展了State
类。State
是一个应用程序定义的类,它为状态创建一个公共抽象。所有状态都有一个它们代表的业务对象类、一个复合键,可以被序列化和反序列化,等等。当我们在帐本上存储多个业务对象类型时,State
可以帮助我们的代码更清晰。检查state.js
文件中的State
类。票据在创建时会计算自己的密钥,在访问帐本时将使用此密钥。密钥由
issuer
和paperNumber
的组合形成。constructor(obj) { super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]); Object.assign(this, obj); }
票据通过交易而不是票据类变更到
ISSUED
状态。那是因为智能合约控制票据的状态生命周期。例如,import
交易可能会立即创建一组新的TRADING
状态的票据。
CommercialPaper
类的其余部分包含简单的辅助方法:
getOwner() {
return this.owner;
}
回想一下智能合约如何使用这样的方法来维护商业票据的整个生命周期。例如,在兑换交易中,我们看到:
if (paper.getOwner() === redeemingOwner) {
paper.setOwner(paper.getIssuer());
paper.setRedeemed();
}
访问账本¶
现在在 paperlist.js
文件中找到 PaperList
类:
class PaperList extends StateList {
此实用程序类用于管理 Hyperledger Fabric 状态数据库中的所有 PaperNet 商业票据。PaperList 数据结构在架构主题中有更详细的描述。
与 CommercialPaper
类一样,此类扩展了应用程序定义的 StateList
类,该类为一系列状态创建了一个通用抽象——在本例中是 PaperNet 中的所有商业票据。
addPaper()
方法是对 StateList.addState()
方法的简单封装:
async addPaper(paper) {
return this.addState(paper);
}
您可以在 StateList.js
文件中看到 StateList
类如何使用 Fabric API putState()
将商业票据作为状态数据写在帐本中:
async addState(state) {
let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
let data = State.serialize(state);
await this.ctx.stub.putState(key, data);
}
帐本中的每个状态数据都需要以下两个基本要素:
- Key:
key
由createCompositeKey()
使用固定名称和state
密钥形成。在构造PaperList
对象时分配了名称,state.getSplitKey()
确定每个状态的唯一键。 - Data:
data
只是商业票据状态的序列化形式,使用State.serialize()
方法创建。State
类使用 JSON 对数据进行序列化和反序列化,并根据需要使用 State 的业务对象类,在我们的例子中为CommercialPaper
,在构造PaperList
对象时再次设置。
注意 StateList
不存储有关单个状态或状态总列表的任何内容——它将所有这些状态委托给 Fabric 状态数据库。这是一个重要的设计模式 – 它减少了 Hyperledger Fabric 中账本 MVCC 冲突的机会。
StateList getState()
和 updateState()
方法以类似的方式工作:
async getState(key) {
let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key));
let data = await this.ctx.stub.getState(ledgerKey);
let state = State.deserialize(data, this.supportedClasses);
return state;
}
async updateState(state) {
let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
let data = State.serialize(state);
await this.ctx.stub.putState(key, data);
}
了解他们如何使用 Fabric APIs putState()
、 getState()
和 createCompositeKey()
来存取账本。我们稍后将扩展这份智能合约,以列出 paperNet 中的所有商业票据。实现账本检索的方法可能是什么样的?
是的!在本主题中,您已了解如何为 PaperNet 实现智能合约。您可以转到下一个子主题,以查看应用程序如何使用 Fabric SDK 调用智能合约。
应用¶
受众:架构师、应用程序和智能合约开发人员
应用程序可以通过将交易提交到帐本或查询帐本内容来与区块链网络进行交互。本主题介绍了应用程序如何执行此操作的机制;在我们的场景中,组织使用应用程序访问 PaperNet,这些应用程序调用定义在商业票据智能合约中的发行、卖出和兑换 交易。尽管 MagnetoCorp 的应用发行商业票据是基础功能,但它涵盖了所有主要的理解点。
在本主题中,我们将介绍:
为了帮助您理解,我们将参考 Hyperledger Fabric 提供的商业票据示例应用程序。您可以下载示例,甚至可以在本地运行。它是用 JavaScript 编写的,但逻辑与语言无关,因此您可以轻松地查看正在发生的事情!(该示例也可用于 Java 和 GOLANG。)
基本流程¶
应用程序使用 Fabric SDK 与区块链网络交互。以下是应用程序如何调用商业票据智能合约的简图:
develop.application
PaperNet 应用程序调用商业票据智能合约来提交发行交易请求。
应用程序必须遵循六个基本步骤来提交交易:
- 从钱包中选择一个身份
- 连接到网关
- 访问所需的网络
- 构建智能合约的交易请求
- 将交易提交到网络
- 处理响应
您将看到典型应用程序如何使用 Fabric SDK 执行这六个步骤。您可以在 issue.js
文件中找到应用程序代码。如果您已下载,请在浏览器中查看,或在您喜欢的编辑器中打开它。花一些时间看一下应用程序的整体结构;尽管有注释和空白,但是它只有100行代码!
钱包¶
在 issue.js
的顶部,您将看到两个 Fabric 类导入代码:
const { FileSystemWallet, Gateway } = require('fabric-network');
您可以在 Node SDK 文档中了解 fabric-network
类,但是现在,让我们看看如何使用它们将 MagnetoCorp 的应用程序连接到 PaperNet。该应用程序使用 Fabric Wallet 类,如下所示:
const wallet = new FileSystemWallet('../identity/user/isabella/wallet');
了解 wallet
如何在本地文件系统中找到钱包。从钱包中检索到的身份显然适用于使用 issue
应用程序的 Isabella 用户。钱包拥有一组身份(X.509 数字证书)可用于访问 PaperNet 或任何其他 Fabric 网络。如果您运行该教程,并查看此目录,您将看到 Isabella 的身份凭证。
想想一个钱包里面装着身份证,驾照或 ATM 卡的数字等价物。其中的 X.509 数字证书将持有者与组织相关联,从而使他们有权在网络通道中获得权利。例如, Isabella
可能是 MagnetoCorp 的管理员,这可能比其他用户更有特权。比如,来自 DigiBank 的 Balaji
。此外,智能合约可以在使用交易上下文的智能合约处理期间检索此身份。
另请注意,钱包不持有任何形式的现金或代币,它们持有身份。
网关¶
第二个关键类是 Fabric Gateway。 最重要的是,网关识别一个或多个提供网络访问的 Peer 节点,在我们的例子中是PaperNet。了解 issue.js
如何连接到其网关:
await gateway.connect(connectionProfile, connectionOptions);
gateway.connect()
有两个重要参数:
- connectionProfile: 连接配置文件的文件系统位置,用于将一组 Peer 节点标识为 PaperNet 的网关
- connectionOptions: 一组用于控制
issue.js
与 PaperNet 交互的选项
了解客户端应用程序如何使用网关将自身与可能发生变化的网络拓扑隔离开来。网关负责使用连接配置文件和连接选项将交易提案发送到网络中的正确 Peer 节点。
花一些时间检查连接配置文件 ./gateway/connectionProfile.yaml
。它使用易于阅读的 YAML格式。
它被加载并转换为 JSON 对象:
let connectionProfile = yaml.safeLoad(file.readFileSync('./gateway/connectionProfile.yaml', 'utf8'));
现在,我们只关注 channels:
和 peers:
配置部分:(我们稍微修改了细节,以便更好地解释发生了什么。)
channels:
papernet:
peers:
peer1.magnetocorp.com:
endorsingPeer: true
eventSource: true
peer2.digibank.com:
endorsingPeer: true
eventSource: true
peers:
peer1.magnetocorp.com:
url: grpcs://localhost:7051
grpcOptions:
ssl-target-name-override: peer1.magnetocorp.com
request-timeout: 120
tlsCACerts:
path: certificates/magnetocorp/magnetocorp.com-cert.pem
peer2.digibank.com:
url: grpcs://localhost:8051
grpcOptions:
ssl-target-name-override: peer1.digibank.com
tlsCACerts:
path: certificates/digibank/digibank.com-cert.pem
了解 channel:
如何识别 PaperNet:
网络通道及其两个 Peer节点。MagnetoCorp 拥有 peer1.magenetocorp.com
,DigiBank 拥有 peer2.digibank.com
,两者都有背书节点的角色。通过 peers:
键链接到这些 Peer 节点,其中包含有关如何连接它们的详细信息,包括它们各自的网络地址。
连接配置文件包含大量信息,不仅仅是 Peer 节点,还有网络通道,网络排序节点,组织和 CA,因此如果您不了解所有信息,请不要担心!
现在让我们将注意力转向 connectionOptions
对象:
let connectionOptions = {
identity: userName,
wallet: wallet
}
看一下它如何指定身份、用户名和钱包、wallet
连接到网关。这些之前就在代码中分配过了。
应用程序可以使用其他连接选项来指示 SDK 代表它智能地执行操作。例如:
let connectionOptions = {
identity: userName,
wallet: wallet,
eventHandlerOptions: {
commitTimeout: 100,
strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
},
}
这里,commitTimeout
告诉 SDK 等待100秒以听取是否已提交交易。strategy:EventStrategies.MSPID_SCOPE_ANYFORTX
指定 SDK 可以在单个 MagnetoCorp 节点确认交易后通知应用程序,与 strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX
相反,strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX
要求 MagnetoCorp 和 DigiBank 的所有节点确认交易。
如果您愿意,请阅读更多有关连接选项如何允许应用程序指定面向目标的行为而不必担心如何实现的信息。
网络通道¶
在网关 connectionProfile.yaml
中定义的节点提供 issue.js
来访问 PaperNet。由于这些节点可以连接到多个网络通道,因此网关实际上为应用程序提供了对多个网络通道的访问!
了解应用程序如何选择特定通道:
const network = await gateway.getNetwork('PaperNet');
从这一点开始,network
将提供对 PaperNet 的访问。此外,如果应用程序想要访问另一个网络,BondNet
,同时,它很容易:
const network2 = await gateway.getNetwork('BondNet');
现在,我们的应用程序可以访问第二个网络 BondNet
,同时可以访问 PaperNet
!
我们在这里可以看到 Hyperledger Fabric 的一个强大功能——应用程序可以通过连接到多个网关节点来加入网络中的网络,每个网关节点都连接到多个网络通道。根据 gateway.connect()
提供的钱包标识,应用程序将在不同的通道中拥有不同的权限。
构造请求¶
该应用程序现在准备发行商业票据。要做到这一点,它将再次使用 CommercialPaperContract
,它可以非常直接地访问这个智能合约:
const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper');
请注意应用程序如何提供名称——papercontract
——以及可选的合约命名空间:org.papernet.commercialpaper
! 我们看一下命名空间如何从包含许多合约的 papercontract.js
链码文件中选出一个合约。在 PaperNet 中,papercontract.js
已安装并使用名称 papercontract
实例化,如果您有兴趣,请阅读如何安装和实例化包含多个智能合约的链代码。
如果我们的应用程序同时需要访问 PaperNet 或 BondNet 中的另一个合同,这将很容易:
const euroContract = await network.getContract('EuroCommercialPaperContract');
const bondContract = await network2.getContract('BondContract');
在这些示例中,请注意我们没有使用限定的合同名称,每个文件只有一个智能合同,而 “getcontract()” 将使用它找到的第一个合同。
回想一下 MagnetoCorp 用于发行其第一份商业票据的交易:
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
我们现在将此交易提交给 PaperNet!
提交交易¶
提交一个交易是对 SDK 的单个方法调用:
const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');
了解 submitTransaction()
参数如何与交易请求匹配。它们的值将传递给智能合约中的 issue()
方法,并用于创建新的商业票据。回想一下它的签名:
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}
在应用程序发出 submitTransaction()
之后不久,智能合约似乎会收到控制权,但事实并非如此。在幕后,SDK 使用 connectionOptions
和 connectionProfile
详细信息将交易提案发送到网络中的正确节点,在那里它可以获得所需的背书。但是应用程序不需要担心任何问题,它只是发出 submitTransaction
而 SDK 会解决所有问题!
Note that the submitTransaction
API includes a process for listening for
transaction commits. Listening for commits is required because without it,
you will not know whether your transaction has successfully been orderered,
validated, and committed to the ledger.
注意,submitTransaction
API 包含监听交易提交的处理。监听提交是必要的,因为没有它你就不知道你的交易是否成功地被排序、验证和提交到账本。
现在让我们将注意力转向应用程序如何处理响应!
处理响应¶
回想一下 papercontract.js
如何发行交易返回一个商业票据响应:
return paper.toBuffer();
您会注意到一个轻微的怪癖,新 paper
需要在返回到应用程序之前转换为缓冲区。请注意 issue.js
如何使用类方法 CommercialPaper.fromBuffer()
将相应缓冲区重新转换为商业票据:
let paper = CommercialPaper.fromBuffer(issueResponse);
这样可以在描述性完成消息中以自然的方式使用 paper
:
console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully issued for value ${paper.faceValue}`);
看一下如何在应用程序和智能合约中使用相同的 paper
类,如果您像这样构建代码,它将有助于可读性和重用。
与交易提案一样,智能合约完成后,应用程序可能会很快收到控制权,但事实并非如此。SDK 负责管理整个共识流程,并根据 strategy
connectionOption 在应用程序完成时通知应用程序。如果您对 SDK 的内容感兴趣,请阅读详细的交易流程。
就是这样!在本主题中,您已了解如何通过检查 MagnetoCorp 的应用程序如何在 PaperNet 中发行新的商业票据,从示例应用程序调用智能合约。现在检查关键账本和智能合约数据结构是由它们背后的架构主题设计的。
APIs¶
应用程序设计元素¶
本节详细介绍了 Hyperledger Fabric 中的客户端应用程序和智能合约开发的主要功能。对功能的充分理解将帮助您设计和实现高效且有效的解决方案。
合约名称¶
受众:架构师、应用程序与合约开发者、管理员
链码(Chaincode)是一种用于部署代码到 Hyperledger Fabric 区块链网络中的通用容器。链码中定义一个或多个相关联的智能合约。每个智能合约在链码中有一个唯一的标识名。应用程序通过合约名称去访问链码容器内的指定的智能合约。
在本主题中,我们将会讲到:
链码¶
在开发应用主题中,我们能够看到 Fabric SDK 如何提供了高级的编程抽象,从而能够帮助那些应用程序和智能合约的开发者们聚焦在他们的业务问题上,而不是将目光放在如何与 Fabric 网络交互的这些细节中。
智能合约(Smart Contract)是一种高级编程抽象的例子,可以在链码容器中定义智能合约。当一个链码被安装和实例化时,则链码内所有的智能合约对于相关联的通道(Channel)来说都是可用的。
contract.chaincode
多个智能合约能够被定义在同一个链码内。每一个智能合约都通过链码内的名字而被唯一标识。
在上图中,链码 A 中定义了三个智能合约,然而链码 B 中有四个智能合约。看一下链码名称如何被用于限定一个特定的智能合约。
账本结构由一组已经部署的智能合约所定义。那是因为账本包括了一些有关网络所感兴趣的业务对象(例如 PaperNet 内的商业票据),并且这些业务对象通过智能合约内定义的交易功能在其生命周期(例如:发行,购买,赎回)中移动。
在大多数情况下,一个链码内仅仅只定义了一个智能合约。然而,将相关联的智能合约放在一个链码中是有意义的。比如,以不同货币计价的商业票据中,可能有 EuroPaperContract
, DollarPaperContract
, YenPaperContract
,这些智能合约在部署他们的通道内可能要互相保持同步。
命名¶
链码里的每一个智能合约都通过合约名称被唯一标识。当智能合约可以在构造类时显示分配这个名称,或者让 Contract
类隐式分配一个默认名称。
查看 papercontract.js 链码文件:
class CommercialPaperContract extends Contract {
constructor() {
// Unique name when multiple contracts per chaincode file
super('org.papernet.commercialpaper');
}
看一下 CommercialPaperContract
构造函数是如何将合约名称指定为 org.papernet.commercialpaper
的。结果是:在 papercontract
链码内,这个智能合约现在与合约名 org.papernet.commercialpaper
相关联。
如果没有明确指定一个合约名,则会分配一个默认的名字——类名。在我们的例子中,默认的合约名是 CommercialPaperContract
。
细心地选择你的名字。不仅仅每一个智能合约必须有一个唯一的名字,而且一个精心挑选的名字也是很有启发性的。特别地,建议使用显式的 DNS 样式命名方法,对组织清晰、有意义的名称有帮助; org.papernet.commercialpaper
表达了 PaperNet 网络已经定义了一个标准的商业票据智能合约。
在一个给定链码内,合约名称也有利于消除具有相同名字的不同合约方法交易函数之间的歧义。当智能合约紧密联系的时候,这种歧义就容易发生;他们的交易名称往往倾向于一致。我们能够看到,通过链码和智能合约的名字的组合,一个交易被唯一地定义在通道内。
合约名称在链码文件内必须是唯一的。在部署前,一些代码编辑器将会检测是否存在具有相同类名的多个定义的情况。如果存在多个类使用了相同的合约名,无论是显式还是隐式指定,链码都将会返回错误。
应用程序¶
一旦链码安装在一个 Peer 节点而且实例化在一个通道上,链码里的智能合约对于应用程序来说是可访问的:
const network = await gateway.getNetwork(`papernet`);
const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper');
const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');
看一下应用程序如何通过 contract.getContract()
方法访问智能合约。papercontract
链码的名 org.papernet.commercialpaper
返回了一个引用,此引用使用 contract.submitTransaction()
接口去提交发布商业票据的交易。
默认合约¶
被定义在链码内的第一个智能合约被成为默认合约。这个默认是有用的,因为链码内往往有一个被定义的智能合约;这个默认的智能合约允许应用程序直接地访问这些交易,而不需要特殊指定合约名称。
default.contract
一个默认地智能合约是第一个被定义在链码的智能合约。
在这个图表中,CommercialPaperContract
就是那个默认的智能合约。即使我们有两个智能合约,默认的智能合约让我们当前的例子更加容易去编写。
const network = await gateway.getNetwork(`papernet`);
const contract = await network.getContract('papercontract');
const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');
在 papercontract
链码内,CommercialPaperContract
就是那个默认的智能合约,同时它有一个 issue
交易。注意,在 BondContract
内发布的交易事务仅仅能通过显示地寻址指来调用。同样地,即使 cancel
交易是唯一的(因为 BondContract
不是默认的智能合约),它也必须要显示地寻址。
在大多数情况下,一个链码仅仅只包括一个单一的智能合约,所以对链码仔细命名,能够降低开发者将链码视为概念来关注的需求。在上述代码例子中,感觉 papercontract
像是一个智能合约。
总而言之,在一个给定的链码内,合约名称是一些简单的机制去标识独自的智能合约。合约名称让应用程序更加容易的发现特定的智能合约而且更方便地使用它访问账本。
链码命名空间¶
本文面向的读者是: 架构师,应用程序和智能合约开发人员,管理员
链码命名空间允许其保持自己的世界状态与其他链码的不同。具体来说,同链码的智能合约对相同的世界状态都拥有直接的访问权,而不同链码的智能合约无法直接访问对方的世界状态。如果一个智能合约需要访问其他链码的世界状态,可通过执行链码-对-链码的调用来完成。最后,区块链可以包含涉及了不同世界状态的交易。
本文我们将讲述以下内容:
动机¶
命名空间是一个常见的概念。我们知道虽然公园街道,纽约和帕克街道,西雅图这些属于不同街道,但是他们有共同的名字。城市为公园街提供了一个命名空间,同时赋予了它自由度和清晰度。
在电脑系统中也是如此。命名空间实现了让不同用户在不影响其他方操作的情况下对一个共享系统的不同部分进行编程和操作。许多编程语言都有命名空间,这就使得程序可以轻松地分配独特的识别符,如变量名,而无须担心其他程序也在进行相同操作。下文中会介绍到Hyperledger Fabric通过命名空间使得智能合约可以保持自己的账本世界状态与其他智能合约的不同。
情景¶
让我们通过下面的图表来看一下账本世界状态如何管理对通道组织很重要的商业对象的相关事实。无论对象是商业票据,债券,还是车牌号码,无论这些对象位于生命周期的什么位置,它们都会被作为状态维持在账本数据状态数据库中。智能合约通过与账本(世界状态和区块链)交互来管理这些商业对象,而多数情况下这一过程会涉及到智能合约查询或更新账本世界状态。
账本世界状态是通过访问该状态的智能合约的链码来进行划分的,理解这一点至关重要 。这种划分或者命名空间对架构师、管理员和程序员来说是一点非常重要的设计考虑。
chaincodens.scenario 账本世界状态根据访问其状态的链码被分成不同命名空间。在一个给定通道上,相同链码的智能合约共享相同世界状态,不同链码的智能合约无法直接访问对方的世界状态。同样的道理,区块链可以包含与不同链码世界状态相关的交易。
在我们的示例中可以看到两种不同的链码中定义了四种智能合约,每个都位于它们自己的链码容器里。 euroPaper
和yenPaper
智能合约被定义在 papers
链码中。 euroBond
和 yenBond
智能合约的情况也类似——它们被定义在bonds
链码中。该设计可帮助应用程序员们理解他们的工作对象,是商业票据还是欧元或日元的债券,因为各金融产品的规则不会受货币种类影响,这就使得用相同链码管理这些金融产品的部署成为可能。
上图 还展示了这一部署选择的结果。数据库管理系统为papers
和 bonds
链码以及各自其中包含的智能合约创建了不同的世界状态数据库。 World state A
和 world state B
被分别放在不同的数据库中;其中的数据也被分隔开,从而使得一个单独的世界状态查询(比如)不会同时访问两个世界状态。据说世界状态根据其链码进行了命名空间设定。
看看 world state A
是如何包含两个列表的商业票据 paperListEuro
和 paperListYen
。状态 PAP11
和PAP21
分别是由 euroPaper
和 yenPaper
智能合约管理的每个票据的示例。由于这些智能合约共享同样的链码命名空间,因此它们的key(PAPxyz
) 必须在 papers
链码的命名空间里是唯一的。要注意怎样能在papers
链码上写一个能对所有商业票据(无论是欧元还是日元)执行汇总计算的智能合约,因为这些商业票据共享相同的命名空间。债券的情况也类似——债券保存在 world state B
中, world state B
与另一个bonds
数据库形成映射,它们的key必须都是唯一的。
同样重要的一点是,命名空间意味着 euroPaper
和 yenPaper
无法直接访问 world state B
,也意味着 euroBond
和 yenBond
无法直接访问 world state A
。这样的分隔是有帮助的,因为商业票据和债券是相差很大的两种金融工具,它们各有不同的属性和规定。同时命名空间还意味着 papers
和 bonds
可以有相同的key,因为它们在不同的命名空间。这是有帮助的,因为它为命名提供了足够大的自由空间。以此来为不同的商业对象命名。
最重要的一点是,我们可以看到区块链是与在特定通道上运行的节点相关的,它包括了影响 world state A
和 world state B
的交易。这就解释了为什么区块链是节点中最基础的数据结构。因为世界状态集是区块链交易的汇总结构,所以它总是能从区块链中重新创建。世界状态通常仅要求该状态的当前值,所以它可以简化智能合约,提升智能合约的效率。通过命名空间来让世界状态彼此各不相同,这能帮助智能合约将各自逻辑从其他智能合约中隔离出来,而不需要担心那些与不同世界状态通信的交易。例如, bonds
合约不需要担心 paper
交易,因为该合约无法看到由它所造成的世界状态。
同样值得注意的是,节点,链码容器和数据库管理系统(DBMS)在逻辑上来说都是不同的过程。节点和其所有的链码容器始终位于不同的操作系统进程中,但是DBMS可被配置为嵌入或分离,这取决于它的种类。对于LevelDB来说,DBMS整体被包含在节点中,但是对于CouchDB来说,这是另外一个操作系统进程。
要记住,该示例中命名空间的选择是商业上要求共享不同币种商业票据的结果,但是要将商业票据与债券区别开,这一点很重要。想想如何对命名空间的结果进行修改以满足保持每个金融资产等级互不相同这一商业需求,或者来共享所有商业票据和债券。
通道¶
如果一个节点加入了多个通道,那么将会为每个通道创建、管理一条新的区块链。而且每当有链码在一条新通道上被实例化了,就会为其创建一个新的世界状态数据库。这就意味着通道还可以随着链码命名空间来为世界状态生成一种命名空间。
然而,相同的节点和链码容器流程可同时加入多个通道,与区块链和世界状态数据库不同,这些流程不会随着加入的通道数而增加。
例如,如果链码 papers
和 bonds
在一个新的通道上被实例化了,将会有一个完全不同的区块链和两个新的世界状态数据库被创建出来。但是,节点和链码容器不会随之增加;它们只会各自连接到更多相应的通道上。
使用¶
让我们来用商业票据的例子讲解一个应用程序是如何使用带有命名空间的智能合约的。值得注意的是应用程序与节点交互,该节点将请求发送到对应的链码容器,随后,该链码容器访问DBMS。请求的传输是由下图中的节点核心组件完成的。
以下是使用商业票据和债券的应用程序代码,以欧元和日元定价。该代码可以说是不言自明的:
const euroPaper = network.getContract(papers, euroPaper);
paper1 = euroPaper.submit(issue, PAP11);
const yenPaper = network.getContract(papers, yenPaper);
paper2 = yenPaper.submit(redeem, PAP21);
const euroBond = network.getContract(bonds, euroBond);
bond1 = euroBond.submit(buy, BON31);
const yenBond = network.getContract(bonds, yenBond);
bond2 = yenBond.submit(sell, BON41);
看一下应用程序是怎么:
- 访问
euroPaper
和yenPaper
合约,用getContract()
API指明paper
的链码。看交互点1a和2a。 - 访问
euroBond
和yenBond
合约,用getContract()
API指明bonds
链码。看交互点3a和4a。 - 向网络提交一个
issue
交易,以使PAP11
票据使用euroPaper
合约。看交互点1a。这就产生了由world state A
中的PAP11
状态代表的商业票据;交互点1b。这一操作在交互点1c上被捕获为区块链的一项交易。 - 向网络发送一个
redeem
交易,让商业票据PAP21
来使用yenPaper
合约。看交互点2a。这就产生了由world state A
中PAP21
代表的一个商业票据;交互点2b。该操作在交互点2c被捕获为区块链的一项交易。 - 向网络发送一个
buy
交易,让债券BON31
来使用euroBond
合约。看交互点3a。这就产生了由world state B
中BON31
代表的一个商业票据;交互点3b。该操作在交互点3c被捕获为区块链的一项交易。 - 向网络发送一个
sell
交易,让债券BON41
来使用yenBond
合约。看交互点4a。这就产生了由world state B
中BON41
代表的一个商业票据;交互点4b。该操作在交互点4c被捕获为区块链的一项交易。
我们来看智能合约是如何与世界状态交互的:
euroPaper
和yenPaper
合约可直接访问world state A
,但无法直接访问world state B
。world state A
储存在与papers
链码相对应的数据库管理系统中的papers
数据库里面。euroBond
和yenBond
合约可以直接访问world state B
,但是无法直接访问world state A
。world state B
储存在与bonds
链码相对应的数据库管理系统中的bonds
数据库里面。
看看区块链是如何为所有世界状态捕获交易的:
- 与交易相对应的交互点1c和2c分别创建和更新商业票据
PAP11
和PAP21
。它们都包含在world state A
中。 - 与交易相对应的交互点3c和4c都会对
BON31
和BON41
进行更新。它们都包含在world state B
中。 - 如果
world state A
或world state B
因任何原因被破坏,可通过重新进行区块链上的所有交易来重新创建world state A
或world state B
。
跨区块链访问¶
如 情景中的示例可见, euroPaper
和 yenPaper
无法直接访问world state B
。这是因为我们对链码和智能合约进行了设置,使得这些链码和世界状态各自之间保持不同。不过,我们来想象一下 euroPaper
需要访问 world state B
。
为什么会发生这种情况呢?想象一下当发行商业票据时,智能合约想根据到期日相似的债券的当前收益情况来对票据进行定价。这种情况时,需要 euroPaper
合约能够在 world state B
中查询债券价格。看以下图表来研究一下应该如何设计交互的结构。
chaincodens.scenario 链码和智能合约如何能够直接访问其他世界状态——通过自己的链码。
注意如何:
- 应用程序在
euroPaper
智能合约中发送issue
交易来颁布PAP11
。看交互点1a。 euroPaper
智能合约中的issue
交易在euroBond
智能合约中调用query
交易。看交互点1b。euroBond
中的query
从world state B
中索取信息。看交互点1c。- 当控制返回到
issue
交易,它可利用响应中的信息对票据定价,并使用信息来更新world state A
。看交互点1d。 - 发行以日元定价的商务票据的控制流程也是一样的。看交互点2a,2b,2c和2d。
链码间的控制传递是利用invokeChiancode()
API(应用程序编程端口)来实现的。该API将控制权从一个链码传递到另一个链码。
虽然我们在示例中只谈论了查询交易,但是用户也能够调用一个将更新所谓链码世界状态的智能合约。请参考下面的 考虑 部分。
考虑¶
- 大体上来说,每各链码都包含一个智能合约。
- 若多个智能合约之间关系紧密,应该把它们部署在同一链码中。通常,只有当这些智能合约共享相同的世界状态时才需要这样做。
- 链码命名空间将不同的世界状态分隔开来。这大体上会将彼此不相关的数据区分开。注意,命名空间是由Hyperledger Fabric分配并直接映射到链码名字的,用户无法选择。
- 对于使用
invokeChaincode()
API的链码间交互,必须将这两个链码安装在相同节点上。- 对于仅需要查询被调用链码的世界状态的交互,该调用可发生在与调用发起者链码不同的通道上。
- 对于需要更新背调用链码世界状态的交互,调用必须发生在与调用发起者链码相同的通道上。
- 对于仅需要查询被调用链码的世界状态的交互,该调用可发生在与调用发起者链码不同的通道上。
交易处理器¶
受众:架构师,应用和智能合约开发者
交易处理器允许智能合约开发人员在应用程序和智能合约交互期间的关键点上定义通用处理。交易处理器是可选的,但是如果定义了,它们将在调用智能合约中的每个交易之前或之后接收控制权。还有一个特定的处理器,当请求调用未在智能合约中定义的交易时,该处理程序接收控制。
这里是一个商业票据智能合约示例的交易处理器的例子:
develop.transactionhandler
前置、后置和未知交易处理器。在这个例子中,
BeforeFunction()
在 issue, buy 和 redeem 交易之前被调用。AfterFunction()
在 issue, buy 和 redeem 交易之后调用。UnknownFunction()
当请求调用未在智能合约中定义的交易时调用。(通过不为每个交易重复 BeforeFunction
和 AfterFunction
框来简化该图)
处理器类型¶
有三种类型的交易处理器,它们涵盖应用程序和智能合约之间交互的不同方面:
- 前置处理器:在每个智能合约交易执行之前调用。该处理器通常用来改变交易使用的交易上下文。处理器可以访问所有 Fabric API;如,可以使用
getState()
和putState()
。 - 后置处理器:在每个智能合约交易执行之后调用。处理器通常会对所有的交易执行通用的后置处理,同样可以访问所有的 Fabric API。
- 未知处理器:试图执行未在智能合约中定义的交易时被调用。通常,处理器将记录管理员后续处理的失败。处理器可以访问所有的 Fabric API。
定义处理器¶
交易处理器作为具有明确定义名称的方法添加到智能合约中。这是一个添加每种类型的处理器的示例:
CommercialPaperContract extends Contract {
...
async beforeTransaction(ctx) {
// Write the transaction ID as an informational to the console
console.info(ctx.stub.getTxID());
};
async afterTransaction(ctx, result) {
// This handler interacts with the ledger
ctx.stub.cpList.putState(...);
};
async unknownTransaction(ctx) {
// This handler throws an exception
throw new Error('Unknown transaction function');
};
}
交易处理器定义的形式对于所有处理程序类型都是类似的,但请注意 afterTransaction(ctx,result)
如何接收交易返回的任何结果。
处理器处理¶
一旦处理器添加到智能合约中,它可以在交易处理期间调用。在处理期间,处理器接收 ctx
,即交易上下文,会执行一些处理,完成后返回控制权。继续如下的处理:
- 前置处理器:如果处理器成功完成,使用更新后的上下文调用交易。如果处理器抛出异常,不会调用交易,智能合约失败并显示异常错误消息。
- 后置处理器:如果处理器成功完成,则智能合约将按调用的交易确定完成。如果处理程序抛出异常,则交易将失败并显示异常错误消息。
- 未知处理器:处理器应该通过抛出包含所需错误消息的异常来完成。如果未指定未知处理器,或者未引发异常,则存在合理的默认处理;智能合约将以未知交易错误消息失败。
如果处理器需要访问函数和参数,这很容易做到:
async beforeTransaction(ctx) {
// Retrieve details of the transaction
let txnDetails = ctx.stub.getFunctionAndParameters();
console.info(`Calling function: ${txnDetails.fcn} `);
console.info(util.format(`Function arguments : %j ${stub.getArgs()} ``);
}
多处理器¶
只能为智能合约的每种类型至多定义一个处理器。如果智能合约需要在之前,之后或未知处理期间调用多个函数,它应该在相应的函数内协调它。
背书策略¶
本节内容所面向的读者包括:架构师,应用程序和智能合约开发人员
背书策略定义了要背书一项交易使其生效所需要的最小组织集合。要想对一项交易背书,组织的背书节点需要运行与该交易有关的智能合约,并对结果签名。当排序服务将交易发送给提交节点,节点们将各自检查该交易的背书是否满足背书策略。如果不满足的话,交易被撤销,不会对世界状态产生影响。
背书策略从以下两种不同的维度来发挥作用:既可以被设置为整个命名空间,也可被设置为单个状态键。它们是使用诸如 AND
和 OR
这样的逻辑表述来构成的。例如,在 PaperNet 中可以这样来用: MagnetoCorp 将一张票据卖给 DigiBank,其背书策略可被设定为 AND(MagnetoCorp.peer, DigiBank.peer)
,这就要求任何对该票据做出的改动需被 MagnetoCorp 和 DigiBank 同时背书。
连接配置文件¶
受众:架构师、应用程序和智能合约开发人员
连接配置文件描述了一组组件,包括 Hyperledger Fabric 区块链网络中的 Peer 节点、排序节点以及 CA。它还包含与这些组件相关的通道和组织信息。连接配置文件主要由应用程序用于配置处理所有网络交互的网关,从而使其可以专注于业务逻辑。连接配置文件通常由了解网络拓扑的管理员创建。
在本主题中,我们将介绍:
场景¶
连接配置文件用于配置网关。网关很重要,很多原因,主要是简化应用程序与网络通道的交互。
profile.scenario
两个应用程序,发行和购买,使用配置有连接配置文件1和2的网关1和2。每个配置文件描述了 MagnetoCorp 和 DigiBank 网络组件的不同子集。每个连接配置文件必须包含足够的信息,以便网关代表发行和购买应用程序与网络进行交互。有关详细说明,请参阅文本。
连接配置文件包含网络视图的描述,以技术语法表示,可以是 JSON 或 YAML。在本主题中,我们使用 YAML 表示,因为它更容易阅读。静态网关需要比动态网关更多的信息,因为后者可以使用服务发现来动态增加连接配置文件中的信息。
连接配置文件不应该是网络通道的详尽描述;它只需要包含足够的信息,足以满足使用它的网关。在上面的网络中,连接配置文件1需要至少包含背书组织和用于 issue
交易的 Peer 节点,以及识别将交易提交到帐本上时会通知网关的 Peer 节点。
最简单的方法是将连接配置文件视为描述网络的视图。这可能是一个综合观点,但由于以下几个原因,这是不现实的:
- 根据需求添加和删除 Peer 节点、排序节点、CA、通道和组织。
- 组件可以启动或停止,或意外失败(例如断电).
- 网关不需要整个网络的视图,只需要成功处理交易提交或事件通知所需的内容。
- 服务发现可以扩充连接配置文件中的信息。具体来说,动态网关可以配置最少的 Fabric 拓扑信息;其余的都可以被发现。
静态连接配置文件通常由详细了解网络拓扑的管理员创建。这是因为静态配置文件可能包含大量信息,管理员需要在相应的连接配置文件中获取到这些信息。相比之下,动态配置文件最小化所需的定义数量,因此对于想要快速掌握的开发人员或想要创建响应更快的网关的管理员来说,这是更好的选择。使用编辑器以 YAML 或 JSON 格式创建连接配置文件。
用法¶
我们将看到如何快速定义连接配置文件;让我们首先看看 MagnetoCorp 示例的 issue
程序如何使用它:
const yaml = require('js-yaml');
const { Gateway } = require('fabric-network');
const connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/paperNet.yaml', 'utf8'));
const gateway = new Gateway();
await gateway.connect(connectionProfile, connectionOptions);
加载一些必需的类后,查看如何从文件系统加载 paperNet.yaml
网关文件,使用 yaml.safeLoad()
方法转换为 JSON 对象,并使用其 connect()
方法配置网关。
通过使用此连接配置文件配置网关,发行应用程序为网关提供应用于处理交易的相关网络拓扑。这是因为连接配置文件包含有关 PaperNet 通道、组织、Peer 节点或排序节点和 CA 的足够信息,以确保可以成功处理交易。
连接配置文件为任何给定的组织定义多个 Peer 节点是一种比较好的做法,它可以防止单点故障。这种做法也适用于动态网关; 为服务发现提供多个起点。
DigiBank 的 buy
程序通常会为其网关配置类似的连接配置文件,但有一些重要的区别。一些元素将是相同的,例如通道;一些元素将重叠,例如背书节点。其他元素将完全不同,例如通知 Peer 节点或 CA。
传递给网关的 connectionOptions
补充了连接配置文件。它们允许应用程序声明网关如何使用连接配置文件。它们由 SDK 解释以控制与网络组件的交互模式,例如选择要连接的标识或用于事件通知的节点。了解可用连接选项列表以及何时使用它们。
结构¶
为了帮助您了解连接配置文件的结构,我们将逐步介绍上面显示的网络示例。其连接配置文件基于 PaperNet 商业票据样例,并存储在 GitHub 仓库中。为方便起见,我们在下面复制了它。您会发现在现在阅读它时,将它显示在另一个浏览器窗口中会很有帮助:
第9行:
name: "papernet.magnetocorp.profile.sample"
这是连接配置文件的名称。尝试使用 DNS 风格名称;它们是传达意义的一种非常简单的方式。
第16行:
x-type: "hlfv1"
用户可以添加自己的“特定于应用程序”的
x-
属性,就像 HTTP 头一样。它们主要供未来使用。第20行:
description: "Sample connection profile for documentation topic"
连接配置文件的简短描述。尽量让这对第一次看到这个的读者有所帮助!
第25行:
version: "1.0"
此连接配置文件的架构版本。目前仅支持版本1.0,并且未设想此架构将经常更改。
第32行:
channels:
这是第一个非常重要的行。
channels:
标识以下内容是此连接配置文件描述的所有通道。但是,最好将不同的通道保存在不同的连接配置文件中,特别是如果它们彼此独立使用。第36行:
papernet:
papernet
详细信息将是此连接配置文件中的第一个通道。第41行:
orderers:
有关
papernet
的所有排序节点的详细信息如下。您可以在第45行看到此通道的排序节点是orderer1.magnetocorp.example.com
。这只是一个逻辑名称;稍后在连接配置文件(第134-147行)中,将会有如何连接到此排序节点的详细信息。请注意orderer2.digibank.example.com
不在此列表中;应用程序使用自己组织的排序节点,而不是来自不同组织的排序节点,这是有道理的。第49行:
peers:
下边将介绍
papernet
所有 Peer 节点的详细信息。您可以看到 MagnetoCorp 列出的三个 Peer 节点:
peer1.magnetocorp.example.com
、peer2.magnetocorp.example.com
和peer3.magnetocorp.example.com
。没有必要列出 MagnetoCorp 中的所有 Peer 节点,就像这里所做的那样。您只能看到 DigiBank 中列出的一个 Peer 节点:peer9.digibank.example.com
; 包括这个 Peer 节点开始隐含背书策略要求 MagnetoCorp和DigiBank 背书交易,正如我们现在要确认的。最好有多个 Peer 节点来避免单点故障。在每个 peer 下面,您可以看到四个非独占角色:endorsingPeer、chaincodeQuery、ledgerQuery 和 eventSource。了解一下
peer1
和peer2
如何在主机papercontract
中执行所有角色。它们与peer3
不同,peer3
只能用于通知,或者用于访问帐本的链组件而不是世界状态的帐本查询,因此不需要安装智能合约。请注意peer9
不应该用于除背书之外的任何其他情况,因为 MagnetoCorp 的节点可以更好地服务于这些角色。再次,看看如何根据 Peer 节点的逻辑名称和角色来描述 Peer 节点。稍后在配置文件中,我们将看到这些 Peer 节点的物理信息。
第97行:
organizations:
所有组织的详细信息将适用与所有通道。请注意,这些组织适用于所有通道,即使
papernet
是目前唯一列出的组织。这是因为组织可以在多个通道中,通道可以有多个组织。此外,一些应用程序操作涉及组织而不是通道。例如,应用程序可以使用连接选项从其组织内的一个或所有 Peer 节点或网络中的所有组织请求通知。为此,需要有一个组织到 Peer 节点的映射,本节提供了它。第101行:
MagnetoCorp:
列出了属于 MagnetoCorp 的所有 Peer 节点:
peer1
、peer2
和peer3
。同样适用于证书颁发机构。再次注意逻辑名称用法,与channels:
部分相同;物理信息将在后面的配置文件中显示。第121行
DigiBank:
只有
peer9
被列为 DigiBank 的一部分,没有证书颁发机构。这是因为这些其他 Peer 节点和 DigiBank CA与此连接配置文件的用户无关。第134行:
orderers:
现在列出了排序节点的物理信息。由于此连接配置文件仅提到了
papernet
一个排序节点,您会看到列出的orderer1.magnetocorp.example.com
的详细信息。包括其 IP 地址和端口,以及可以覆盖与排序节点通信时使用的默认值的 gRPC 选项(如有必要)。对于peers:
为了实现高可用性,可以指定多个排序节点。第152行:
peers:
现在列出所有先前 Peer 节点的物理信息。此连接配置文件有三个 MagnetoCorp 的 Peer 节点:
peer1
、peer2
和peer3
;对于 DigiBank,单个 peerpeer9
列出了其信息。对于每个 Peer 节点,与排序节点一样,列出了它们的 IP 地址和端口,以及可以覆盖与特定节点通信时使用的默认值的 gRPC 选项(如有必要)。第194行:
certificateAuthorities:
现在列出了证书颁发机构的物理信息。连接配置文件为 MagnetoCorp 列出了一个 CA
ca1-magnetocorp
,然后是其物理信息。除了 IP 详细信息,注册商信息允许此 CA 用于证书签名请求(CSR)。这些都是用本地生成的公钥/私钥对来请求新证书。
现在您已经了解了 MagnetoCorp 的连接配置文件,您可能希望查看 DigiBank 的相关配置文件。找到与 MagnetoCorp 相同的配置文件的位置,查看它的相似之处,并比较最终哪里不同。想想为什么这些差异对 DigiBank 应用程序有作用。
这就是您需要了解的有关连接配置文件的所有信息。总之,连接配置文件为应用程序定义了足够的通道、组织、Peer 节点、排序节点和 CA 以配置网关。网关允许应用程序专注于业务逻辑而不是网络拓扑的细节。
示例¶
该文件是从 GitHub 商业票据示例中复制的。
1: ---
2: #
3: # [Required]. A connection profile contains information about a set of network
4: # components. It is typically used to configure gateway, allowing applications
5: # interact with a network channel without worrying about the underlying
6: # topology. A connection profile is normally created by an administrator who
7: # understands this topology.
8: #
9: name: "papernet.magnetocorp.profile.sample"
10: #
11: # [Optional]. Analogous to HTTP, properties with an "x-" prefix are deemed
12: # "application-specific", and ignored by the gateway. For example, property
13: # "x-type" with value "hlfv1" was originally used to identify a connection
14: # profile for Fabric 1.x rather than 0.x.
15: #
16: x-type: "hlfv1"
17: #
18: # [Required]. A short description of the connection profile
19: #
20: description: "Sample connection profile for documentation topic"
21: #
22: # [Required]. Connection profile schema version. Used by the gateway to
23: # interpret these data.
24: #
25: version: "1.0"
26: #
27: # [Optional]. A logical description of each network channel; its peer and
28: # orderer names and their roles within the channel. The physical details of
29: # these components (e.g. peer IP addresses) will be specified later in the
30: # profile; we focus first on the logical, and then the physical.
31: #
32: channels:
33: #
34: # [Optional]. papernet is the only channel in this connection profile
35: #
36: papernet:
37: #
38: # [Optional]. Channel orderers for PaperNet. Details of how to connect to
39: # them is specified later, under the physical "orderers:" section
40: #
41: orderers:
42: #
43: # [Required]. Orderer logical name
44: #
45: - orderer1.magnetocorp.example.com
46: #
47: # [Optional]. Peers and their roles
48: #
49: peers:
50: #
51: # [Required]. Peer logical name
52: #
53: peer1.magnetocorp.example.com:
54: #
55: # [Optional]. Is this an endorsing peer? (It must have chaincode
56: # installed.) Default: true
57: #
58: endorsingPeer: true
59: #
60: # [Optional]. Is this peer used for query? (It must have chaincode
61: # installed.) Default: true
62: #
63: chaincodeQuery: true
64: #
65: # [Optional]. Is this peer used for non-chaincode queries? All peers
66: # support these types of queries, which include queryBlock(),
67: # queryTransaction(), etc. Default: true
68: #
69: ledgerQuery: true
70: #
71: # [Optional]. Is this peer used as an event hub? All peers can produce
72: # events. Default: true
73: #
74: eventSource: true
75: #
76: peer2.magnetocorp.example.com:
77: endorsingPeer: true
78: chaincodeQuery: true
79: ledgerQuery: true
80: eventSource: true
81: #
82: peer3.magnetocorp.example.com:
83: endorsingPeer: false
84: chaincodeQuery: false
85: ledgerQuery: true
86: eventSource: true
87: #
88: peer9.digibank.example.com:
89: endorsingPeer: true
90: chaincodeQuery: false
91: ledgerQuery: false
92: eventSource: false
93: #
94: # [Required]. List of organizations for all channels. At least one organization
95: # is required.
96: #
97: organizations:
98: #
99: # [Required]. Organizational information for MagnetoCorp
100: #
101: MagnetoCorp:
102: #
103: # [Required]. The MSPID used to identify MagnetoCorp
104: #
105: mspid: MagnetoCorpMSP
106: #
107: # [Required]. The MagnetoCorp peers
108: #
109: peers:
110: - peer1.magnetocorp.example.com
111: - peer2.magnetocorp.example.com
112: - peer3.magnetocorp.example.com
113: #
114: # [Optional]. Fabric-CA Certificate Authorities.
115: #
116: certificateAuthorities:
117: - ca-magnetocorp
118: #
119: # [Optional]. Organizational information for DigiBank
120: #
121: DigiBank:
122: #
123: # [Required]. The MSPID used to identify DigiBank
124: #
125: mspid: DigiBankMSP
126: #
127: # [Required]. The DigiBank peers
128: #
129: peers:
130: - peer9.digibank.example.com
131: #
132: # [Optional]. Orderer physical information, by orderer name
133: #
134: orderers:
135: #
136: # [Required]. Name of MagnetoCorp orderer
137: #
138: orderer1.magnetocorp.example.com:
139: #
140: # [Required]. This orderer's IP address
141: #
142: url: grpc://localhost:7050
143: #
144: # [Optional]. gRPC connection properties used for communication
145: #
146: grpcOptions:
147: ssl-target-name-override: orderer1.magnetocorp.example.com
148: #
149: # [Required]. Peer physical information, by peer name. At least one peer is
150: # required.
151: #
152: peers:
153: #
154: # [Required]. First MagetoCorp peer physical properties
155: #
156: peer1.magnetocorp.example.com:
157: #
158: # [Required]. Peer's IP address
159: #
160: url: grpc://localhost:7151
161: #
162: # [Optional]. gRPC connection properties used for communication
163: #
164: grpcOptions:
165: ssl-target-name-override: peer1.magnetocorp.example.com
166: request-timeout: 120001
167: #
168: # [Optional]. Other MagnetoCorp peers
169: #
170: peer2.magnetocorp.example.com:
171: url: grpc://localhost:7251
172: grpcOptions:
173: ssl-target-name-override: peer2.magnetocorp.example.com
174: request-timeout: 120001
175: #
176: peer3.magnetocorp.example.com:
177: url: grpc://localhost:7351
178: grpcOptions:
179: ssl-target-name-override: peer3.magnetocorp.example.com
180: request-timeout: 120001
181: #
182: # [Required]. Digibank peer physical properties
183: #
184: peer9.digibank.example.com:
185: url: grpc://localhost:7951
186: grpcOptions:
187: ssl-target-name-override: peer9.digibank.example.com
188: request-timeout: 120001
189: #
190: # [Optional]. Fabric-CA Certificate Authority physical information, by name.
191: # This information can be used to (e.g.) enroll new users. Communication is via
192: # REST, hence options relate to HTTP rather than gRPC.
193: #
194: certificateAuthorities:
195: #
196: # [Required]. MagnetoCorp CA
197: #
198: ca1-magnetocorp:
199: #
200: # [Required]. CA IP address
201: #
202: url: http://localhost:7054
203: #
204: # [Optioanl]. HTTP connection properties used for communication
205: #
206: httpOptions:
207: verify: false
208: #
209: # [Optional]. Fabric-CA supports Certificate Signing Requests (CSRs). A
210: # registrar is needed to enroll new users.
211: #
212: registrar:
213: - enrollId: admin
214: enrollSecret: adminpw
215: #
216: # [Optional]. The name of the CA.
217: #
218: caName: ca-magnetocorp
连接选项¶
受众:架构师、管理员、应用程序与智能合约开发者
连接选项(Connection option)用于与连接配置文件联合使用去精准地控制一个网关如何与网络交互。通过使用网关,允许一个应用开发专注于商业逻辑而不是网络拓扑。
本主题中,我们将涉及:
方案¶
连接选项指定网关行为的特定方面。网关对于一些原因是重要,其主要目的是允许应用程序去聚焦在业务逻辑和智能合约,同时管理与网络的一些组件的交互。
profile.scenario
连接控制行为的不同交互点。这些选项将会在本文中被充分说明
一个连接选项的例子可能是想说明:被 issue
程序使用的网关应该使用 Isabella
身份将交易提交到 papernet
网络中。另一个可能是网关应该等待来自 MagnetoCorp 的三个节点去确定一个交易被提交并返回控制。连接选项允许应用程序去指定与网络交互的网关的精准行为。如果没有网关,应用程序需要去很多的工作;网关节省了你的时间,让你的应用程序更加可读,不易出错。
用法¶
我们将即刻描述应用程序可用的连接选项的全集;首先让我们看看他们如何被示例 MagnetoCorp issue
应用指定:
const userName = 'User1@org1.example.com';
const wallet = new FileSystemWallet('../identity/user/isabella/wallet');
const connectionOptions = {
identity: userName,
wallet: wallet,
eventHandlerOptions: {
commitTimeout: 100,
strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
}
};
await gateway.connect(connectionProfile, connectionOptions);
看一下 identity
和 wallet
选项是 connectionOptions
对象的简单属性。他们分别有 userName
and wallet
,他们较早的被设置在代码里。相比于这些属性,eventHandlerOptions
本身就是一个独立的对象。它有两个属性:commitTimeout: 100
(以秒为单位)和 strategy:EventStrategies.MSPID_SCOPE_ANYFORTX
。
看一下 connectionOptions
如何作为一个 connectionProfile
的补充被传递给网关的;网络被网络配置文件所标识,这些选项精确地指定了网关该如何与它交互。现在让我们看下可用选项。
选项¶
这里是可用选项的列表以及他们的作用。
wallet
标识了将要被应用程序上的网关所使用的钱包。看一下交互1,钱包是由应用程序指定的,但它实际上是检索身份的网关。一个钱包必须被指定;最重要的决定是钱包使用的类型,是文件系统、内存、HSM 还是数据库。
identity
应用程序从wallet
中使用的用户身份。看一下交互2a;用户身份被应用程序指定,也代表了应用程序的用户 Isabella,2b。身份实际上被网络检索。在我们的例子中,Isabella 的身份将会被不同的 MSP(2c,2d)使用,用于确定他为来自 MagnetoCorp 的一员,并且在其中扮演着一个特殊的角色。这两个事实将相应地决定了他在资源上的许可。
一个用户的身份必须被指定。正如你所看到的,这个身份对于 Hyperledger Fabric 是一个有权限的网络的概念来说是基本的原则——所有的操作者有一个身份,包括应用程序、Peer 节点和排序节点,这些身份决定了他们在资源上的控制。你可以在成员身份服务话题中阅读更过关于这方面的概念。
clientTIsIdentity
是可以从钱包(3a)获取到的身份,用于确保网关和不同的t通道组件之间的交流(3b),比如 Peer 节点和排序节点。注意:这个身份不同于用户身份。即使
clientTlsIdentity
对于安全通信来说很重要,但它并不像用户身份那样基础,因为它的范围没有超过确保网络的通信。clientTlsIdentity
是可选项。建议你把它设置进生产环境中。你应该也使用不同的clientTlsIdentity
用做identity
,因为这些身份有着非常不同的意义和生命周期。例如,如果clientTIsIdentity
被破坏,那么你的identity
也会被破坏;让他们保持分离会更加安全。eventHandlerOptions.commitTimeout
是可选的。它以秒为单位指定网关在将控制权返回给应用程序之前,应该等待任何对等方提交事务的最大时间量(4a)。用于通知的 Peer 节点集合由eventHandlerOptions
选项决定。如果没有指定 commitTimeout,网关默认将使用300秒的超时。eventHandlerOptions.strategy
是可选的。它定义了网关用来监听交易被提交通知的 Peer 节点集合。例如,是监听组织中的单一的 Peer 节点还是全部的节点。可以采用以下参数之一:EventStrategies.MSPID_SCOPE_ANYFORTX
监听用户组织内的一些 Peer 节点。在我们的例子中,看一下交互点4b;MagnetoCorp 中的 peer1、peer2、peer3 节点能够通知网关。EventStrategies.MSPID_SCOPE_ALLFORTX
这是一个默认值。监听用户组织内的所有 Peer 节点。在我们例子中,看一下交互点4b。所有来自 MagnetoCorp 的节点必须全部通知网关:peer1、peer2 和 peer3 节点。只有已知/被发现和可用的 Peer 节点才会被计入,停止或者失效的节点不包括在内。EventStrategies.NETWORK_SCOPE_ANYFORTX
监听整个网络通道内的一些 Peer 节点。在我们的例子中,看一下交互点 4b 和 4c;MagnetoCorp 中部分 Peer 节点1-3 或者 DigiBank 的部分 Peer 节点 7-9 能够通知网关。EventStrategies.NETWORK_SCOPE_ALLFORTX
监听整个网络通道内所有 Peer 节点。在我们的例子中,看一些交互 4b 和 4c。所有来自 MagnetoCorp 和 DigiBank 的 Peer 节点 1-3 和 Peer 节点 7-9 都必须通知网关。只有已知/被发现和可用的 Peer 节点才会被计入,停止或者失效的节点不包括在内。<
PluginEventHandlerFunction
> 用户定义的事件处理器的名字。这个允许用户针对事件处理而定义他们自己的逻辑。看一下如何定义一个时间处理器插件,并检验一个样本处理器。如果你有一个迫切需要事件处理的需求的话,那么用户定义的事件处理器是有必要的;大体上来说,一个内建的事件策略是足够的。一个用户定义的事件处理器的例子可能是等待超过半数的组织内的 Peer 节点,以去确认交易被提交。
如果你确实指定了一个用户定义的事件处理器,她不会影响你的一个应用程序的逻辑;它是完全独立的。处理器被处理过程中的SDK调用;它决定了什么时候调用它,并且使用它的处理结果去选择哪个 Peer 节点用于事件通知。当SDK完成它的处理的时候,应用程序收到控制。
如果一个用户定义的事件处理器没有被指定,那么
EventStrategies
的默认值将会被使用。discovery.enabled
是可选项,可能的值是true
还是false
。默认是ture
。它决定了网关是否使用服务发现去增加连接配置文件里指定的网络拓扑。看一下交互点6;peer 节点的 gossip 信息会被网关使用。这个值会被
INITIALIIZE-WITH-DISCOVERY
的环境变量覆盖,其值可以被设置成true
或者false
。discovery.asLocalhost
是可选项,可能的值是true
或者false
。默认是true
。它决定在服务发现期间发现的 IP 地址是否从 docker 网络传递给本地主机。通常,开发人员将为其网络组件(如 Peer 节点、排序节点 和 CA 节点)编写使用 docker 容器的应用程序,但是他们自己本身不会运行在 docker 容器内。这个就是为什么
true
是默认的,这生产环境中,应用程序很可能以网络组件相同的方式运行在 docker 中,因此不需要地址转换。在这种情况下,应用程序应该要不明确指定false
,要不使用环境变量来覆盖。这个值会被
DISCOVERY-AS-LOCALHOST
环境变量覆盖,其值可以被设置成true
或者false
。
注意事项¶
当打算选择连接选项的时候,下面的事项列表是有帮助的。
eventHandlerOptions.commitTimeout
和eventHandlerOptions.strategy
协同工作。例如,commitTimeout: 100
和strategy:EventStrategies.MSPID_SCOPE_ANYFORTX
意味着网关最多等待100秒,使得一些 Peer 节点确定交易被提交。相反,指定strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX
意味着网关将会等待所有组织里的所有 Peer 节点100秒。eventHandlerOptions.strategy:EventStrategies.MSPID_SCOPE_ALLFORTX
的默认值将等待应用程序内的组织的所有 Peer 节点提交提交交易。这是一个好的默认设置,因为应用程序能够确保所有的节点有一个最新账本的拷贝,最小的并发性问题。然而,当组织内的 Peer 节点数量增加,等待所有 Peer 节点则没有必要,在这种情况下,使用一个可插拔的事件处理器能够提供更多有效果的策略。例如,在一个公式能够保持所有帐本同步的安全假设下,相同的 Peer 节点集合能够用于提交交易和监听通知。
服务发现要求
clientTlsIdentity
被设置。因为与应用程序有交换信息的 Peer 节点需要确信他们在与信任的实体交换信息。如果clientTlsIdentity
没有被设置,那么discovery
不会生效,不管它是否被设置。即使应用程序在连接网关时能够设置连接选项,但是管理员可能需要覆盖这些选项。这是因为选项与网络交互有关,而网络交互可能随时间而变化。例如,管理员试图了解使用服务发现对网络性能的影响。
一个好的方式是在一个配置文件中定义应用程序覆盖,此文件在配置与网关的连接的时候,会被应用程序读取。
因为服务发现选项
enabled
和asLocalHost
最容易被管理员频繁的覆盖,环境变量INITIALIIZE-WITH-DISCOVERY
和DISCOVERY-AS-LOCALHOST
是因为方便而提供的。管理员应该在应用程序的生产环境中设置他们,其生产环境很可能是一个 docker 容器。
钱包¶
受众:架构师、应用程序和智能合约应用开发者们
一个钱包包括了一组用户身份。当与通道连接的时候,应用程序会从这些身份中选择一个用户来运行。对通道资源的访问权限,比如账本,由与 MSP(Membership provider)相关联的这个身份所定义。
在本主题中,我们将涉及:
场景¶
当应用程序连接到一个比如像 PaperNer 的网络通道的时候,它选择一个用户身份去这么做,例如 ID1
。通道的 MSP 将 ID1
与组织内特定的角色相关联,而且这个角色将最终决定应用程序在通道上的权限。、
wallet.scenario
两个用户, Isabella 和 Balaji 拥有包含能够用于连接不同网络通道的不同身份的钱包,PaperNet 和 BondNet。
思考一下这两个用户的例子;来自于 MagnetoCorp 的 lsabella 和来自 DigiBank 的 Balaji。Isabella 打算使用应用程序1去调用 PaperNet 里的智能合约和一个 BondNet 的智能合约。同理,Balaji 打算使用应用程序2去调用智能合约,但是这是在 PaperNet 中。(对于应用程序来说,访问多个网络和其中多个的智能合约是简单的。)
看一下:
- MagnetoCorp 使用 CA1 去颁发身份,DigiBank 使用 CA2 去颁发身份。这些身份被存储在用户的钱包中。
- Balaji 的钱包拥有一个唯一的身份,
ID4
由 CA2 发行。Isabella 的钱包拥有多种被 CA1 颁发的身份,ID1
、ID2
和ID3
。钱包能够让一个用户拥有多个身份,并且每一个身份都能由不同的 CA 颁发。 - Isabella 和 Balaji 都连接到 PaperNet,它(PaperNet)的 MSP 确定了 Isabella 是 MagnetoCorp 的成员,且确定了 Balaji 是 DigiBank 组织的一员,因为信任颁发他们身份的 CA。(一个组织使用多个 CA 或者一个 CA 支持多个组织是可能的)
- Isabella 能够使用
ID1
去连接 PaperNet 和 BondNet。在这两种情况下,当 Isabella 使用这个身份时候,她会被是识别为 MangetoCorp 的一员。 - Isabella 能够使用
ID2
去连接 BondNet,在这种情况下,她被标识为 MagnetoCorp 的管理员。这给了 Isabella 两种不同的权利:ID1
把她标识为能够读写 BondNet 账本的 MagnetoCorp 的普通一员,然而ID2
标识她为能够给 BondNet 添加组织的 MagnetoCorp 的管理员。 - Balaji 不能够使用
ID4
连接 BondNet。如果他尝试去连接,ID4
将不会被认可其属于 DigiBank,因为 CA2 不知道 BondNet 的 MSP。
类型¶
根据他们身份存储的位置,会有不同的钱包的类型。
wallet.types
四种不同的钱包类型:文件系统、内存、硬件安全模块和 CouchDB
文件系统(FileSystem):这是存储钱包最常见的地方;文件系统是无处不在的、容易理解且可以挂载在网络上。对于钱包来说,这是很好的默认选择。
使用
FileSystemWallet
class去管理文件系统钱包。内存(In-memory):存储在应用程序里的钱包。当你的应用程序正在运行在一个没有访问文件系统的约束环境的时候,使用这种类型的钱包;有代表性的是 web 浏览器。需要记住的是这种类型的钱包是不稳定的;在应用程序正常结束或者崩溃的时候,身份将失去丢失。
使用
InMemoryWallet
类 去管理内存钱包。硬件安全模块(HSM):一个钱包存储在 HSM。这个超安全、防干扰设备存储数字身份信息,特别是私钥。HSM 能够本地连接到你的电脑或可访问的网络。
一般你应该将
FileSystemWallet
类 与 HSMWalletMixin 类联合使用以管理 HSM 钱包。CouchDB:存储在 Couch DB 的钱包。这是最罕见的一种钱包存储形式,但是对于想去使用数据备份和恢复机制的用户来说,CouchDB 钱包能够提供一个有用的选择去简化崩溃的恢复。
使用
CouchDBWallet
类去管理 CouchDB 钱包.
结构¶
一个单一的钱包能够保存多个身份,每一个都被一个被指定的证书机构发放。每一个身份都有一个规范的带有描述性标签的结构,一个包括公钥、私钥与一些 Fabric-specific 元数据的 X.509 证书。不同的钱包类型 将这个结构合理地映射到他们地存储机制上。
wallet.structure
Fabric 钱包能够持有多个被不同证书机构颁发的身份,身份包含了证书、私钥和一些 Fabric 元数据
这是几个关键的类方法,使得钱包和身份更容易被管理:
const identity = X509WalletMixin.createIdentity('Org1MSP', certificate, key);
await wallet.import(identityLabel, identity);
看一下 X509WalletMixin.createIdentity()
方法如何创建一个包含元数据 Org1MSP
、证书
和一个私钥
的身份 。
网关
类只需要为一个身份设置一个 mspid
元数据,如上述例子中的 Org1MSP
。它当前使用这个值去识别来自于连接配置特定的 Peer 节点,例如,当一个特定的通知策略被要求的时候。在 DigiBank 网关文件 networkConnection.yaml
,看一下 Org1MSP
通知将如何与 peer0.org1.example.com
关联:
organizations:
Org1:
mspid: Org1MSP
peers:
- peer0.org1.example.com
你真的不需要担心内部不同的钱包类型的结构,但是如果你感兴趣,导航到商业票据例子里的用户身份文件夹:
magnetocorp/identity/user/isabella/
wallet/
User1@org1.example.com/
User1@org.example.com
c75bd6911aca8089...-priv
c75bd6911aca8089...-pub
你能够检查这些文件,但是正如讨论的,它很容易去使用 SDK 实现这些数据。
选项¶
一个共同的钱包基类衍生出不同的钱包类,该基类提供了管理身份的一套标准的 API 。这意味着可以使应用程序独立于底层钱包存储机制;比如,文件系统和 HSM 钱包被一个非常相似方法来处理。
wallet.operations
钱包遵循一个生命周期:他们能够被创建、被打开,身份能够被读取、添加、删除和导出。
应用程序能够根据一个简单的生命周期去使用钱包。钱包能够被打开、创建,随后可以添加、读取、更新、删除和导出身份。花费一些时间在 JSDOC 中不同的 Wallet
方法上来看一下他们是如何工作的;商业票据的教程在 addToWallet.js
中提供了一个很好的例子:
const wallet = new FileSystemWallet('../identity/user/isabella/wallet');
const cert = fs.readFileSync(path.join(credPath, '.../User1@org1.example.com-cert.pem')).toString();
const key = fs.readFileSync(path.join(credPath, '.../_sk')).toString();
const identityLabel = 'User1@org1.example.com';
const identity = X509WalletMixin.createIdentity('Org1MSP', cert, key);
await wallet.import(identityLabel, identity);
注意:
- 当程序第一次运行的时候,钱包被创建在本地文件系统
.../isabella/wallet
。 证书
和密钥
从文件系统中下载。- 一个新的身份使用
证书
,密钥
来创建,并且Org1MSP
使用X509WalletMixin.createIdentity()
。 - 新的身份通过
wallet.import()
导出带有User1@org1.example.com
标签的钱包中。
这是关于钱包你所要知道的所有事情。你已经看到用户代表在访问 Fabric 网络资源上,钱包如何持有被应用程序使用的身份的。根据你的应用程序和安全需要,这里有一些有用的不同类型的钱包,和一套简单的 API 去帮助应用程序去管理其内部的钱包和身份。
网关¶
受众:架构师,应用和智能合约开发者
网关代表应用程序管理与区块链网络的交互,使其能够专注于业务逻辑。应用程序连接到网关,然后使用该网关的配置管理所有后续交互。
本主题,将包括如下内容:
场景¶
Hyperledger Fabric 网络通道可以不断变化。Peer 节点、排序节点和 CA 组件,由网络中的不同组织提供,会经常变化。造成这种情况的原因包括业务需求增加或减少,以及计划内和计划外中断。网关为应用解除这种负担,使其能够专注于它试图解决的业务问题。
gateway.scenario
MagnetoCorp 和 DigiBank 应用程序(发行和购买)将各自的网络交互委托给其网关。每个网关都了解网络信道拓扑,包括 MagnetoCorp 和 DigiBank 两个组织的多个 Peer 节点 和 排序节点,使应用程序专注于业务逻辑。Peer 节点可以使用 gossip 协议在组织内部和组织之间相互通信。
应用程序可以通过两种不同的方式使用网关:
静态:网关配置完全定义在连接配置中。应用程序可用的所有 Peer 节点、排序节点和 CA 在用于配置网关的连接配置文件中静态定义。比如对 Peer 节点来说,这包括他们作为背书节点或事件通知节点的角色配置。你可以在连接配置主题中了解到更多关于角色的内容。
SDK 将使用此静态拓扑并结合网关连接配置来管理交易提交和通知过程。连接配置必须包括足够多的网络拓扑来允许网关以应用程序的身份和网络进行交互;包括网络通道、组织、排序节点、Peer 节点和他们的角色。
动态:网关配置被最小化的定义在连接配置中。通常,指定应用程序组织中的一个或两个 Peer 节点,然后使用服务发现来发现可用的网络拓扑。这包括 Peer 节点, 排序节点、通道、实例化过的智能合约和他们的背书策略。(在生产环境中,网关配置需要至少指定两个可用的 Peer 节点。)
SDK 将使用静态的配置和发现的拓扑信息,结合网关连接选项来管理交易提交和通知过程。作为其中的一部分,它还将智能地使用发现的拓扑;比如,它会使用为智能合约发现的背书策略来计算需要的最少背书节点。
你可能会问静态或动态网关哪一个更好?这取决于对可预测性和响应性的权衡。静态网络将始终以相同的方式运行,因为它们将网络视为不变的。从这个意义上讲,它们是可预测的——如果它们可用,它们将始终使用相同的 Peer 节点和排序节点。动态网络在了解网络如何变化时更具响应性——它们可以使用新添加的 Peer 节点和排序节点,从而带来额外的弹性和可扩展性,可能会带来一定的可预测性成本。一般情况下,使用动态网络比较好,实际上这是网关的默认模式。
请注意,静态或动态可以使用相同的连接配置文件。显然,如果配置文件被静态网关使用,它需要是全面的,而动态网关使用只需要很少的配置。
这两种类型的网关对应用程序都是透明的;无论是静态网关还是动态网关应用程序设计无序改变。意思就是一些应用可以使用服务发现,另外一些可以不使用。通常,SDK 使用动态发现意味着更少的定义和更加的智能;而这是默认的。
连接¶
当应用连接到一个网关,需要提供两个配置项。在随后的 SDK 处理程序中会使用到:
await gateway.connect(connectionProfile, connectionOptions);
连接配置:不管是静态网关还是动态网关,
connectionProfile
是被 SDK 用来做交易处理的网关配置项,它可以使用 YAML 或 JSON 进行配置,尽管在传给网关后会被转换成 JSON 对象。let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/paperNet.yaml', 'utf8'));
阅读更多关于连接配置的信息,了解如何配置。
连接选项:
connectionOptions
允许应用程序声明而不是实现所需的交易处理行为。连接选项被 SDK 解释以控制与网络组件的交互模式,比如选择哪个身份进行连接,使用哪个节点做事件通知。这些选项可在不影响功能的情况下显著地降低应用复杂度。这是可能的,因为 SDK 已经实现了许多应用程序所需的低级逻辑; 连接选项控制逻辑流程。阅读可用的连接选项列表以及何时使用它们。
静态网关¶
静态网关定义了一个固定的网络视图。在 MagnetoCorp 场景中,网关可以识别来自 MagnetoCorp 的单个 Peer 节点,来自 DigiBank 的单个 Peer 节点以及 MagentoCorp 的排序节点。或者,网关可以定义来自 MagnetCorp 和 DigiBank 的所有 Peer 节点和排序节点。在两个案例中,网关必须定义充分的网络视图来获取和分发商业票据的交易背书。
应用可以通过在 gateway.connect()
API 上明确指定连接选项 discovery: { enabled:false }
来使用静态网关。或者,在环境变量中设置 FABRIC_SDK_DISCOVERY=false
,将始终覆盖应用程序的选择。
检查被 MagnetoCorp 发行票据应用使用的连接选项。了解一下所有的 Peer 节点、排序节点和 CA 是如何配置的,包括他们的角色。
值得注意的是,静态网关代表这一刻的网络视图。随着网络的变化,在网关文件中反映变更可能很重要。当重新加载网关配置文件时,应用程序将会自动生效这些变更。
动态网关¶
动态网关为网络定义一个小的固定起点。在 MagnetoCorp 场景中,动态网关可能只识别来自 MagnetoCorp 的单个 Peer 节点; 其他一切都将被发现!(为了提供弹性,最好定义两个这样的引导节点。)
如果应用程序选择了服务发现,则网关文件中定义的拓扑将使用此进程生成的拓扑进行扩充。服务发现从网关定义开始,使用 gossip 协议查找 MagnetoCorp 组织内的所有连接的 Peer 节点和排序节点。如果已为某个通道定义了锚节点,则服务发现将使用跨组织的 goosip 协议来发现已连接组织的组件。此过程还将发现安装在 Peer 节点上的智能合约及其在通道级别定义的背书策略。与静态网关一样,发现的网络必须足以获取和分发商业票据交易背书。
动态网关是 Fabric 应用的默认设置。可在 gateway.connect()
API 上明确的指定连接选项配置 discovery: { enabled:true }
。或者,设置环境变量 FABRIC_SDK_DISCOVERY=true
,将会覆盖应用程序的选择。
动态网络代表了一个与时俱进的网络视图。随着网络的改变,服务发现将会确保网络视图精确反映应用程序可见的拓扑。应用程序将会自动生效这些变更;甚至不需要重载网关配置文件。
多网关¶
最后,应用程序可以直接为相同或不同的网络定义多个网关。此外,应用程序可以静态和动态地使用命名网关。
拥有多个网关可能会有所帮助。 原因如下:
- 代表不同用户处理请求。
- 同时连接不同的网络。
- 通过同时将其行为与现有配置进行比较来测试网络配置。
本主题介绍了如何使用 Hyperledger Fabric 开发客户端应用程序和智能合约来解决业务问题。实际的**商业票据**场景涉及到多个组织,你将了解到达成目的所需的所有概念和任务。我们假设区块链网络已经可用。
该主题为多个受众设计:
- 方案和应用架构师
- 客户端应用开发者
- 智能合约开发者
- 业务专家
你可以选择按顺序阅读,也可以根据需要选择各个部分阅读。各个部分是根据读者的相关性来进行标记的,所以 无论你是在寻找业务信息还是技术信息,都可以清楚地知道这个主题是否适合你。
该主题遵循典型的软件开发生命周期。它以业务需求开始,然后覆盖所有主要的技术活动以满足这些业务需要,这些主要的技术活动都是开发应用程序 和智能合约所必须的。
如果你愿意,只要您运行商业票据 [教程](https://hyperledger-fabric.readthedocs.io/en/latest/tutorial/commercial_paper.html). ,按照这份简短的解释说明来操作,就可以立即试用 商业票据场景。若您想了解该教程中所提及概念的详细解释,可返回这个主题。
教程¶
我们提供这个教程来帮助你开始学习 Hyperledger Fabric。第一部分是面向 Hyperledger Fabric 应用开发者 的, 编写你的第一个应用 。这一部分将带你体验使用 Node SDK 开发你的第一个 Hyperledger Fabric 区块链应用。
第二部分是面向 Hyperledger Fabric 网络管理员的, 构建你的第一个网络 。这一部分将带你体验使用 Hyperledger Fabric 搭建一个区块链网络并提供了一个基础的应用示例来进行测试。
还有更新通道的教程 向通道添加组织 和将你的网络升级为最新 Hyperledger Fabric 版本的教程 升级你的网络组件 。
最后,我们提供了两个链码教程。一个面向开发者 链码开发者教程 ,另一个面向管理员 链码操作者教程 。
注解
如果本文档不能解决你的问题,或者你使用本教程的过程中遇到了其他问题,请阅读 还有问题? 章节来寻求额外的帮助。
编写你的第一个应用¶
注解
如果你对 Fabric 网络的基本架构还不熟悉,在继续本部分之前,你可能想先 阅读 关键概念 部分。
本教程的价值仅限于介绍 Fabric 应用和使用简单的智能合约和应用。更深入 的了解 Fabric 应用和智能合约请查看 开发应用 或 商业票据教程 部分。
本教程我们将通过手动开发一个简单的示例程序来演示 Fabric 应用是如何工作的。
使用的这些应用和智能合约统称为 FabCar
。他们提供了理解 Hyperledger Fabric
区块链的一个很好的起点。我们将学习怎么写一个应用程序和智能合约来查询和更新账本,
还有如何使用证书授权服务来生成一个 X.509 证书,应用程序将使用这个证书和授权区块链
进行交互。
我们将使用应用程序 SDK —— 详细介绍在 应用 —— 使用智能 合约 SDK 来执行智能合约的查询和更新账本 —— 详细介绍在 —— 智能合约处理 。
我们将按照以下三个步骤进行:
1. 搭建开发环境。 我们的应用程序需要和网络交互,所以我们需要一个智能合约和 应用程序使用的基础网络。
![]()
2. 学习一个简单的智能合约, FabCar。 我们使用一个 JavaScript 写的智能合约。 我们将查看智能合约来学习他们的交易,还有应用程序是怎么使用他们来进行查询和更新账本的。
3. 使用 FabCar 开发一个简单的应用程序。 我们的应用程序将使用 FabCar 智能合约来查询和 更新账本上的汽车资产。我们将进入到应用程序的代码和他们创建的交易,包括查询一辆汽车, 查询一批汽车和创建一辆新车。
在完成这个教程之后,你将基本理解一个应用是如何通过编程关联智能合约来和 Fabric 网络上的多个节点的账本的进行交互的。
设置区块链网络¶
注解
下边的部分需要进入你克隆到本地的 fabric-samples
仓库的
first-network
子目录。
如果你已经学习了 构建你的第一个网络 ,你应该已经下载 fabric-samples
而且已经运行起来了一个网络。在你进行本教程之前,你必须停止这个网络:
./byfn.sh down
如果你之前运行过这个教程,使用下边的命令关掉所有停止或者在运行的容器。注意, 这将关掉你 所有 的容器,无论是否和 Fabric 有关。
docker rm -f $(docker ps -aq)
docker rmi -f $(docker images | grep fabcar | awk '{print $3}')
如果你没有网络和应用的开发环境和相关构件,访问 准备阶段 页面,确保你已经 在你的机器上安装了必要的依赖。
然后,如果已经完成了这些,访问 安装示例、二进制文件和 Docker 镜像 页面,跟着上边的说明操作。当你克隆
了 fabric-samples
仓库后返回本教程,然后下载最新的稳定版 Fabric 镜像和相关
工具。
如果你使用的是 Mac OS 和 Mojava,你需要 `install Xcode<./tutorial/installxcode.html>`_.
启动网络¶
注解
下边的章节需要进入你克隆到本地的 fabric-samples
仓库的 fabcar
子目录。
使用 startFabric.sh
启动你的网络。这个命令将启动一个区块链网络,这个网络由
peer 节点、排序节点和证书授权服务等组成。同时也将安装和初始化 javascript 版的
FabCar
智能合约,我们的应用程序将通过它来控制账本。我们将通过本教程学习更多
关于这些组件的内容。
./startFabric.sh javascript
好了,现在我们运行起来了一个示例网络,还有安装和初始化了 FabCar
智能合约。
为了使用我们的应用程序,我们现在需要安装一些依赖,同时我们也看一下这些程序是如
何一起工作的。
安装应用程序¶
注解
下边的章节需要进入你克隆到本地的 fabric-samples
仓库的
fabcar/javascript
子目录。
运行下边的命令来安装应用程序所需要的 Fabric 依赖。将要花费大约 1 分钟:
npm install
这个指令将安装应用程序的主要依赖,这些依赖定义在 package.json
中。其中最重要
的是 fabric-network
类;它使得应用程序可以使用身份、钱包和连接到通道的网关,
以及提交交易和等待通知。本教程也将使用 fabric-ca-client
类来注册用户以及他们
的授权证书,生成一个 fabric-network
在后边会用到的合法身份。
一旦 npm install
完成了,运行应用程序所需要的一切就准备好了。在这个教程中,
你将主要使用 fabcar/javascript
目录下的 JavaScript 文件来操作应用程序。
让我们来看一眼它里边有什么吧:
ls
你会看到下边的文件:
enrollAdmin.js node_modules package.json registerUser.js
invoke.js package-lock.json query.js wallet
里边也有一些其他编程语言的文件,比如在 fabcar/typescript
目录中。当你使用
过 JavaScript 示例之后,你可以看一下它们,主要的内容都是一样的。
如果你在使用 Mac OS 而且运行的是 Mojava ,你将 需要 install Xcode.
登记管理员用户¶
注解
下边的部分执行和证书授权服务器通讯。你在运行下边的程序时,你会发现,
打开一个新终端,并运行 docker logs -f ca.example.com
来查看 CA
的日志流,会很有帮助。
当我们创建网络的时候,一个管理员用户 — 叫 admin
— 被证书授权服务器(CA)
创建成了 登记员 。我们第一步要使用 enroll.js
程序为 admin
生成私钥、
公钥和 x.509 证书。这个程序使用一个 证书签名请求 (CSR) — 现在本地生成公钥
和私钥,然后把公钥发送到 CA ,CA 会发布会一个让应用程序使用的证书。这三个证书会
保存在钱包中,以便于我们以管理员的身份使用 CA 。
我们接下来会注册和登记一个新的应用程序用户,我们将使用这个用户来通过应用程序和 区块链交互。
我们登记一个 admin
用户:
node enrollAdmin.js
这个命令将 CA 管理员的证书保存在 wallet
目录。
注册和登记 user1
¶
注意我们在钱包里存放了管理员的证书,我们可以登记一个新用户 — user1
—
他将被用来查询和更新账本:
node registerUser.js
和管理员的登记类似,这个程序使用一个 CSR 来登记 user1
并把他的证书保存到 admin
所在的钱包里。我们现在有了两个独立的用户 — admin
和 user1
— 他们将用于
我们的应用程序。
账本交互时间。。。
查询账本¶
区块链网络中的每个节点都拥有一个账本的副本,应用程序可以通过执行智能合约查询账本 上最新的数据来实现来查询账本,并将查询结果返回给应用程序。
这里是一个查询工作如何进行的简单说明:

应用程序使用查询从 ledger 读取数据。最常用的查询是查 寻账本中询当前的值 – 也就是 world state 。 世界状态是一个键值对的集合,应用程序可以根据一个键或者多个键来查询数据。而且, 当键值对是以 JSON 值模式组织的时候,世界状态可以通过配置使用数据库(如 CouchDB ) 来支持富查询。这对于查询所有资产来匹配特定的键的值是很有用的,比如查询一个人的所 有汽车。
首先,我们来运行我们的 query.js
程序来返回账本上所有汽车的侦听。这个程序使用
我们的第二个身份 – user1
– 来操作账本。
node query.js
输入结果应该类似下边:
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
[{"Key":"CAR0", "Record":{"colour":"blue","make":"Toyota","model":"Prius","owner":"Tomoko"}},
{"Key":"CAR1", "Record":{"colour":"red","make":"Ford","model":"Mustang","owner":"Brad"}},
{"Key":"CAR2", "Record":{"colour":"green","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},
{"Key":"CAR3", "Record":{"colour":"yellow","make":"Volkswagen","model":"Passat","owner":"Max"}},
{"Key":"CAR4", "Record":{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}},
{"Key":"CAR5", "Record":{"colour":"purple","make":"Peugeot","model":"205","owner":"Michel"}},
{"Key":"CAR6", "Record":{"colour":"white","make":"Chery","model":"S22L","owner":"Aarav"}},
{"Key":"CAR7", "Record":{"colour":"violet","make":"Fiat","model":"Punto","owner":"Pari"}},
{"Key":"CAR8", "Record":{"colour":"indigo","make":"Tata","model":"Nano","owner":"Valeria"}},
{"Key":"CAR9", "Record":{"colour":"brown","make":"Holden","model":"Barina","owner":"Shotaro"}}]
让我们更进一步看一下这个程序。使用一个编辑器(比如, atom 或 visual studio)
打开 query.js
。
应用程序开始的时候就从 fabric-network
模块引入了两个关键的类
FileSystemWallet
和 Gateway
。这两个类将用于定位钱包中 user1
的身份,这个身份将用于连接网络。
const { FileSystemWallet, Gateway } = require('fabric-network');
应用程序通过网关连接网络:
const gateway = new Gateway();
await gateway.connect(ccp, { wallet, identity: 'user1' });
这段代码创建了一个新网关,然后通过它让应用程序连接到网络。 cpp
描述了网关将
通过 wallet
中的 user1
来使用网络。打开 ../../basic-network/connection.json
来查看 cpp
是如何解析一个 JSON 文件的:
const ccpPath = path.resolve(__dirname, '..', '..', 'basic-network', 'connection.json');
const ccpJSON = fs.readFileSync(ccpPath, 'utf8');
const ccp = JSON.parse(ccpJSON);
如果你想了解更多关于连接配置文件的结构,和它是怎么定义网络的,请查阅 the connection profile topic 。
一个网络可以被差分成很多通道,代码中下一个很重的一行是将应用程序连接到网络
中特定的通道 mychannel
上:
在这个通道中,我们可以通过 fabcar
智能合约来和账本进行交互:
const contract = network.getContract('fabcar');
在 fabcar
中有许多不同的 交易 ,我们的应用程序先使用 queryAllCars
交
易来查询账本世界状态的值:
const result = await contract.evaluateTransaction('queryAllCars');
evaluateTransaction
方法代表了一种区块链网络中和智能合约最简单的交互。它只是
的根据配置文件中的定义连接一个节点,然后向节点发送请求,请求内容将在节点中执行。
智能合约查询节点账本上的所有汽车,然后把结果返回给应用程序。这次交互没有导致账本
的更新。
FabCar 智能合约¶
让我们看一看 FabCar
智能合约里的交易。进入 fabric-samples
下的子目录
chaincode/fabcar/javascript/lib
,然后用你的编辑器打开 fabcar.js
。
看一下我们的智能合约是如何通过 Contract
类来定义的:
class FabCar extends Contract {...
在这个类结构中,你将看到定义了以下交易: initLedger
, queryCar
,
queryAllCars
, createCar
, and changeCarOwner
。例如:
async queryCar(ctx, carNumber) {...}
async queryAllCars(ctx) {...}
让我们更进一步看一下 queryAllCars
,看一下它是怎么和账本交互的。
async queryAllCars(ctx) {
const startKey = 'CAR0';
const endKey = 'CAR999';
const iterator = await ctx.stub.getStateByRange(startKey, endKey);
这段代码定义了 queryAllCars
将要从账本获取的汽车的范围。从 CAR0
到 CAR999
的每一辆车 – 一共 1000 辆车,假定每个键都被合适地锚定了 – 将会作为查询结果被返回。
代码中剩下的部分,通过迭代将查询结果打包成 JSON 并返回给应用。
下边将展示应用程序如何调用智能合约中的不同交易。每一个交易都使用一组 API 比如
getStateByRange
来和账本进行交互。了解更多 API 请阅读 detail.

你可以看到我们的 queryAllCars
交易,还有另一个叫做 createCar
。我们稍后将
在教程中使用他们来更细账本,和添加新的区块。
但是在那之前,返回到 query
程序,更改 evaluateTransaction
的请求来查询
CAR4
。 query
程序现在看起来应该是这个样子:
const result = await contract.evaluateTransaction('queryCar', 'CAR4');
保存程序,然后返回到 fabcar/javascript
目录。现在,再次运行 query
程序:
node query.js
你应该会看到如下:
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
如果你回头去看一下 queryAllCars
的交易结果,你会看到 CAR4
是 Adriana 的
黑色 Tesla model S,也就是这里返回的结果。
我们可以使用 queryCar
交易来查询任意汽车,使用它的键 (比如 CAR0
)得到车
辆的制造商、型号、颜色和车主等相关信息。
很棒。现在你应该已经了解了智能合约中基础的查询交易,也手动修改了查询程序中的参数。
账本更新时间。。。
更新账本¶
现在我们已经完成一些账本的查询和添加了一些代码,我们已经准备好更新账本了。有很多 的更新操作我们可以做,但是我们从创建一个 新 车开始。
从一个应用程序的角度来说,更新一个账本很简单。应用程序向区块链网络提交一个交易, 当交易被验证和提交后,应用程序会收到一个交易成功的提醒。但是在底层,区块链网络中 各组件中不同的 共识 程序协同工作,来保证账本的每一个更新提案都是合法的,而且 有一个大家一致认可的顺序。 .. image:: tutorial/write_first_app.diagram.2.png
上图中,我们可以看到完成这项工作的主要组件。同时,多个节点中每一个节点都拥有一 份账本的副本,并可选的拥有一份智能合约的副本,网络中也有一个排序服务。排序服务 保证网络中交易的一致性;它也将连接到网络中不同的应用程序的交易以定义好的顺序生 成区块。
我们对账本的的第一个更新是创建一辆新车。我们有一个单独的程序叫做 invoke.js
,
用来更新账本。和查询一样,使用一个编辑器打开程序定位到我们构建和提交交易到网络的
代码段:
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');
看一下应用程序如何调用智能合约的交易 createCar
来创建一量车主为 Tom 的黑
色 Honda Accord 汽车。我们使用 CAR12
作为这里的键,这也说明了我们不必使用
连续的键。
保存并运行程序:
node invoke.js
如果执行成功,你将看到类似输出:
Wallet path: ...fabric-samples/fabcar/javascript/wallet
2018-12-11T14:11:40.935Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "9076cd4279a71ecf99665aed0ed3590a25bba040fa6b4dd6d010f42bb26ff5d1"
Transaction has been submitted
注意 inovke
程序是怎样使用 submitTransaction
API 和区块链网络交互的,
而不是 evaluateTransaction
。
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');
submitTransaction
比 evaluateTransaction
要复杂的多。不只是和单个节点
交互,SDK 将把 submitTransaction
提案发送到区块链网络中每一个必要的组织的
节点。每一个节点都将根据这个提案执行请求的智能合约,并生成一个该节点签名的交易
响应并返回给 SDK 。SDK 将所有经过签名的交易响应收集到一个交易中,这个交易将会
被发送到排序节点。排序节点搜集并排序每个应用的交易,并把这些交易放入到一个交易
区块。然后排序节点将这些区块分发到网络中的节点,每一笔交易都会在节点中进行验证
和提交。最后,SDK 会收到提醒,并把控制权返回给应用程序。
注解
submitTransaction
也包含一个监听者来检查交易是否已经通过验证并提交到账本。应用程序可以利用提交监听或者像 submitTransaction
这样的 API 来实现这个功能。如果没有这个功能,你的交易可能会没有被成功排序、验证或者提交到账本,而你却无法获知。
应用程序中的这些工作由 submitTransaction
完成!应用程序、智能合约、节点和
排序服务一起工作来保证网络中账本一致性的程序被称为共识,它的详细解释在这里
section 。
为了查看这个被写入账本的交易,返回到 query.js
并将参数 CAR4
更改为 CAR12
。
就是说,将:
const result = await contract.evaluateTransaction('queryCar', 'CAR4');
改为:
const result = await contract.evaluateTransaction('queryCar', 'CAR12');
再次保存,然后查询:
node query.js
应该返回这些:
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"colour":"Black","make":"Honda","model":"Accord","owner":"Tom"}
恭喜。你创建了一辆汽车并验证了它记录在账本上!
现在我们已经完成了,我们假设 Tom 很大方,想把他的 Honda Accord 送给一个 叫 Dave 的人。
为了完成这个,返回到 invoke.js
然后利用输入的参数,将智能合约的交易从
createCar
改为 changeCarOwner
:
await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');
第一个参数 — CAR12
— 表示将要易主的车。第二个参数 — Dave
— 表示
车的新主人。
再次保存并执行程序:
node invoke.js
现在我们来再次查询账本,以确定 Dave 和 CAR12
键已经关联起来了:
node query.js
将返回如下结果:
Wallet path: ...fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is:
{"colour":"Black","make":"Honda","model":"Accord","owner":"Dave"}
CAR12
的主人已经从 Tom 变成了 Dave。
注解
在真实实现的应用中,智能合约应该有一些权限控制。例如只有特定权限的用户才可以创建新车,只有车辆的拥有者才可以将车辆转移给他人。
总结¶
现在我们完成了一些查询和更新,你应该已经比较了解如何通过智能合约和区块链网络进 行交互来查询和更新账本。我们已经看过了查询和更新的基本角智能合约、API 和 SDK , 你也应该对如何在其他的商业场景和操作中使用不同应用有了一些认识。
商业票据教程¶
受众: 架构师,应用和智能合约开发者,管理员
本教程将向你展示如何安装和使用商业票据样例应用程序和智能合约。该主题是以任务为导向的, 因此它更侧重的是流程而不是概念。如果你想更深入地了解这些概念,可以阅读开发应用程序主题。
commercialpaper.tutorial
在本教程中,MagnetoCorp 和 DigiBank 这两个组织使用 Hyperledger Fabric 区块链网络 PaperNet 相互交易商业票据。
一旦建立了一个基本的网络,你就将扮演 MagnetoCorp 的员工 Isabella,她将代表公司发行商业票据。然后,你将转换角色,担任 DigiBank 的员工 Balaji,他将购买此商业票据,持有一段时间,然后向 MagnetoCorp 兑换该商业票据,以获取小额利润。
你将扮演开发人员,最终用户和管理员,这些角色位于不同组织中,都将执行以下步骤,这些步骤旨在帮助你了解作为两个不同组织独立工作但要根据Hyperledger Fabric 网络中双方共同商定的规则来进行协作是什么感觉。
- 组建机器和下载示例
- 创建网络
- 理解智能合约的结构
- 作为组织 [MagnetoCorp](#像 MagnetoCorp 一样工作) 来安装和实例化智能合约
- 理解 MagnetoCorp 应用的结构,包括它的依赖项
- 配置并使用钱包和身份
- 启动 MagnetoCorp 的应用程序发行商业票据
- 理解第二个组织 [Digibank](#像 DigiBank 一样工作) 是如何在它们的[应用](#Digibank 应用)中使用智能合约的
- 作为 Digibank, [运行](#像 DigiBank 一样运行)购买和兑换商业票据的应用程序
本教程已经在 MacOS 和 Ubuntu 上进行了测试,应该可以在其他 Linux 发行版上运行。Windows版本的教程正在开发中。
先决条件¶
在开始之前,你必须安装本教程所需的一些必备技术。我们将必备技术控制在最低限度,以便你能快速开始。
你必须安装以下软件:
- Node 版本 8.9.0 或更高版本。Node 是一个 Javascript 运行时,可用于运行应用程序和智能合约。推荐使用 node 的 TLS 版本。安装 node 看这里。
- Docker 版本 18.06 或更高版本。Docker 帮助开发人员和管理员创建标准环境,以构建和运行应用程序和智能合约。提供Hyperledger Fabric 是作为一组Docker 镜像的,PaperNet 智能合约将在 docker 容器中运行。安装 Docker 看这里。
你会发现安装以下软件很有帮助:
源码编辑器,如 Visual Studio Code 版本 1.28,或更高版本。 VS Code 将会帮助你开发和测试你的应用程序和智能合约。安装 VS Code 看这里。
许多优秀的代码编辑器都可以使用,包括 Atom, Sublime Text 和 Brackets。
你可能会发现,随着你在应用程序和智能合约开发方面的经验越来越丰富,安装以下软件会很有帮助。 首次运行教程时无需安装这些:
- Node Version Manager。NVM 帮助你轻松切换不同版本的 node – 如果你同时处理多个项目的话,那将非常有用。安装 NVM 看这里。
下载示例¶
商业票据教程是在名为 fabric-samples
的公共 Github 仓库中保存的 Hyperledger Fabric 示例之一。当你要在你的机器上运行教程时,
你的首要任务是下载 fabric-samples
仓库。
commercialpaper.download 把
fabric-samples
GitHub 仓库下载到你的本地机器
$GOPATH
Hyperledger Fabric 中重要的环境变量;它识别了要安装的根目录。无论您使用哪种
编程语言,都必须正确行事!打开一个新的终端窗口,然后使用 env
命令检查一下你的 $GOPATH
:
$ env
...
GOPATH=/Users/username/go
NVM_BIN=/Users/username/.nvm/versions/node/v8.11.2/bin
NVM_IOJS_ORG_MIRROR=https://iojs.org/dist
...
如果你的 $GOPATH
没有设置,请看这个说明。
现在,你可以创建一个相对于 $GOPATH
的目录,将在其中安装 fabric-samples
:
$ mkdir -p $GOPATH/src/github.com/hyperledger/
$ cd $GOPATH/src/github.com/hyperledger/
使用 git clone
命令把 fabric-samples
仓库复制到这个地址:
$ git clone https://github.com/hyperledger/fabric-samples.git
随意检查 fabric-samples
的目录结构:
$ cd fabric-samples
$ ls
CODE_OF_CONDUCT.md balance-transfer fabric-ca
CONTRIBUTING.md basic-network first-network
Jenkinsfile chaincode high-throughput
LICENSE chaincode-docker-devmode scripts
MAINTAINERS.md commercial-paper README.md
fabcar
注意 commercial-paper
目录 – 我们的示例就在这里!
现在你已经完成了教程的第一个阶段!随着你继续操作,你将为不同用户和组件打开多个命令窗口。例如:
- 以 Isabella 和 Balaji 的身份运行应用程序,他们将相互交易商业票据
- 以 MagnetoCorp 和 DigiBank 管理员的身份发行命令,包括安装和实例化智能合约
- 展示 peer, orderer 和 CA 的日志输出
当你应该从特定命令窗口运行一项命令时,我们将详细说明这一点。例如:
(isabella)$ ls
这表示你应该在 Isabella 的窗口中执行 ls
命令。
创建网络¶
这个教程目前使用的是基础网络;很快将会更新配置,从而更好的反映出 PaperNet 的多组织结构。 但就目前来说,这个网络已经能够满足向你展示如何开发应用程序和智能合约。
commercialpaper.network The Hyperledger Fabric 基础网络的组成部分包括一个节点及该节点的账本数据库,一个排序服务和一个证书授权中心。以上每个组件都在 一个Docker 容器中运行。
节点及其账本,排序服务和 CA 都是在自己的docker 容器中运行。在生产环境中,组织通常使用的是与其他系统共享的现有 CA;它们并非专门用于 Fabric 网络的。
你可以使用 fabric-samples\basic-network
目录下的命令和配置来管理基础网络。让我们在你本地的机器上使用 start.sh
脚本来启动网络:
$ cd fabric-samples/basic-network
$ ./start.sh
docker-compose -f docker-compose.yml up -d ca.example.com orderer.example.com peer0.org1.example.com couchdb
Creating network "net_basic" with the default driver
Pulling ca.example.com (hyperledger/fabric-ca:)...
latest: Pulling from hyperledger/fabric-ca
3b37166ec614: Pull complete
504facff238f: Pull complete
(...)
Pulling orderer.example.com (hyperledger/fabric-orderer:)...
latest: Pulling from hyperledger/fabric-orderer
3b37166ec614: Already exists
504facff238f: Already exists
(...)
Pulling couchdb (hyperledger/fabric-couchdb:)...
latest: Pulling from hyperledger/fabric-couchdb
3b37166ec614: Already exists
504facff238f: Already exists
(...)
Pulling peer0.org1.example.com (hyperledger/fabric-peer:)...
latest: Pulling from hyperledger/fabric-peer
3b37166ec614: Already exists
504facff238f: Already exists
(...)
Creating orderer.example.com ... done
Creating couchdb ... done
Creating ca.example.com ... done
Creating peer0.org1.example.com ... done
(...)
2018-11-07 13:47:31.634 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2018-11-07 13:47:31.730 UTC [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel
注意 docker-compose -f docker-compose.yml up -d ca.example.com...
命令如何从DockerHub中拉取然后启动了4 个 Hyperledger Fabric 容器镜像。这些容器都使用了 Hyperledger Fabric 组件的最新版本软件。欢迎浏览 basic-network
目录 – 我们将在本教程中使用该目录的大部分内容。
你可以使用 docker ps
命令列出运行基本网络组件的 docker 容器:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ada3d078989b hyperledger/fabric-peer "peer node start" About a minute ago Up About a minute 0.0.0.0:7051->7051/tcp, 0.0.0.0:7053->7053/tcp peer0.org1.example.com
1fa1fd107bfb hyperledger/fabric-orderer "orderer" About a minute ago Up About a minute 0.0.0.0:7050->7050/tcp orderer.example.com
53fe614274f7 hyperledger/fabric-couchdb "tini -- /docker-ent…" About a minute ago Up About a minute 4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp couchdb
469201085a20 hyperledger/fabric-ca "sh -c 'fabric-ca-se…" About a minute ago Up About a minute 0.0.0.0:7054->7054/tcp ca.example.com
看看你是否可以将这些容器映射到基本网络上(可能需要横向移动才能找到信息):
- 节点
peer0.org1.example.com
在容器ada3d078989b
中运行 - 排序服务
orderer.example.com
在容器1fa1fd107bfb
中运行 - CouchDB 数据库
couchdb
在容器53fe614274f7
中运行 - CA
ca.example.com
在容器469201085a20
中运行
所有这些容器构成了被称作 net_basic
的 docker 网络。你可以使用 docker network
命令查看该网络:
$ docker network inspect net_basic
{
"Name": "net_basic",
"Id": "62e9d37d00a0eda6c6301a76022c695f8e01258edaba6f65e876166164466ee5",
"Created": "2018-11-07T13:46:30.4992927Z",
"Containers": {
"1fa1fd107bfbe61522e4a26a57c2178d82b2918d5d423e7ee626c79b8a233624": {
"Name": "orderer.example.com",
"IPv4Address": "172.20.0.4/16",
},
"469201085a20b6a8f476d1ac993abce3103e59e3a23b9125032b77b02b715f2c": {
"Name": "ca.example.com",
"IPv4Address": "172.20.0.2/16",
},
"53fe614274f7a40392210f980b53b421e242484dd3deac52bbfe49cb636ce720": {
"Name": "couchdb",
"IPv4Address": "172.20.0.3/16",
},
"ada3d078989b568c6e060fa7bf62301b4bf55bed8ac1c938d514c81c42d8727a": {
"Name": "peer0.org1.example.com",
"IPv4Address": "172.20.0.5/16",
}
},
"Labels": {}
}
看看这四个容器如何在作为单个docker网络一部分的同时使用不同的IP地址。(为了清晰起见,我们对输出进行了缩写。)
回顾一下: 你已经从 GitHub 下载了 Hyperledger Fabric 样本仓库,并且已经在本地机器上运行了基本的网络。现在让我们开始扮演 MagnetoCorp 的角色来交易商业票据。
像 MagnetoCorp 一样工作¶
为了监控 PaperNet 网络中的 MagnetoCorp 组件,管理员可以使用 logspout
工具 来查看一组 docker 容器日志的聚合结果。该工具可以将不同输出流采集到一个地方,从而在一个窗口中就可以轻松看到正在发生的事情。比如,对于正在安装智能合约的管理员或者正在调用智能合约的开发人员来说,这个工具确实很有帮助。
现在让我们作为 MagnetoCorp 的管理员来监控 PaperNet 网络。在 fabric-samples
目录下打开一个新窗口,找到并运行 monitordocker.sh
脚本,以启动与 docker 网络 net_basic
关联的 PaperNet docker 容器的 logspout
工具:
(magnetocorp admin)$ cd commercial-paper/organization/magnetocorp/configuration/cli/
(magnetocorp admin)$ ./monitordocker.sh net_basic
...
latest: Pulling from gliderlabs/logspout
4fe2ade4980c: Pull complete
decca452f519: Pull complete
(...)
Starting monitoring on all containers on the network net_basic
b7f3586e5d0233de5a454df369b8eadab0613886fc9877529587345fc01a3582
注意,如果 monitordocker.sh
中的默认端口已经在使用,你可以向以上命令中传一个端口号码。
(magnetocorp admin)$ ./monitordocker.sh net_basic <port_number>
这个窗口将会显示 docker 容器的输出,所以让我们启动另一个终端窗口来让 MagnetoCorp 的管理员和 网络交互。
commercialpaper.workmagneto MagnetoCorp 管理员通过一个 docker 容器和网络交互。
为了和 PaperNet 交互,MagnetoCorp 管理员需要使用 Hyperledger Fabric peer
命令。而这些命令
都可以在 hyperledger/fabric-tools
docker 镜像中预先构建,所以非常方便。
让我们使用 docker-compose
命令来为管理员启动一个 MagnetoCorp 特定的 docker 容器:
(magnetocorp admin)$ cd commercial-paper/organization/magnetocorp/configuration/cli/
(magnetocorp admin)$ docker-compose -f docker-compose.yml up -d cliMagnetoCorp
Pulling cliMagnetoCorp (hyperledger/fabric-tools:)...
latest: Pulling from hyperledger/fabric-tools
3b37166ec614: Already exists
(...)
Digest: sha256:058cff3b378c1f3ebe35d56deb7bf33171bf19b327d91b452991509b8e9c7870
Status: Downloaded newer image for hyperledger/fabric-tools:latest
Creating cliMagnetoCorp ... done
再次查看如何从 Docker Hub 中检索 hyperledger/fabric-tools
docker 镜像并将其添加到网络中:
(magnetocorp admin)$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
562a88b25149 hyperledger/fabric-tools "/bin/bash" About a minute ago Up About a minute cliMagnetoCorp
b7f3586e5d02 gliderlabs/logspout "/bin/logspout" 7 minutes ago Up 7 minutes 127.0.0.1:8000->80/tcp logspout
ada3d078989b hyperledger/fabric-peer "peer node start" 29 minutes ago Up 29 minutes 0.0.0.0:7051->7051/tcp, 0.0.0.0:7053->7053/tcp peer0.org1.example.com
1fa1fd107bfb hyperledger/fabric-orderer "orderer" 29 minutes ago Up 29 minutes 0.0.0.0:7050->7050/tcp orderer.example.com
53fe614274f7 hyperledger/fabric-couchdb "tini -- /docker-ent…" 29 minutes ago Up 29 minutes 4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp couchdb
469201085a20 hyperledger/fabric-ca "sh -c 'fabric-ca-se…" 29 minutes ago Up 29 minutes 0.0.0.0:7054->7054/tcp ca.example.com
MagnetoCorp 管理员在容器 562a88b25149
中使用命令行和 PaperNet 交互。同样也注意 logspout
容器 b7f3586e5d02
;它将为 monitordocker.sh
命令捕获来自其他所有容器的输出。
现在让我们作为 MagnetoCorp 的管理员使用命令行和 PaperNet 交互吧。
智能合约¶
issue
, buy
和 redeem
是 PaperNet 智能合约的三个核心功能。应用程序使用这些功能来提交交易,相应地,在账本上会发行、购买和赎回商业票据。我们接下来的任务就是检查这个智能合约。
打开一个新的终端窗口来代表 MagnetoCorp 开发人员,然后切换到包含 MagnetoCorp 的智能合约拷贝的目录,使用你选定的编辑器进行查看(这个教程用的是 VS Code)。
(magnetocorp developer)$ cd commercial-paper/organization/magnetocorp/contract
(magnetocorp developer)$ code .
在这个文件夹的 lib
目录下,你将看到 papercontract.js
文件 – 其中包含了商业票据智能合约!
commercialpaper.vscode1 一个示例代码编辑器在
papercontract.js
文件中展示商业票据智能合约
papercontract.js
是一个在 node.js 环境中运行的 JavaScript 程序。注意下面的关键程序行:
const { Contract, Context } = require('fabric-contract-api');
这个语句引入了两个关键的 Hyperledger Fabric 类 –
Contract
和Context
,它们被智能合约广泛使用。你可以在fabric-shim
JSDOCS 中了解到这些类的更多信息。class CommercialPaperContract extends Contract {
这里基于内置的 Fabric
Contract
类定义了智能合约类CommercialPaperContract
。实现了issue
,buy
和redeem
商业票据关键交易的方法被定义在该类中。async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime...) {
这个方法为 PaperNet 定义了商业票据
issue
交易。传入的参数用于创建新的商业票据。找到并检查智能合约内的buy
和redeem
交易。let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime...);
在
issue
交易内部,这个语句根据提供的交易输入使用CommercialPaper
类在内存中创建了一个新的商业票据。检查buy
和redeem
交易看如何类似地使用该类。await ctx.paperList.addPaper(paper);
这个语句使用
ctx.paperList
在账本上添加了新的商业票据,其中ctx.paperList
是PaperList
类的一个实例,当智能合约场景CommercialPaperContext
被初始化时,就会创建出一个ctx.paperList
。再次检查buy
和redeem
方法,以了解这些方法是如何使用这一类的。return paper.toBuffer();
该语句返回一个二进制缓冲区,作为来自
issue
交易的响应,供智能合约的调用者处理。
欢迎检查 contract
目录下的其他文件来理解智能合约是如何工作的,请仔细阅读智能合约主题中 papercontract.js
是如何设计的。
安装合约¶
要想使应用程序能够调用 papercontract
合约,必须先在 PaperNet 中合适的peer节点上安装该合约。MagnetoCorp 和 DigiBank 的管理员能够将 papercontract
安装到他们各自拥有权限的节点上。
commercialpaper.install MagnetoCorp 的管理员将
papercontract
的一个副本安装在 MagnetoCorp 的节点上。
智能合约是应用开发的重点,它被包含在一个名为链码的 Hyperledger Fabric 组件中。在一个链码中可以定义一个或多个智能合约,安装链码就使得 PaperNet 中的不同组织可以使用其中的智能合约。这意味着只有管理员需要关注链码;其他人都只需关注智能合约。
MagnetoCorp 的管理员使用 peer chaincode install
命令将 papercontract
智能合约从他们的本地机器文件系统中复制到目标节点 docker 容器的文件系统中。一旦智能合约被安装在节点上且在通道上进行实例化,应用程序就可以调用 papercontract
智能合约,然后通过 Fabric 的 API putState() 和 getState() 与账本数据库进行交互。
检查一下 StateList
类是如何在 ledger-api\statelist.js
中使用这些 API 的。
现在让我们以 MagnetoCorp 的管理员身份来安装 papercontract
智能合约。在 MagnetoCorp 管理员的命令窗口中,使用 docker exec
命令在 cliMagnetCorp
容器中运行 peer chaincode install
命令:
(magnetocorp admin)$ docker exec cliMagnetoCorp peer chaincode install -n papercontract -v 0 -p /opt/gopath/src/github.com/contract -l node
2018-11-07 14:21:48.400 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2018-11-07 14:21:48.400 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
2018-11-07 14:21:48.466 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response:<status:200 payload:"OK" >
cliMagnetCorp
容器设置了 CORE_PEER_ADDRESS=peer0.org1.example.com:7051
来将其命令指向 peer0.org1.example.com
,INFO 003 Installed remotely...
表示 papercontract
已经成功安装在节点上。现在,MagnetoCorp 的管理员只需要在 MagentoCorp 的一个节点上安装 papercontract
的副本。
注意 peer chaincode install
命令是如何指定智能合约的路径的,-p
参数是相对于 cliMagnetoCorp
容器的文件系统:/opt/gopath/src/github.com/contract
。这个路径已经通过 magnetocorp/configuration/cli/docker-compose.yml
文件映射到本地文件系统通道 .../organization/magnetocorp/contract
上。
volumes:
- ...
- ./../../../../organization/magnetocorp:/opt/gopath/src/github.com/
- ...
看一下 volume
指令是如何将 organization/magnetocorp
映射到 /opt/gopath/src/github.com/
中,从而向该容器提供访问你本地文件系统的权限,你的本地文件系统中存储了 MagnetoCorp 的 papercontract
智能合约副本。
你可以在这里阅读更多关于 docker compose
命令的内容,在这里阅读更多关于 peer chaincode install
命令的内容。
实例化合约¶
既然已经将包含 CommercialPaper
智能合约的 papercontract
链码安装在所需的 PaperNet 节点上, 管理员可以把它提供给不同的网络通道来使用,这样一来,连接到这些通道上的应用软件就能够调用此链码。因为我们在 PaperNet 中所用的是最基本的网络配置,所以只会把 papercontract
提供到 mychannel
这一个网络通道上。commercialpaper.instant MagnetoCorp 的一名管理员将包含此智能合约的
papercontract
链码进行实例化。创建一个新的docker容器来运行papercontract
。
MagnetoCorp 的管理员使用 peer chaincode instantiate
命令来在 mychannel
上实例化 papercontract
:
(magnetocorp admin)$ docker exec cliMagnetoCorp peer chaincode instantiate -n papercontract -v 0 -l node -c '{"Args":["org.papernet.commercialpaper:instantiate"]}' -C mychannel -P "AND ('Org1MSP.member')"
2018-11-07 14:22:11.162 UTC [chaincodeCmd] InitCmdFactory -> INFO 001 Retrieved channel (mychannel) orderer endpoint: orderer.example.com:7050
2018-11-07 14:22:11.163 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default escc
2018-11-07 14:22:11.163 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default vscc
这个命令可能要花费几分钟时间。
instantiate
中最重要的参数之一是 -P
。该参数为 papercontract
指明了背书策略,它描述了在认定一项交易为有效之前必须对其背书(执行和签名)的一群组织。所有交易,无论有效与否,都将被记录在 账本区块链上,但仅有效交易会更新世界状态。
在传递阶段,看看 instantiate
如何传递排序服务地址 orderer.example.com:7050
。这是因为instantiate
向排序服务额外提交了一项实例化交易,排序服务将把该交易包括到下一个区块中,并将其分布到所有加入了 mychannel
的节点上,使得所有节点都可以在各自独立的链码容器中执行此链码。注意,虽然多数节点一般都会安装papercontract
,但是如果通道上将运行 papercontract
,则只需为该通道发布一次 instantiate
。
看看如何使用 docker ps
命令开启 papercontract
容器:
(magnetocorp admin)$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4fac1b91bfda dev-peer0.org1.example.com-papercontract-0-d96... "/bin/sh -c 'cd /usr…" 2 minutes ago Up 2 minutes dev-peer0.org1.example.com-papercontract-0
注意该容器名为 dev-peer0.org1.example.com-papercontract-0-d96...
,表明了哪个节点启用了它,以及容器正在运行 papercontract
版本 0
。
既然我们已经组件并运行了一个基本的 PaperNet网络,还安装和实例化了 papercontract
,现在就让我们把目光转向发行了一张商业票据的 MagnetoCorp 应用程序吧。
应用结构¶
包含在 papercontract
中的智能合约由MagnetoCorp的应用程序 issue.js
调用。Isabella 使用该程序向发行商业票据00001
的账本提交一项交易。让我么来快速检验一下 issue
应用是怎么工作的。
commercialpaper.application 网关允许应用程序专注于交易的生成、提交和响应。它协调不同网络组件之间的交易提案、排序和通知处理。
issue
应用程序代表Isabella提交交易,它通过从Isabella的钱包中索取其X.509证书来开始运行,此证书可能储存在本地文件系统中或一个硬件安全模块HSM里。随后,issue
应用程序就能够利用网关在通道上提交交易。Hyperledger Fabric的软件开发包(SDK)提供了一个gateway抽象,因此应用程序在将网络交互托管给网关时能够专注于应用逻辑。网关和钱包使得编写Hyperledger Fabric应用程序变得很简单。
让我们来检验一下Isabella将要使用的 issue
应用程序,为其打开另一个终端窗口,在 fabric-samples
中找到MagnetoCorp的 /application
文件夹:
(magnetocorp user)$ cd commercial-paper/organization/magnetocorp/application/
(magnetocorp user)$ ls
addToWallet.js issue.js package.json
addToWallet.js
是Isabella将用来把自己的身份装进钱包的程序,而 issue.js
将使用这一身份通过调用 papercontract
来代表MagnetoCorp生成商业票据 00001
。
切换至包含MagnetoCorp的 issue.js
应用程序副本的目录,并且使用你的代码编辑器来对此目录进行检测:
(magnetocorp user)$ cd commercial-paper/organization/magnetocorp/application
(magnetocorp user)$ code issue.js
检查该目录;目录包含了issue应用程序和其所有依赖项。
commercialpaper.vscode2 一个展示了商业票据应用程序目录内容的代码编辑器。
注意以下在 issue.js
中的关键程序行:
const { FileSystemWallet, Gateway } = require('fabric-network');
该语句把两个关键的Hyperledger Fabric软件开发包(SDK)类代入了
Wallet
和Gateway
的范畴。因为Isabella的X.509 证书位于本地文件系统中,所以应用程序使用的是FileSystemWallet
。const wallet = new FileSystemWallet('../identity/user/isabella/wallet');
该语句表明了应用程序在连接到区块链网络通道上时将使用
Isabella
钱包。应用程序会在isabella
钱包中选择一个特定的身份。(该钱包必须已经装有Isabella的 X.509 证书,这就是addToWallet.js
的工作)await gateway.connect(connectionProfile, connectionOptions);
此行代码使用
connectionProfile
识别的网关来连接到网络,使用ConnectionOptions
当中引用的身份。
看看 ../gateway/networkConnection.yaml
和 User1@org1.example.com
是如何分别被用于这些值的。
const network = await gateway.getNetwork('mychannel');
该语句是将应用程序连接到网络通道
mychannel
上,papercontract
之前就已经在该通道上实例化过了。const contract = await network.getContract('papercontract', 'org.papernet.comm...');
该语句是让应用程序可以访问由papercontract
中的org.papernet.commercialpaper
命名空间定义的智能合约。一旦应用程序发送了getContract,那么它就能提交任意在其内实现的交易。
const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001'...);
该行代码是使用在智能合约中定义的
issue
交易来向网络提交一项交易。MagnetoCorp
,00001
… 都是被issue
交易用来生成一个新的商业票据的值。let paper = CommercialPaper.fromBuffer(issueResponse);
此语句是处理
issue
交易发来的响应。该响应需要从缓冲区被反序列化成paper
,这是一个能够被应用程序准确解释的CommercialPaper
对象。欢迎检查
/application
目录下的其他文档来了解issue.js
是如何工作的,并仔细阅读应用程序主题中关于如何实现issue.js
的内容。
应用程序依赖项¶
issue.js
应用程序是用 JavaScript 编写的,旨在作为PaperNet 网络的客户端来在 node.js 环境中运行。按照惯例,会在多个网络外部的节点包上建立MagnetoCorp 的应用程序,以此来提升开发的质量和速度。考虑一下 issue.js
是如何纳入 js-yaml
包装 来处理 YAML 网关连接配置文件的,或者issue.js
是如何纳入 fabric-network
包装 来访问 Gateway
和 Wallet
类的:
const yaml = require('js-yaml');
const { FileSystemWallet, Gateway } = require('fabric-network');
需要使用 npm install
命令来将这些包装从 npm 下载到本地文件系统中。按照惯例,必须将包装安装进一个相对于应用程序的/node_modules
目录中,以供运行时使用。
检查 package.json
文件来看看 issue.js
是如何通过识别包装来下载自己的准确版本的:
"dependencies": {
"fabric-network": "^1.4.0-beta",
"fabric-client": "^1.4.0-beta",
"js-yaml": "^3.12.0"
},
npm 版本控制功能非常强大;点击这里可以了解更多相关信息。
让我们使用 npm install
命令来安装这些包装,安装过程可能需要一分钟:
(magnetocorp user)$ npm install
( ) extract:lodash: sill extract ansi-styles@3.2.1
(...)
added 738 packages in 46.701s
看看这个命令是如何更新目录的:
(magnetocorp user)$ ls
addToWallet.js node_modules package.json
issue.js package-lock.json
检查 node_modules
目录,查看已经安装的包装。能看到很多已经安装了的包装,这是因为 js-yaml
和 fabric-network
本身都被搭建在其他 npm 包装中! package-lock.json
文件 能准确识别已安装的版本,如果你想精确地复制环境(以完成测试、诊断问题或者提交已验证的应用程序等任务)的话,那么这一点对你来说就极为重要。
钱包¶
Isabella 马上就能够运行 issue.js
来发行MagnetoCorp 商业票票据 00001
了;现在还剩最后一步!因为 issue.js
代表 Isabella,所以也就代表 MagnetoCorp, issue.js
将会使用 Isabella 钱包中反应以上事实的身份。现在我们需要执行这个一次性的活动,向 Isabella 的钱包中添 X.509 证书。
在 Isabella 的终端窗口中运行 addToWallet.js
程序来把身份信息添加到她的钱包中:
(isabella)$ node addToWallet.js
done
(isabella)$ node addToWallet.js
done
虽然在我们的示例中 Isabella 只使用了 User1@org.example.com
这一个身份,但其实她可以在钱包中储存多个身份。目前与这个身份相关联的是基本网络,而不是更实际化的 PaperNet 配置,我们很快会更新此版教程。
addToWallet.js
是一个简单的文件复制程序,你可以在闲暇时检查该程序。它把一个身份从基本网络样本上转移到 Isabella 的钱包中。让我们专注于该程序的结果,也就是将被用来向 PaperNet
提交交易的钱包的内容:
(isabella)$ ls ../identity/user/isabella/wallet/
User1@org1.example.com
看看目录结构如何映射 User1@org1.example.com
身份——Isabella 使用的其他身份都会有自己的文件夹。在该目录中你会找到 issue.js
将代表 isabella
使用的身份信息:
(isabella)$ ls ../identity/user/isabella/wallet/User1@org1.example.com
User1@org1.example.com c75bd6911a...-priv c75bd6911a...-pub
注意:
使用秘钥
c75bd6911a...-priv
代表 Isabella 来签名交易,但始终把这个秘钥置于Isabella的控制之内。公钥
c75bd6911a...-pub
与Isabella的私钥有密码联系,并且该公钥完全包含在 Isabella的X.509证书里。在证书生成阶段,证书授权中心添加了
User1@org.example.com
证书,其中包含了Isabella的公钥和其他 X.509 属性。该证书被分布至网络中,这样一来,不同操作者就能够在不同时间内对Isabella的私钥生成的信息进行密码验证。点击此处获取更多信息。在实践中,证书文档还包含一些Fabric专门的元数据,例如Isabella的组织和角色——在钱包主题阅读更多内容。
发行应用¶
Isabella 现在可以用 issue.js
来提交一项交易,该交易将发行MagnetoCorp 商业票据 00001
:
(isabella)$ node issue.js
Connect to Fabric gateway.
Use network channel: mychannel.
Use org.papernet.commercialpaper smart contract.
Submit commercial paper issue transaction.
Process issue transaction response.
MagnetoCorp commercial paper : 00001 successfully issued for value 5000000
Transaction complete.
Disconnect from Fabric gateway.
Issue program complete.
node
命令初始化一个node.js 环境,并运行 issue.js
。从程序输出我们能看到,系统发行了一张MagnetoCorp 商业票据 00001,面值为 500万美元。
如您所见,为实现这一点,应用程序调用了papercontract.js
中 CommercialPaper
智能合约里定义的 issue
交易。MagnetoCorp 管理员已经在网络上安装并实例化了CommercialPaper
智能合约。在世界状态里作为一个矢量状态来代表新的商业票据的是通过Fabric应用程序编码端口(API)来与账本交互的智能合约,其中最显著的API是 putState()
和 getState()
。我们即将看到该矢量状态在随后是如何被 buy
和 redeem
交易来操作的,这两项交易同样也是定义在那个智能合约中。
潜在的Fabric软件开发包(SDK)一直都在处理交易的背书、排序和通知流程,使得应用程序的逻辑变得简单明了; SDK 用网关提取出网络细节信息和连接选项 ,以此来声明更先进的流程策略,如交易重试。
现在让我们将重点转换到DigiBank(将购买商业票据),以遵循MagnetoCorp 00001
的生命周期。
像 DigiBank 一样工作¶
既然MagnetoCorp已经发行了商业票据 00001
,现在让我们转换身份,作为DigiBank的雇员与PaperNet交互。首先,我们将作为一个管理员,生成一个操作台,该操作台是被用来与PaperNet交互的。随后我们将作为终端用户Balaji,利用 Digibank的 buy
交易来购买商业票据 00001
,从而将进程转换到其生命周期的下一阶段。
commercialpaper.workdigi DigiBank管理员和应用程序与 PaperNet 网络交互。
因为教程目前使用的是PaperNet基本网络,所以网络配置非常简单。管理员使用的操作台与MagnetoCorp的相似,但操作台是为Digibank的文件系统配置的。同样地,Digibank终端用户将使用和MagnetoCorp应用程序调取相同智能合约的应用程序,尽管它们包含了Digibank专门的逻辑和配置。无论受哪项交易调用,总是智能合约捕获了共享的商业流程,也总是账本维持了共享的商业数据。
让我们打开另外一个终端窗口来让DigiBank管理员与PaperNet交互。 在 fabric-samples
中:
(digibank admin)$ cd commercial-paper/organization/digibank/configuration/cli/
(digibank admin)$ docker-compose -f docker-compose.yml up -d cliDigiBank
(...)
Creating cliDigiBank ... done
该docker容器现在可供Digibank 管理员来与网络进行交互:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORT NAMES
858c2d2961d4 hyperledger/fabric-tools "/bin/bash" 18 seconds ago Up 18 seconds cliDigiBank
在此教程中,你将使用名为 cliDigiBank
的命令行容器来代表Digibank与网络进行交互。我们尚未展示所有的docker容器,在现实中,DigiBank 用户只能看到它们有访问权限的网络组件(peer节点,排序节点和证书授权中心)。
因为PaperNet网络配置十分简单,所以目前在此教程中Digibank管理员的任务并不多。让我们把注意力转向Balaji。
Digibank 应用¶
Balaji 使用 DigiBank 的 buy
应用程序来向账本提交一项交易,该账本将商业票据 00001
的所属权从MagnetoCorp 转向DigiBank。 CommercialPaper
智能合约与MagnetoCorp应用程序使用的相同,但是此次的交易不同,是 buy
交易而不是 issue
交易。让我们检查一下DigiBank 的应用程序是怎样工作的。
为 Balaji 打开另一个终端窗口。 在 fabric-samples
中,切换到包含 buy.js
应用程序的DigiBank 应用程序目录,并用编辑器打开该目录:
(balaji)$ cd commercial-paper/organization/digibank/application/
(balaji)$ code buy.js
如你所见,该目录同时包含了Balaji将使用的 buy
和 redeem
应用程序 。
commercialpaper.vscode3 DigiBank的商业票据目录包含
buy.js
和 redeem.js
应用程序。
DigiBank的 buy.js
应用程序在结构上与MagnetoCorp的
issue.js
十分相似,但存在两个重要的差异:
- 身份:用户是DigiBank的用户
Balaji
而不是MagnetoCorp的Isabella
const wallet = new FileSystemWallet('../identity/user/balaji/wallet');`
```
看看应用程序在连接到PaperNet网络上时是如何使用 `balaji` 钱包的。`buy.js` 在 `balaji` 钱包里选择一个特定的身份。
* **交易**:被调用的交易是 `buy` 而不是 `issue`
```JavaScript
`const buyResponse = await contract.submitTransaction('buy', 'MagnetoCorp', '00001'...);`
```
提交一项 `buy` 交易,其值为 `MagnetoCorp`, `00001`..., `CommercialPaper` 智能合约类使用这些值来将商业票据 `00001` 的所属权转换成DigiBank。
欢迎检查 `application` 目录下的其他文档来理解应用程序 的工作原理,并仔细阅读应用程序[主题](../developapps/application.html)中关于如何实现 `buy.js` 的内容。
## 像 DigiBank 一样运行
负责购买和赎回商业票据的DigiBank 应用程序的结构和 MagnetoCorp的发行交易十分相似。所以,我们来安装这些应用程序 的依赖项,并搭建Balaji的钱包,这样一来,Balaji就能使用这些应用程序购买和赎回商业票据。
和 MagnetoCorp 一样, Digibank 必须使用 `npm install` 命令来安装规定的应用包,同时,安装时间也很短。
在DigiBank管理员窗口安装应用程序依赖项:
(digibank admin)$ cd commercial-paper/organization/digibank/application/ (digibank admin)$ npm install
( ) extract:lodash: sill extract ansi-styles@3.2.1 (…) added 738 packages in 46.701s
在Balaji的终端窗口运行 `addToWallet.js` 程序,把身份信息添加到他的钱包中:
(balaji)$ node addToWallet.js
done
`addToWallet.js` 程序为 `balaji` 将其身份信息添加到他的钱包中, `buy.js` 和 `redeem.js` 将使用这些身份信息来向 `PaperNet` 提交交易。
虽然在我们的示例中,Balaji只使用了`Admin@org.example.com` 这一个身份,但是和Isabella一样, 他也可以在钱包中储存多个身份。`digibank/identity/user/balaji/wallet/Admin@org1.example.com` 中包含的Balaji的相应钱包结构与Isabella的十分相似,欢迎对其进行检查。
## 购买应用
Balaji现在可以使用 `buy.js` 来提交一项交易,该交易将会把MagnetoCorp 商业票据 `00001` 的所属权转换成DigiBank。
在Balaji的窗口运行 `buy` 应用程序:
(balaji)$ node buy.js
Connect to Fabric gateway. Use network channel: mychannel. Use org.papernet.commercialpaper smart contract. Submit commercial paper buy transaction. Process buy transaction response. MagnetoCorp commercial paper : 00001 successfully purchased by DigiBank Transaction complete. Disconnect from Fabric gateway. Buy program complete.
你可看到程序输出为:Balaji已经代表DigiBank成功购买了MagnetoCorp 商业票据 00001。 `buy.js` 调用了 `CommercialPaper` 智能合约中定义的 `buy` 交易,该智能合约使用Fabric 应用程序编程接口(API) `putState()` 和 `getState()` 在世界状态中更新了商业票据 `00001` 。如您所见,就智能合约的逻辑来说,购买和发行商业票据的应用程序逻辑彼此十分相似。
## 收回应用
商业票据 `00001` 生命周期的最后一步交易是DigiBank从MagnetoCorp那里收回商业票据。Balaji 使用 `redeem.js` 提交一项交易来执行智能合约中的收回逻辑。
在Balaji的窗口运行 `redeem` 交易:
(balaji)$ node redeem.js
Connect to Fabric gateway. Use network channel: mychannel. Use org.papernet.commercialpaper smart contract. Submit commercial paper redeem transaction. Process redeem transaction response. MagnetoCorp commercial paper : 00001 successfully redeemed with MagnetoCorp Transaction complete. Disconnect from Fabric gateway. Redeem program complete.
同样地,看看当 `redeem.js` 调用了 `CommercialPaper` 中定义的 `redeem` 交易时,商业票据 00001 是如何被成功收回的。 `redeem` 交易在世界状态中更新了商业票据 `00001` ,以此来反映商业票据的所属权又归回其发行方MagnetoCorp。
## 下一步
要想更深入地理解以上教程中所介绍的应用程序和智能合约的工作原理,可以参照[开发应用程序](../developapps/developing_applications.html)。该主题将为您详细介绍商业票据场景、`PaperNet` 商业网络,网络操作者以及它们所使用的应用程序和智能合约的工作原理。
欢迎使用该样本来开始创造你自己的应用程序和智能合约!
<!--- Licensed under Creative Commons Attribution 4.0 International License
https://creativecommons.org/licenses/by/4.0/ -->
构建你的第一个网络¶
注解
本文内容已经过验证,它可以在最新稳定版的 docker 镜像和 tar 包中的提供的预编译的工具下工作。如果你使用 master 分支下的镜像或者工具使用这些命令,你可能会遇到配置或者 panic 错误。
在构建你的第一个网络(BYFN)场景中,提供了一个包含两个组织的 Hyperledger Fabric 网络,每个组织包含两个 Peer 节点,一个 “Solo” 模式的排序服务。
安装准备¶
如果你之前没有操作过,在我们开始之前,你应该检查你将要开发区块链应用或者操作 Hyperledger Fabric 的平台上是否安装了全部的 准备阶段。
你还需要 安装示例、二进制文件和 Docker 镜像 。fabric-samples
仓库中包含了许多示例。我们将使用 first-network
作为例子。现在我们一起打开这个子目录。
cd fabric-samples/first-network
注解
这个文档里提供的命令 必须 在你克隆的 fabric-samples
项目的子目录 first-network
里运行。如果你选择从其他位置运行命令,提供的脚本将无法找到二进制文件。
想要现在运行吗?¶
我们提供了一个有完整注释的脚本——byfn.sh
,它可以通过镜像快速启动一个 Hyperledger Fabric 网络,这个网络由代表两个组织的四个 Peer 节点和一个排序节点组成。它还将启动一个容器用于运行一个将 Peer 节点加入通道,部署并且实例化链码以及驱动已经部署的链码执行交易的脚本。
以下是该脚本 byfn.sh
的帮助文档:
Usage:
byfn.sh <mode> [-c <channel name>] [-t <timeout>] [-d <delay>] [-f <docker-compose-file>] [-s <dbtype>] [-l <language>] [-o <consensus-type>] [-i <imagetag>] [-v]"
<mode> - one of 'up', 'down', 'restart', 'generate' or 'upgrade'"
- 'up' - bring up the network with docker-compose up"
- 'down' - clear the network with docker-compose down"
- 'restart' - restart the network"
- 'generate' - generate required certificates and genesis block"
- 'upgrade' - upgrade the network from version 1.3.x to 1.4.0"
-c <channel name> - channel name to use (defaults to \"mychannel\")"
-t <timeout> - CLI timeout duration in seconds (defaults to 10)"
-d <delay> - delay duration in seconds (defaults to 3)"
-f <docker-compose-file> - specify which docker-compose file use (defaults to docker-compose-cli.yaml)"
-s <dbtype> - the database backend to use: goleveldb (default) or couchdb"
-l <language> - the chaincode language: golang (default), node, or java"
-o <consensus-type> - the consensus-type of the ordering service: solo (default), kafka, or etcdraft"
-i <imagetag> - the tag to be used to launch the network (defaults to \"latest\")"
-v - verbose mode"
byfn.sh -h (print this message)"
Typically, one would first generate the required certificates and
genesis block, then bring up the network. e.g.:"
byfn.sh generate -c mychannel"
byfn.sh up -c mychannel -s couchdb"
byfn.sh up -c mychannel -s couchdb -i 1.4.0"
byfn.sh up -l node"
byfn.sh down -c mychannel"
byfn.sh upgrade -c mychannel"
Taking all defaults:"
byfn.sh generate"
byfn.sh up"
byfn.sh down"
如果你不设置启动参数,脚本会使用默认值。
生成网络构件¶
准备好了没?OK,执行下面的命令:
./byfn.sh generate
你会看到一个简要说明,同时会有一个命令行提示 yes/no。输入 Y 或者回车键来继续执行。
Generating certs and genesis block for channel 'mychannel' with CLI timeout of '10' seconds and CLI delay of '3' seconds
Continue? [Y/n] y
proceeding ...
/Users/xxx/dev/fabric-samples/bin/cryptogen
##########################################################
##### Generate certificates using cryptogen tool #########
##########################################################
org1.example.com
2017-06-12 21:01:37.334 EDT [bccsp] GetDefault -> WARN 001 Before using BCCSP, please call InitFactories(). Falling back to bootBCCSP.
...
/Users/xxx/dev/fabric-samples/bin/configtxgen
##########################################################
######### Generating Orderer Genesis block ##############
##########################################################
2017-06-12 21:01:37.558 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.562 EDT [msp] getMspConfig -> INFO 002 intermediate certs folder not found at [/Users/xxx/dev/byfn/crypto-config/ordererOrganizations/example.com/msp/intermediatecerts]. Skipping.: [stat /Users/xxx/dev/byfn/crypto-config/ordererOrganizations/example.com/msp/intermediatecerts: no such file or directory]
...
2017-06-12 21:01:37.588 EDT [common/configtx/tool] doOutputBlock -> INFO 00b Generating genesis block
2017-06-12 21:01:37.590 EDT [common/configtx/tool] doOutputBlock -> INFO 00c Writing genesis block
#################################################################
### Generating channel configuration transaction 'channel.tx' ###
#################################################################
2017-06-12 21:01:37.634 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.644 EDT [common/configtx/tool] doOutputChannelCreateTx -> INFO 002 Generating new channel configtx
2017-06-12 21:01:37.645 EDT [common/configtx/tool] doOutputChannelCreateTx -> INFO 003 Writing new channel tx
#################################################################
####### Generating anchor peer update for Org1MSP ##########
#################################################################
2017-06-12 21:01:37.674 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.678 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 002 Generating anchor peer update
2017-06-12 21:01:37.679 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 003 Writing anchor peer update
#################################################################
####### Generating anchor peer update for Org2MSP ##########
#################################################################
2017-06-12 21:01:37.700 EDT [common/configtx/tool] main -> INFO 001 Loading configuration
2017-06-12 21:01:37.704 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 002 Generating anchor peer update
2017-06-12 21:01:37.704 EDT [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO 003 Writing anchor peer update
第一步为我们的各种网络实体生成证书和秘钥。创世区块 genesis block
用于引导排序服务,也包含了一组配置 Channel 所需要的配置交易集合。
启动网络¶
接下来,你可以用下面的命令启动网络:
./byfn.sh up
上面的命令会编译 Golang 智能合约的镜像并且启动相应的容器。Go 语言是默认的链码语言,但是它也支持 Node.js 和 Java 的链码。如果你想要在这个教程里运行 node 链码,你可以使用下面的命令:
# we use the -l flag to specify the chaincode language
# forgoing the -l flag will default to Golang
./byfn.sh up -l node
注解
更多关于 Node.js shim 的信息,请查看这个 文档 。
注解
更多关于 Java shim 的信息,请查看这个 文档 。
要让示例运行 Java 链码,你需要指定 -l java
:
./byfn.sh up -l java
注解
不要同时运行这两个命令。除非你停止并重新创建了网络,否则只能尝试一种语言。
如果你想支持多种链码语言,请指定参数启动五个 Raft 节点或者 Kafka 的排序服务来取代一个节点的 Solo 排序。更多已经支持的排序服务的实现,请查阅 排序服务 。
要启用 Raft 排序服务的网络,请执行:
./byfn.sh up -o etcdraft
要启用 Kafka 排序服务的网络,请执行:
./byfn.sh up -o kafka
您将再一次被提示要继续或中止。输入 y
或者按下回车键来继续执行:
Starting for channel 'mychannel' with CLI timeout of '10' seconds and CLI delay of '3' seconds
Continue? [Y/n]
proceeding ...
Creating network "net_byfn" with the default driver
Creating peer0.org1.example.com
Creating peer1.org1.example.com
Creating peer0.org2.example.com
Creating orderer.example.com
Creating peer1.org2.example.com
Creating cli
____ _____ _ ____ _____
/ ___| |_ _| / \ | _ \ |_ _|
\___ \ | | / _ \ | |_) | | |
___) | | | / ___ \ | _ < | |
|____/ |_| /_/ \_\ |_| \_\ |_|
Channel name : mychannel
Creating channel...
日志会从那里继续。这一步会启动所有的容器,然后启动一个完整的 end-to-end 应用场景。完成后,它应该在您的终端窗口中显示以下内容:
Query Result: 90
2017-05-16 17:08:15.158 UTC [main] main -> INFO 008 Exiting.....
===================== Query successful on peer1.org2 on channel 'mychannel' =====================
===================== All GOOD, BYFN execution completed =====================
_____ _ _ ____
| ____| | \ | | | _ \
| _| | \| | | | | |
| |___ | |\ | | |_| |
|_____| |_| \_| |____/
你可以滚动这些日志去查看各种交易。如果你没有获得这个结果,请移步疑难解答部分 故障排除 ,看看我们是否可以帮助你发现问题。
关闭网络¶
最后,让我们把他停下来,这样我们可以一步步探索网络设置。接下来的命令会结束掉你所有的容器,移除加密的材料和四个构件,并且从 Docker 仓库删除链码镜像。
./byfn.sh down
您将再一次被提示要继续或中止,输入 y
或者按下回车键来继续执行:
Stopping with channel 'mychannel' and CLI timeout of '10'
Continue? [Y/n] y
proceeding ...
WARNING: The CHANNEL_NAME variable is not set. Defaulting to a blank string.
WARNING: The TIMEOUT variable is not set. Defaulting to a blank string.
Removing network net_byfn
468aaa6201ed
...
Untagged: dev-peer1.org2.example.com-mycc-1.0:latest
Deleted: sha256:ed3230614e64e1c83e510c0c282e982d2b06d148b1c498bbdcc429e2b2531e91
...
如果你想要了解更多关于底层工具和引导机制的信息,请继续阅读。在接下来的章节,我们将浏览构建一个功能完整的 Hyperledger Fabric 网络的各个步骤和要求。
注解
下面列出的手动步骤假设 cli
容器中的 FABRIC_LOGGING_SPEC
设置为 DEBUG
。你可以通过修改 first-network
中的 docker-compose-cli.yaml
文件来设置。例如:
cli:
container_name: cli
image: hyperledger/fabric-tools:$IMAGE_TAG
tty: true
stdin_open: true
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- FABRIC_LOGGING_SPEC=DEBUG
#- FABRIC_LOGGING_SPEC=INFO
密钥生成器¶
我们将使用 cryptogen
工具为我们的网络实体生成各种加密材料( x509 证书和签名秘钥)。这些证书是身份的代表,在实体之间通信和交易的时候,它们允许对身份验证进行签名和验证。
它是怎么工作的?¶
Cryptogen 通过一个包含网络拓扑的文件 crypto-config.yaml
,为所有组织和属于这些组织的组件生成一组证书和秘钥。每一个组织被分配一个唯一的根证书(ca-cert
),它绑定该组织的特定组件(Peer 节点和排序节点)。通过为每个组织分配一个唯一的 CA 证书,我们模拟了一个典型的网络,网络中的成员可以使用它自己的证书授权中心。Fabric 中的事务和通信由一个实体的私钥(keystore
)签名,然后通过公钥(signcerts
)验证。
在这个文件里你会发现一个 count
变量。我们通过它来指定每个组织的 Peer 节点数量。在我们的案例里每个组织有两个 Peer 节点。我们现在不会深入研究 x.509 证书和公钥结构 的细节。如果有兴趣,你可以仔细阅读一下这些主题。
在我们运行 cryptogen
工具之后,生成的证书和密钥将保存到一个名为 crypto-config
的文件夹中。注意, crypto-config.yaml
文件在排序组织中设置了五个排序节点。cryptogen
会为这五个排序节点生成证书,除非使用 Raft 或者 Kafka 排序服务,Solo 排序服务只会使用一个排序节点来创建系统通道和 mychannel
。
配置交易生成器¶
configtxgen
工具用来创建四个配置构件:
- 排序节点的
创世区块
,- 通道
配置交易
,- 两个
锚节点交易
,一个对应一个 Peer 组织。
有关此工具的完整说明,请参阅 configtxgen
排序区块是排序服务的创世区块,通道配置交易在通道创建的时候广播给排序服务。锚节点交易,指定了每个组织在此通道上的锚节点。
它是怎么工作的?¶
Configtxgen 使用一个文件——configtx.yaml
,这个文件包含了一个示例网络的定义。它拥有三个成员:一个排序组织(OrdererOrg
)和两个 Peer 组织(Org1
& Org2
),这两个 Peer 组织每个都管理和维护两个 Peer 节点。这个文件还定义了一个联盟——SampleConsortium
,包含了我们的两个 Peer 组织。注意一下文件中 “Profiles” 部分的最下边。你会看到我们有一些特别的标题。其中有一些值得注意:
TwoOrgsOrdererGenesis
: 为 Solo 排序服务生成创世区块。SampleMultiNodeEtcdRaft
: 为 Raft 排序服务生成创世区块。只有将-o
参数指定为etcdraft
时才可用。SampleDevModeKafka
: 为 Kafka 排序服务生成创世区块。只有将-o
参数指定为kafka
时才可用。TwoOrgsChannel
: 为我们的通道mychannel
生成创世区块。
这些标题很重要,因为在我们创建网络各项构件的时侯,需要将它们将作为参数传入。
注解
注意我们的 SampleConsortium
在系统级配置项中定义,并且在通道级的配置项中引用。通道存在于联盟的范围内,所有的联盟必须定义在整个网络范围内。
该文件还包含两个值得注意的附加规范。第一,我们为每个组织指定了锚节点(peer0.org1.example.com
& peer0.org2.example.com
)。第二,我们为每个成员指定 MSP 文件位置,进而我们可以在排序节点的创世区块中存储每个组织的根证书。这是一个关键概念。现在每个和排序服务通信的网络实体都可以验证它们的数字签名。
运行工具¶
你可以用 configtxgen
和 cryptogen
命令来手动生成证书/密钥和各种配置。或者,你可以尝试使用 byfn.sh 脚本来完成你的目标。
手动生成构件¶
你可以参考 byfn.sn 脚本中的 generateCerts
函数,这个函数包含了生成 crypto-config.yaml
中所定义的证书的必要命令,这些证书将被作为你的网络配置。然而,为了方便起见,我们在这里也提供一个参考。
首先,让我们来运行 cryptogen
工具。这个二进制文件存放在 bin
文件目录下,所以我们需要提供工具所在的相对路径。
../bin/cryptogen generate --config=./crypto-config.yaml
你会在你的终端中看到下面的内容:
org1.example.com
org2.example.com
证书和秘钥(例如 MSP 材料)将会保存在 first-network
目录的 crypto-config
文件夹中。
接下来,我们需要告诉 configtxgen
工具去哪儿寻找它需要的 configtx.yaml
文件。我们会告诉它在当前的工作目录:
export FABRIC_CFG_PATH=$PWD
然后我们会调用 configtxgen
工具去创建排序通道创世区块:
../bin/configtxgen -profile TwoOrgsOrdererGenesis -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block
要生成 Raft 排序服务的创世区块,需要如下命令:
../bin/configtxgen -profile SampleMultiNodeEtcdRaft -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block
注意,这里使用了 SampleMultiNodeEtcdRaft
选项。
要生成 Kafka 排序服务的创世区块,执行如下命令:
../bin/configtxgen -profile SampleDevModeKafka -channelID byfn-sys-channel -outputBlock ./channel-artifacts/genesis.block
如果你没有使用 Raft 或者 Kafka,你会看到类似下边的输出:
2017-10-26 19:21:56.301 EDT [common/tools/configtxgen] main -> INFO 001 Loading configuration
2017-10-26 19:21:56.309 EDT [common/tools/configtxgen] doOutputBlock -> INFO 002 Generating genesis block
2017-10-26 19:21:56.309 EDT [common/tools/configtxgen] doOutputBlock -> INFO 003 Writing genesis block
注解
排序通道创世区块和其他生成的构件都保存在当前项目根目录中的 channel-artifacts
文件夹。上边命令中的 channelID 是系统通道的名字。
创建通道配置交易¶
接下来,我们需要去创建通道的交易构件。请确保替换 $CHANNEL_NAME
或者将 CHANNEL_NAME
设置为整个说明中可以使用的环境变量:
# The channel.tx artifact contains the definitions for our sample channel
export CHANNEL_NAME=mychannel && ../bin/configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/channel.tx -channelID $CHANNEL_NAME
注意,如果你使用 Raft 或者 Kafka 排序服务,你也不需要为通道指定特殊命令。TwoOrgsChannel
选项会使用你指定的排序服务配置为网络创建创世区块。
如果你没有使用 Raft 或者 Kafka,你会看到类似下边的输出:
2017-10-26 19:24:05.324 EDT [common/tools/configtxgen] main -> INFO 001 Loading configuration
2017-10-26 19:24:05.329 EDT [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 002 Generating new channel configtx
2017-10-26 19:24:05.329 EDT [common/tools/configtxgen] doOutputChannelCreateTx -> INFO 003 Writing new channel tx
接下来,我们会为构建的通道上的 Org1 定义锚节点。请再次确认 $CHANNEL_NAME
已被替换或者设置了环境变量。终端输出类似通道交易构件:
../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
现在,我们将在同一个通道上为 Org2 定义锚节点:
../bin/configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP
启动网络¶
注解
如果之前启动了 byfn.sh
示例,再继续之前确认你已经把这个测试网络关掉了(查看 `Bring Down the Network`_ )。
我们将使用一个脚本启动我们的网络。docker-compose 文件关联了我们之前下载的镜像,然后通过我们之前生成的创世区块 genesis.block
引导排序节点。
我们要手动运行那些命令,目的是为了探索每个语法和调用的功能。
首先,启动我们的网络:
docker-compose -f docker-compose-cli.yaml up -d
如果你想要实时查看你的网络日志,请不要加 -d
标识。如果你想要查看日志流,你需要打开第二个终端来执行 CLI 调用。
创建和加入通道¶
回想一下,我们在 创建通道配置交易 章节中使用 configtxgen
工具创建通道配置交易。你可以使用相同的方式创建额外的通道配置交易,使用 configtx.yaml
中相同或者不同的选项传给 configtxgen
工具。然后你可以重复在本章节中的过程在你的网络中创建其他通道。
我们可以使用 docker exec
输入 CLI 容器命令:
docker exec -it cli bash
成功的话你会看到下面的输出:
root@0d78bb69300d:/opt/gopath/src/github.com/hyperledger/fabric/peer#
要想运行后边的 CLI 命令,我们需要使用下边的命令来设置四个环境变量。这些 peer0.org1.example.com
的环境变量已经在 CLI 容器中设置过了,所以不用再设置了。但是,如果你想向其他 Peer 节点或者排序节点发送调用,那么你发送任何 CLI 调用的时候都需要像下边的命令一样覆盖这些环境变量:
# Environment variables for PEER0
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
接下来,我们会把在 创建通道配置交易 章节中创建的通道配置交易配置(channel.tx
)作为创建通道请求的一部分传递给排序节点。
我们使用 -c
标志指定通道的名称,-f
标志指定通道配置交易,在这个例子中它是 channel.tx
,当然你也可以使用不同的名称挂载你自己的交易配置。我们将再次在 CLI 容器中设置 CHANNEL_NAME
环境变量,这样我们就不用显式的传递这个参数。通道的名称必须全部是消息字母,小于 250 个字符,并且匹配正则表达式 [a-z][a-z0-9.-]*
。
export CHANNEL_NAME=mychannel
# the channel.tx file is mounted in the channel-artifacts directory within your CLI container
# as a result, we pass the full path for the file
# we also pass the path for the orderer ca-cert in order to verify the TLS handshake
# be sure to export or replace the $CHANNEL_NAME variable appropriately
peer channel create -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/channel.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
注解
注意 --cafile
会作为命令的一部分。这是排序节点的根证书的本地路径,允许我们去验证 TLS 握手。
这个命令返回一个创世区块,<channel-ID.block>
。我们将会用它来加入通道。它包含了 channel.tx
中的配置信息。如果你没有修改默认的通道名称,命令会返回给你一个叫 mychannel.block
的 proto。
注解
你将在 CLI 容器中继续执行这些手动命令的其余部分。当你的目标是 peer0.org1.example.com
节点之外的 peer 时,你必须记住用相应的环境变量作为所有命令的前言。
现在让我们把 peer0.org1.example.com
加入通道。
# By default, this joins ``peer0.org1.example.com`` only
# the <CHANNEL_NAME.block> was returned by the previous command
# if you have not modified the channel name, you will join with mychannel.block
# if you have created a different channel name, then pass in the appropriately named block
peer channel join -b mychannel.block
你可以通过适当的修改在 创建和加入通道 章节中的四个环境变量来让其他的节点加入通道。
不是加入每一个节点,我们只是简单的加入 peer0.org2.example.com
以便我们可以更新定义在通道中的锚节点。由于我们正在覆盖 CLI 容器中默认的环境变量,整个命令将会是这样:
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:9051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt peer channel join -b mychannel.block
注解
在 v1.4.1 版本之前,docker 网络中的所有 Peer 节点都使用 7051
端口。如果使用 v1.4.1 版本之前的 fabric-samples,需要将所有的 CORE_PEER_ADDRESS
修改为 7051
端口。
或者,您可以选择单独设置这些环境变量而不是传递整个字符串。设置完成后,只需再次执行 peer channel join
命令,然后 CLI 容器会代表 peer0.org2.example.com
起作用。
更新锚节点¶
接下来的命令是通道更新,它会传递到通道的定义中去。实际上,我们在通道创世区块的头部添加了额外的配置信息。注意我们没有编辑创世区块,但是简单的把将会定义锚节点的增量添加到了链中。
更新通道定义,将 Org1 的锚节点定义为 peer0.org1.example.com
。
peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org1MSPanchors.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
现在更新通道定义,将 Org2 的锚节点定义为 peer0.org2.example.com
。与执行 Org2 节点的 peer channel join
命令相同,我们需要为这个命令配置合适的环境变量。
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer0.org2.example.com:9051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt peer channel update -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-artifacts/Org2MSPanchors.tx --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
安装和实例化链码¶
注解
我们将利用现有的一个简单链码。要学习怎么编写你自己的链码,请参考 链码开发者教程 教程。
应用程序和区块链账本通过链码 chaincode
进行交互。因此,我们要在每个会执行以及背书我们交易的节点安装链码,然后在通道上实例化链码。
首先,在 Org1 的 peer0 节点上安装 Go、Node.js 或者 Java 链码。这些命令把指定的源码放在节点的文件系统里。
注解
每个链码的名称和版本号你只能安装一个版本的源码。源码存在于 Peer 节点文件系统上的链码名称和版本号的上下文里,它与语言无关。同样,被实例化的链码容器将反映出是什么语言被安装在 Peer 节点上。
Golang
# this installs the Go chaincode. For go chaincode -p takes the relative path from $GOPATH/src
peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/
Node.js
# this installs the Node.js chaincode
# make note of the -l flag to indicate "node" chaincode
# for node chaincode -p takes the absolute path to the node.js chaincode
peer chaincode install -n mycc -v 1.0 -l node -p /opt/gopath/src/github.com/chaincode/chaincode_example02/node/
Java
# make note of the -l flag to indicate "java" chaincode
# for java chaincode -p takes the absolute path to the java chaincode
peer chaincode install -n mycc -v 1.0 -l java -p /opt/gopath/src/github.com/chaincode/chaincode_example02/java/
当我们在通道上实例化链码之后,背书策略被设定为需要 Org1 和 Org2 的节点都背书。所以,我们需要在 Org2 的节点上也安装链码。
为了执行在 Org2 的 peer0 上安装命令,需要修改以下四个环境变量:
# Environment variables for PEER0 in Org2
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer0.org2.example.com:9051
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
现在在 Org2 peer0 上安装 Go、Node.js 或者 Java 的示例链码。这些命令将源代码安装到节点的文件系统上。
Golang
# this installs the Go chaincode. For go chaincode -p takes the relative path from $GOPATH/src
peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/
Node.js
# this installs the Node.js chaincode
# make note of the -l flag to indicate "node" chaincode
# for node chaincode -p takes the absolute path to the node.js chaincode
peer chaincode install -n mycc -v 1.0 -l node -p /opt/gopath/src/github.com/chaincode/chaincode_example02/node/
Java
# make note of the -l flag to indicate "java" chaincode
# for java chaincode -p takes the absolute path to the java chaincode
peer chaincode install -n mycc -v 1.0 -l java -p /opt/gopath/src/github.com/chaincode/chaincode_example02/java/
接下来,在通道上实例化链码。这会在通道上初始化链码,为链码指定背书策略,然后为目标节点启动链码容器。注意 -P
这个参数。这是我们的策略,我们在此策略中指定针对要验证的此链码的交易所需的背书级别。
在下面的命令里你将会注意到我们指定 -P "AND ('Org1MSP.peer','Org2MSP.peer')"
作为策略。这表明我们需要属于 Org1 和 Org2 的节点“背书” (就是说要两个背书)。如果我们把语法改成 OR
,那我们将只需要一个背书。
Golang
# be sure to replace the $CHANNEL_NAME environment variable if you have not exported it
# if you did not install your chaincode with a name of mycc, then modify that argument as well
peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')"
Node.js
注解
Node.js 链码实例化大约需要一分钟。命令任务没有挂掉,而是在编译和安装 fabric-shim 层镜像。
# be sure to replace the $CHANNEL_NAME environment variable if you have not exported it
# if you did not install your chaincode with a name of mycc, then modify that argument as well
# notice that we must pass the -l flag after the chaincode name to identify the language
peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -l node -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')"
Java
注解
请注意,Java 链码初始化可能也会花费一些时间,它需要编译链码和下载 Java 环境 docker 镜像。
peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc -l java -v 1.0 -c '{"Args":["init","a", "100", "b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')"
更多策略实现的内容,请查看 背书策略 。
如果你想让其他的节点与账本交互,你需要将他们加入通道,然后在节点的文件系统上安装名字、版本和语言一样的链码。一旦它们尝试与特定的链代码进行交互,就会为每一个节点启动一个链码容器。再一次,要认识到 Node.js 镜像的编译速度会慢一些。
一旦链码在通道上实例化,我们可以放弃 l
标志。我们只需传递通道标识符和链码的名称。
查询¶
让我们查询 a
的值,以确保链码被正确实例化并且向状态数据库写入了数据。查询的语法是这样的:
# be sure to set the -C and -n flags appropriately
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
调用¶
现在我们从 a
账户向 b
账户转账 10 。这个交易将会产生一个新的区块并更新状态数据库。
调用的语法是这样的:
# be sure to set the -C and -n flags appropriately
peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["invoke","a","b","10"]}'
查询¶
我们来确认一下我们之前的调用正确执行了。我们为键 a
初始化一个 100 的值,
通过刚才的调用减少了 10
。这样查询出的值应该是 90
,查询的语法是这样的:
# be sure to set the -C and -n flags appropriately
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
我们会看到下面的结果:
Query Result: 90
现在,你可以随意重新开始并操纵键值对和后续调用。
安装¶
现在我们将在第三个节点上安装链码, Org2 的 peer1 。为了执行在 Org2 的 peer1 上的安装命令,需要改变以下四个环境变量:
# Environment variables for PEER1 in Org2
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer1.org2.example.com:10051
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt
现在在 Org2 的 peer1 上安装 Go、Node.js 或者 Java 的示例链码。这些命令会安装指定的源码到节点的文件系统上。
Golang
# this installs the Go chaincode. For go chaincode -p takes the relative path from $GOPATH/src
peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/chaincode_example02/go/
Node.js
# this installs the Node.js chaincode
# make note of the -l flag to indicate "node" chaincode
# for node chaincode -p takes the absolute path to the node.js chaincode
peer chaincode install -n mycc -v 1.0 -l node -p /opt/gopath/src/github.com/chaincode/chaincode_example02/node/
Java
# make note of the -l flag to indicate "java" chaincode
# for java chaincode -p takes the absolute path to the java chaincode
peer chaincode install -n mycc -v 1.0 -l java -p /opt/gopath/src/github.com/chaincode/chaincode_example02/java/
查询¶
让我们确认一下我们可以执行对 Org2 的 Peer1 的查询。我们把键 a
的值初始化为 100
而且上一个操作转移了 10
。所以对 a
的查询结果仍应该是 90
。
Org2 的 peer1 必须先加入通道才可以响应查询。下边的命令可以让它加入通道:
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp CORE_PEER_ADDRESS=peer1.org2.example.com:10051 CORE_PEER_LOCALMSPID="Org2MSP" CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt peer channel join -b mychannel.block
在加入通道的命令返回之后,查询就可以执行了。下边是执行查询的语法。
# be sure to set the -C and -n flags appropriately
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
我们会看到下面的结果:
Query Result: 90
现在,你可以随意重新开始并操纵键值对和后续调用。
幕后发生了什么?¶
注解
这些步骤描述了在 script.sh
脚本中的场景,它是由 ./byfn.sh up
启动的。通过 ./byfn.sh down
清除你的网络,确保此命令处于活动状态。然后用同样的 docker-compose 去再次启动你的网络。
- 脚本
script.sh
被保存在 CLI 容器中。这个脚本通过提供的通道名称和通道配置文件 channel.tx 来执行创建通道createChannel
的命令。 createChannel
的输出是一个创世区块 ——<你的通道名>.block
,它被存储在节点文件系统上并包含有来自 channel.tx 的通道配置。joinChannel
命令被所有的四个节点执行,作为之前产生创世区块的输入。这个命令指示那些节点去加入通道<你的通道名>
并且通过<你的通道名>.block
开始创建一条链。- 现在我们有一个由四个节点,两个组织组成的通道,这是我们
TwoOrgsChannel
的结构。 peer0.org1.example.com
和peer1.org1.example.com
属于 Org1;peer0.org2.example.com
和peer1.org2.example.com
属于 Org2- 这些关系在
crypto-config.yaml
中定义,MSP 的路径在我们的 docker compose 中指定。 - Org1MSP(
peer0.org1.example.com
) 和 Org2MSP(peer0.org2.example.com
) 的锚节点将会被更新。我们通过把Org1MSPanchors.tx
和Org2MSPanchors.tx
加上通道名称一起传给排序节点来做到这一点。 - 链码 chaincode_example02 被安装在
peer0.org1.example.com
和peer0.org2.example.com
- 链码在
mychannel
上“实例化”。实例化是把链码添加到通道上,为目标节点启动容器。初始化链码相关的键值对。对于本例来说初始化的值是 [“a”,”100” “b”,”200”]。这个“初始化”的结果是启动名为dev-peer0.org2.example.com-mycc-1.0
的容器。 - 这个实例化过程也给背书策略传递了一个参数。这个策略被定义为
-P "AND ('Org1MSP.peer','Org2MSP.peer')"
,意思是任何交易都要两个分别属于 Org1 和 Org2 的 Peer 节点背书。 - 向
peer0.org2.example.com
发出针对键 “a” 的值的查询。在链码实例化的时候,为 Org2 peer0 启动了一个名为dev-peer0.org2.example.com-mycc-1.0
的容器。查询结果返回了。没有对 “a” 执行写操作,所以返回的值仍为 “100” 。 - 向
peer0.org1.example.com
和peer0.org2.example.com
发送了一次调用,来从 “a” 向 “b” 转账 “10”。 - 向
peer0.org2.example.com
发送一次对 “a” 的值的查询。返回值为 90,正确反映了之前交易期间,键 “a” 的值被转走了 10。 - 链码 chaincode_example02 被安装在
peer1.org2.example.com
- 向
peer1.org2.example.com
发送一次对 “a” 的值的查询。启动了第三个名为dev-peer1.org2.example.com-mycc-1.0
的链码容器。返回值为 90,正确反映了之前交易期间,键 “a” 的值被转走了 10。
这表明了什么?¶
链码 必须 安装在节点上才能实现对账本的读写操作。此外,一个链码容器不会在节点里启动,除非让链码执行 init
或者交易,(例如查询“a”的值)。交易导致容器的启动。当然,所有通道中的节点都持有以块的形式顺序存储的不可变的账本精确的备份,以及用来保存当前状态的快照状态数据库。这包括了没有在其上安装链码的节点(例如上面例子中的 peer1.org1.example.com
)。最后,链码在被安装后将是可用状态(例如上面例子中的 peer1.org2.example.com
),因为它已经被实例化了。
我如何查看这些交易?¶
检查 CLI 容器的日志。
docker logs -f cli
你会看到下面的输出:
2017-05-16 17:08:01.366 UTC [msp] GetLocalMSP -> DEBU 004 Returning existing local MSP
2017-05-16 17:08:01.366 UTC [msp] GetDefaultSigningIdentity -> DEBU 005 Obtaining default signing identity
2017-05-16 17:08:01.366 UTC [msp/identity] Sign -> DEBU 006 Sign: plaintext: 0AB1070A6708031A0C08F1E3ECC80510...6D7963631A0A0A0571756572790A0161
2017-05-16 17:08:01.367 UTC [msp/identity] Sign -> DEBU 007 Sign: digest: E61DB37F4E8B0D32C9FE10E3936BA9B8CD278FAA1F3320B08712164248285C54
Query Result: 90
2017-05-16 17:08:15.158 UTC [main] main -> INFO 008 Exiting.....
===================== Query successful on peer1.org2 on channel 'mychannel' =====================
===================== All GOOD, BYFN execution completed =====================
_____ _ _ ____
| ____| | \ | | | _ \
| _| | \| | | | | |
| |___ | |\ | | |_| |
|_____| |_| \_| |____/
你可以滚动这些日志来查看各种交易。
我如何查看链码日志?¶
检查每个独立的链码服务容器来分别查看每个容器内的交易。下面是每个链码服务容器的日志的综合输出:
$ docker logs dev-peer0.org2.example.com-mycc-1.0
04:30:45.947 [BCCSP_FACTORY] DEBU : Initialize BCCSP [SW]
ex02 Init
Aval = 100, Bval = 200
$ docker logs dev-peer0.org1.example.com-mycc-1.0
04:31:10.569 [BCCSP_FACTORY] DEBU : Initialize BCCSP [SW]
ex02 Invoke
Query Response:{"Name":"a","Amount":"100"}
ex02 Invoke
Aval = 90, Bval = 210
$ docker logs dev-peer1.org2.example.com-mycc-1.0
04:31:30.420 [BCCSP_FACTORY] DEBU : Initialize BCCSP [SW]
ex02 Invoke
Query Response:{"Name":"a","Amount":"90"}
了解 Docker Compose 技术¶
BYFN 示例给我们提供了两种风格的 Docker Compose 文件,它们都继承自 docker-compose-base.yaml
(在 base
目录下)。我们的第一种类型, docker-compose-cli.yaml
,给我们提供了一个 CLI 容器,以及一个 orderer 容器,四个 Peer 容器。我们用此文件来展开这个页面上的所有说明。
注解
本节的剩余部分涵盖了为 SDK 设计的 docker-compose 文件。有关运行这些测试的详细信息, 请参阅 Node SDK 仓库。
第二种风格是 docker-compose-e2e.yaml
,被构造为使用 Node.js SDK 来运行端到端测试。除了 SDK 的功能之外,它主要的区别在于它有运行 fabric-ca 服务的容器。因此,我们能够向组织的 CA 节点发送用于注册和登记用户的 REST 请求。
如果你在没有运行 byfn.sh
脚本的情况下,想使用 docker-compose-e2e.yaml
,我们需要进行四个轻微的修改。我们需要指出本组织 CA 的私钥。你可以在 crypto-config
文件夹中找到这些值。举个例子,为了定位 Org1 的私钥,我们将使用 crypto-config/peerOrganizations/org1.example.com/ca/
。Org2 的路径为 crypto-config/peerOrganizations/org2.example.com/ca/
。
在 docker-compose-e2e.yaml
里为 ca0 和 ca1 更新 FABRIC_CA_SERVER_TLS_KEYFILE 变量。你同样需要编辑 command 中启动 ca server 的路径。你为每个 CA 容器提供了两次同样的私钥。
使用CouchDB¶
状态数据库可以从默认的 goleveldb
切换到 CouchDB
。链码就可以使用 CouchDB
的功能了, CouchDB
提供了额外的能力来根据 JSON 形式的链码服务数据提供更加丰富以及复杂的查询。
使用 CouchDB 代替默认的数据库(goleveldb),除了在启动网络的时侯传递 docker-compose-couch.yaml
之外,请遵循前面提到的生成配置文件的过程:
docker-compose -f docker-compose-cli.yaml -f docker-compose-couch.yaml up -d
chaincode_example02 现在在使用 CouchDB。
注解
如果你选择将 fabric-couchdb 容器端口映射到主机端口,请确保你意识到了安全性的影响。在开发环境中映射端口可以使 CouchDB REST API 可用,并允许通过 CouchDB Web 界面(Fauxton)对数据库进行可视化。生产环境将避免端口映射,以限制对 CouchDB 容器的外部访问。
你可以按照上面列出的步骤使用 CouchDB 来执行 chaincode_example02 ,然而为了联系 CouchDB 的查询能力,你将需要使用被格式化为 JSON 的数据(例如 marbles02)。你可以在 fabric/examples/chaincode/go
目录中找到 marbles02
链码。
我们将同样按照 :ref:createandjoin
部分的过程创建和加入通道。一旦你将 Peer 节点加入到了通道,请使用以下步骤与 marbles02 链码交互:
- 在
peer0.org1.example.com
上安装和实例化链:
# be sure to modify the $CHANNEL_NAME variable accordingly for the instantiate command
peer chaincode install -n marbles -v 1.0 -p github.com/chaincode/marbles02/go
peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -v 1.0 -c '{"Args":["init"]}' -P "OR ('Org1MSP.peer','Org2MSP.peer')"
- 创建一些 marble 并转移它们:
# be sure to modify the $CHANNEL_NAME variable accordingly
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble2","red","50","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble3","blue","70","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["transferMarble","marble2","jerry"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["transferMarblesBasedOnColor","blue","jerry"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["delete","marble1"]}'
如果你选择在 docker-compose 文件中映射你的 CouchDB 的端口,那么你现在就可以用浏览器打开下面的 URL 来使用 CouchDB Web 界面(Fauxton):
http://localhost:5984/_utils
你应该可以看到一个名为 mychannel
(或者你唯一的通道名字)的数据库以及它的文档在里面:
注解
对于下面的命令,请确定 $CHANNEL_NAME 变量被更新了。
你可以 CLI 中运行常规的查询(例如读取 marble2
):
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["readMarble","marble2"]}'
marble2
的详细输出应该显示为:
Query Result: {"color":"red","docType":"marble","name":"marble2","owner":"jerry","size":50}
你可以检索特定 marble 的历史记录,例如 marble1
:
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["getHistoryForMarble","marble1"]}'
关于 marble1
的交易的输出:
Query Result: [{"TxId":"1c3d3caf124c89f91a4c0f353723ac736c58155325f02890adebaa15e16e6464", "Value":{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}},{"TxId":"755d55c281889eaeebf405586f9e25d71d36eb3d35420af833a20a2f53a3eefd", "Value":{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"jerry"}},{"TxId":"819451032d813dde6247f85e56a89262555e04f14788ee33e28b232eef36d98f", "Value":}]
你还可以对数据内容执行富查询,例如通过拥有者 jerry
查询 marble:
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesByOwner","jerry"]}'
输出应该显示出两个属于 jerry
的 marble:
Query Result: [{"Key":"marble2", "Record":{"color":"red","docType":"marble","name":"marble2","owner":"jerry","size":50}},{"Key":"marble3", "Record":{"color":"blue","docType":"marble","name":"marble3","owner":"jerry","size":70}}]
为什么是 CouchDB¶
CouchDB 是一种 NoSQL 解决方案。它是一个面向文档的数据库,其中文档字段存储为键值映射。字段可以是简单的键值对、列表或映射。
除了 LevelDB 支持的键值、复合键、键范围查询外,CouchDB 还支持完整数据的富查询功能,例如针对整个区块链数据的无键查询,因为其数据内容以 JSON 格式存储所以是可查询的。因此,CouchDB 可以用于链码,审计和需求报告等许多 LevelDB 不支持的用例。
CouchDB 还可以增强区块链中的合规性和数据保护的安全性。因为它能够通过过滤和屏蔽事务中的各个属性来实现字段级安全性,并且在需要时只授权只读权限。
此外,CouchDB 属于 CAP 定理的 AP 类型(可用性和分区容错性)。它使用具有 最终一致性
的主-主复制模型。更多的信息可以在这里找到: Eventual Consistency page of the CouchDB documentation 。
CouchDB 是 Fabric 的第一个外部可插拔状态数据库,可能也应该有其他外部数据库选项。例如,IBM 为其区块链启用了关系数据库。并且 CP 类型(一致性和分区容错性)数据库也可能需要,以便在没有应用程序级别保证的情况下实现数据一致性。
关于数据持久化的提示¶
如果需要在节点容器或者 CouchDB 容器进行数据持久化,一种选择是将 docker 容器内相应的目录挂载到容器所在的宿主机的一个目录中。例如,你可以添加下列的两行到 docker-compose-base.yaml
文件中指定节点容器的配置中:
volumes:
- /var/hyperledger/peer0:/var/hyperledger/production
对于 CouchDB 容器,你可以在 CouchDB 的约定中添加两行:
volumes:
- /var/hyperledger/couchdb0:/opt/couchdb/data
故障排除¶
始终保持你的网络是全新的。使用以下命令来移除之前生成的构件、证书文件、容器以及链码镜像:
./byfn.sh down
注解
如果你不移除旧的容器和镜像,你 将会 看到错误信息
如果你看到相关的 Docker 错误信息,首先检查你的版本( 准备阶段 ),然后重启你的 Docker 进程。Docker 的问题通常不会被立即识别。例如,你可能看到由于容器内未能找到密钥材料导致的错误。
如果坚持删除你的镜像,并从头开始:
docker rm -f $(docker ps -aq) docker rmi -f $(docker images -q)
如果在你创建、实例化、调用或者查询的时候报错,请确保你已经更新了通道和链码的名字。提供的示例命令中有占位符。
如果你看到如下错误:
Error: Error endorsing chaincode: rpc error: code = 2 desc = Error installing chaincode code mycc:1.0(chaincode /var/hyperledger/production/chaincodes/mycc.1.0 exits)
你可能有以前运行的链码镜像(例如
dev-peer1.org2.example.com-mycc-1.0
或dev-peer0.org1.example.com-mycc-1.0
)。删除它们,然后重试。docker rmi -f $(docker images | grep peer[0-9]-peer[0-9] | awk '{print $3}')
如果你看到类似以下内容的错误信息:
Error connecting: rpc error: code = 14 desc = grpc: RPC failed fast due to transport failure Error: rpc error: code = 14 desc = grpc: RPC failed fast due to transport failure
请确保你的 fabric 网络运行在被标记为 “latest” 的 “1.0.0” 镜像上。
如果你看到类似以下内容的错误信息:
[configtx/tool/localconfig] Load -> CRIT 002 Error reading configuration: Unsupported Config Type "" panic: Error reading configuration: Unsupported Config Type ""
那么你没有正确设置
FABRIC_CFG_PATH
环境变量。configtxgen 工具需要这个变量才能找到 configtx.yaml。返回并执行export FABRIC_CFG_PATH=$PWD
,然后重新创建通道构件。要清理网络,请使用
down
选项:./byfn.sh down
如果你看到一条指示你依然有 “active endpoints” ,然后你应该清理你的 Docker 网络。这将会清除你之前的网络并且给你一个全新的环境:
docker network prune
你会看到下面的内容:
WARNING! This will remove all networks not used by at least one container. Are you sure you want to continue? [y/N]
选择
y
。如果你看到类似以下内容的错误信息:
/bin/bash: ./scripts/script.sh: /bin/bash^M: bad interpreter: No such file or directory
请确保问题中的文件(本例是 script.sh )被编码为 Unix 格式。这主要可能是由于你的 Git 配置没有设置
core.autocrlf
为false
。有几种方法解决。例如,如果您有权访问 vim 编辑器,打开这个文件:vim ./fabric-samples/first-network/scripts/script.sh
通过下面的命令改变它的编码:
:set ff=unix
注解
如果你仍旧看到了错误,请把你的日志分享在 Hyperledger Rocket Chat fabric-questions 频道上或者 StackOverflow 。
向通道添加组织¶
注解
确保你已经下载了 安装示例、二进制文件和 Docker 镜像 和 准备阶段 中所罗列的和本文版
本左边内容列表的底部可以查看)一致的镜像和二进制。特别注意,在你的版
本中,fabric-samples
文件夹必须包含 eyfn.sh
( “Extending
Your First Network” )脚本以及和它相关的脚本。
本教程是 构建你的第一个网络 ( BYFN ) 教程的扩展,将演示一个由 BYFN 自动生成的新
的组织 – Org3
– 加入到应用通道 ( mychannel
) 的过程。本教程假设你对 BYFN
有很好地理解,包括用法以及上面提及的工具的功能。
虽然我们在这里将只关注新组织的集成,但执行其他通道配置更新(如更新修改策略,调整块大小) 也可以采取相同的方式。要了解更多的通道配置更新的相关过程,请查看 更新通道配置 。 值得注意的是,像本文演示的这些通道配置更新通常是组织管理者(而非链码或者应用开发者)的 职责。
注解
在继续本文前先确保自动化脚本 byfn.sh
运行无误。如果你已经把你的二进
制文件和相关工具(如 cryptogen
,configtxgen
等)放在了 PATH 变量
指定的路径下,你可以修改相应的命令而不使用全路径。
环境构建¶
你将从你克隆到本地的 fabric-samples
的子目录 first-network
进行操作。现在,
进入那个目录。你还要打开一些额外的终端窗口以便于使用。
首先,使用 byfn.sh
脚本清理环境。这个命令会清除运行、终止状态的容器,并且移除之前
生成的构件。关闭 Fabric 网络并非执行通道配置升级的 必要 步骤。但是为了本教程,我们
希望从一个已知的初始状态开始,因此让我们运行以下命令来清理之前的环境:
./byfn.sh down
现在生成默认的 BYFN 构件:
./byfn.sh generate
启动网络,并执行 CLI 容器内的脚本:
./byfn.sh up
现在你的机器上运行着一个干净的 BYFN 版本,你有两种不同的方式可选。第一种,我们提供 了一个有很好注释的脚本,来执行把 Org3 加入网络的配置交易更新。
我们也提供同样过程的“手动”版本,演示每一个步骤并解释它完成了什么(我们在之前演示了 如何停止你的网络,你可以先运行那个脚本,然后再来看每个步骤)。
使用脚本将 Org3 加入通道¶
在 first-network
目录下,简单地执行以下命令来使用脚本:
./eyfn.sh up
此处的输出值得一读。你可以看到添加了 Org3 的加密材料,配置更新被创建和签名,然后安装 链码, Org3 就可以执行账本查询了。
如果一切顺利,你会看到以下信息:
========= All GOOD, EYFN test execution completed ===========
eyfn.sh
可以像 byfn.sh
一样使用 Node.js 链码和数据库选项,如下所示
(替代 ./byfn.sh up
):
./byfn.sh up -c testchannel -s couchdb -l node
然后:
./eyfn.sh up -c testchannel -s couchdb -l node
对于想要详细了解该过程的人,文档的剩余部分会为你展示通道升级的每个命令,以及命令的 作用。
Bring Org3 into the Channel Manually¶
注解
下面的步骤均假设 CORE_LOGGING_LEVEL
变量在 cli
和 Org3cli
容器中设置为 DEBUG
。
对于 cli
容器,你可以通过修改 first-network
目录下的
docker-compose-cli.yaml
文件来配置。例如:
cli:
container_name: cli
image: hyperledger/fabric-tools:$IMAGE_TAG
tty: true
stdin_open: true
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
#- FABRIC_LOGGING_SPEC=INFO
- FABRIC_LOGGING_SPEC=DEBUG
对于 Org3cli
容器,你可以通过修改 first-network
目录下的
docker-compose-org3.yaml
文件来配置。例如:
Org3cli:
container_name: Org3cli
image: hyperledger/fabric-tools:$IMAGE_TAG
tty: true
stdin_open: true
environment:
- GOPATH=/opt/gopath
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
#- FABRIC_LOGGING_SPEC=INFO
- FABRIC_LOGGING_SPEC=DEBUG
如果你已经使用了 eyfn.sh
脚本,你需要先关闭你的网络。通过如下所示命令来完成:
./eyfn.sh down
这会关闭网络,删除所有的容器,并且撤销我们添加 Org3 的操作。
当网络停止后,再次将它启动起来。
./byfn.sh generate
然后:
./byfn.sh up
这会将你的网络恢复到你执行 eyfn.sh
脚本之前的状态。
现在我们可以手动添加 Org3 了。第一步,我们需要生成 Org3 的加密材料。
生成 Org3 加密材料¶
在另一个终端,切换到 first-network
的子目录 org3-artifacts
中。
cd org3-artifacts
这里需要关注两个 yaml
文件: org3-crypto.yaml
和 configtx.yaml
。首先,
生成 Org3 的加密材料:
../../bin/cryptogen generate --config=./org3-crypto.yaml
该命令读取我们新的加密配置的 yaml
文件 – org3-crypto.yaml
– 然后调用
cryptogen
来为 Org3 CA 和其他两个绑定到这个新组织的节点生成秘钥和证书。就像
BYFN 实现的,加密材料放到当前目录新生成的 crypto-config
文件夹下(在我们例子
中是 org3-artifacts
)。
现在使用 configtxgen
工具以 JSON 格式打印出 Org3 对应的配置材料。我们将在执
行命令时告诉这个工具去获取当前目录的 configtx.yaml
文件。
export FABRIC_CFG_PATH=$PWD && ../../bin/configtxgen -printOrg Org3MSP > ../channel-artifacts/org3.json
上面的命令会创建一个 JSON 文件 – org3.json
– 并把文件输出到 first-network
的 channel-artifacts
子目录下。这个文件包含了 Org3 的策略定义,还有三个 base 64
格式的重要的证书:管理员用户证书(之后作为 Org3 的管理员角色),一个根证书,一个 TLS
根证书。之后的步骤我们会将这个 JSON 文件追加到通道配置。
我们最后的工作是拷贝排序节点的 MSP 材料到 Org3 的 crypto-config
目录下。我们
尤其关注排序节点的 TLS 根证书,它可以用于 Org3 的节点和网络的排序节点间的安全通信。
cd ../ && cp -r crypto-config/ordererOrganizations org3-artifacts/crypto-config/
Now we’re ready to update the channel configuration…
现在我们准备开始升级通道配置。¶
更新的步骤需要用到配置转换工具 – configtxlator
。这个工具提供了独立于 SDK 的
无状态 REST API。它还额外提供了 CLI,用于简化 Fabric 网络中的配置任务。这个工具对
不同的数据表示或格式间的转化提供了便利的功能(在这个例子中就是 protobufs 和 JSON
格式的互转)。另外,这个工具能基于两个不同的通道配置计算出配置更新交易。
首先,进入到 CLI 容器。这个容器挂载了 BYFN 的 crypto-config
目录,允许我们访问之
前两个节点组作织和排序组织的 MSP 材料。默认的身份是 Org1 的管理员用户,所以如果我们
想作为 Org2 进行任何操作,需要设置和 MSP 相关的环境变量。
docker exec -it cli bash
设置 ORDERER_CA
和 CHANNEL_NAME
变量:
export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem && export CHANNEL_NAME=mychannel
检查并确保环境变量已正确设置:
echo $ORDERER_CA && echo $CHANNEL_NAME
注解
如果需要重启 CLI 容器,你需要重新设置 ORDERER_CA
和 CHANNEL_NAME
这两个
环境变量。
获取配置¶
现在我们有了一个设置了 ORDERER_CA
和 CHANNEL_NAME
环境变量的 CLI 容器。让我们
获取通道 mychannel
的最新的配置区块。
我们必须拉取最新版本配置的原因是通道配置元素是版本化的。版本管理由于一些原因显得很重要。 它可以防止通道配置更新被重复或者重放攻击(例如,回退到带有旧的 CRLs 的通道配置将会产生 安全风险)。同时它保证了并行性(例如,如果你想从你的通道中添加新的组织后,再删除一个组 织 ,版本管理可以帮助你移除想移除的那个组织,并防止移除两个组织)。
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA
这个命令将通道配置区块以二进制 protobuf 形式保存在 config_block.pb
。注意文件的
名字和扩展名可以任意指定。然而,为了便于识别,我们建议根据区块存储对象的类型和编码格
式( protobuf 或 JSON )进行命名。
当你执行 peer channel fetch
命令后,在终端上会有相当数量的打印输出。日志的最后一
行比较有意思:
2017-11-07 17:17:57.383 UTC [channelCmd] readBlock -> DEBU 011 Received block: 2
这是告诉我们最新的 mychannel
的配置区块实际上是区块 2, 并非 初始区块。 peer
channel fetch config
命令默认返回目标通道最新的配置区块,在这个例子里是第三个区块。
这是因为 BYFN 脚本分别在两个不同通道更新交易中为两个组织 – Org1
和 Org2
– 定
义了锚节点。
最终,我们有如下的配置块序列:
- block 0: genesis block
- block 1: Org1 anchor peer update
- block 2: Org2 anchor peer update
将配置转换到 JSON 格式并裁剪¶
现在我们用 configtxlator
工具将这个通道配置解码为 JSON 格式(以便友好地被阅读
和修改)。我们也必须裁剪所有的头部、元数据、创建者签名等和我们将要做的修改无关的内
容。我们通过 jq
这个工具来完成裁剪:
.. code:: bash
configtxlator proto_decode –input config_block.pb –type common.Block | jq .data.data[0].payload.data.config > config.json
我们得到一个裁剪后的 JSON 对象 – config.json
,放置在 fabric-samples
下的 first-network
文件夹中 – first-network
是我们配置更新的基准工作
目录。
花一些时间用你的文本编辑器(或者你的浏览器)打开这个文件。即使你已经完成了这个教程, 也值得研究下它,因为它揭示了底层配置结构,和能做的其它类型的通道更新升级。我们将在 更新通道配置 更详细地讨论。
添加Org3加密材料¶
注解
目前到这里你做的步骤和其他任何类型的配置升级所需步骤几乎是一致的。我们之 所以选择在教程中添加一个组织,是因为这是能做的配置升级里最复杂的一个。
我们将再次使用 jq
工具去追加 Org3 的配置定义 – org3.json
– 到通道的应用组
字段,同时定义输出文件是 – modified_config.json
。
jq -s '.[0] * {"channel_group":{"groups":{"Application":{"groups": {"Org3MSP":.[1]}}}}}' config.json ./channel-artifacts/org3.json > modified_config.json
现在,我们在 CLI 容器有两个重要的 JSON 文件 – config.json
和
modified_config.json
。初始的文件包含 Org1 和 Org2 的材料,而 “modified” 文件包
含了总共 3 个组织。现在只需要将这 2 个 JSON 文件重新编码并计算出差异部分。
首先,将 config.json
文件倒回到 protobuf 格式,命名为 config.pb
:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
下一步,将 modified_config.json
编码成 modified_config.pb
:
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
现在使用 configtxlator
去计算两个protobuf 配置的差异。这条命令会输出一个新的
protobuf 二进制文件,命名为 org3_update.pb
。
configtxlator compute_update --channel_id $CHANNEL_NAME --original config.pb --updated modified_config.pb --output org3_update.pb
这个新的 proto 文件 – org3_update.pb
– 包含了 Org3 的定义和指向 Org1 和 Org2
材料的更高级别的指针。我们可以抛弃 Org1 和 Org2 相关的 MSP 材料和修改策略信息,因
为这些数据已经存在于通道的初始区块。因此,我们只需要两个配置的差异部分。
在我们提交通道更新前,我们执行最后做几个步骤。首先,我们将这个对象解码成可编辑的
JSON 格式,并命名为 org3_update.json
。
configtxlator proto_decode --input org3_update.pb --type common.ConfigUpdate | jq . > org3_update.json
现在,我们有了一个解码后的更新文件 – org3_update.json
– 我们需要用信封消息来包装它。这
个步骤要把之前裁剪掉的头部信息还原回来。我们将命名这个新文件为 org3_update_in_envelope.json
。
code:: bash
echo ‘{“payload”:{“header”:{“channel_header”:{“channel_id”:”mychannel”, “type”:2}},”data”:{“config_update”:’$(cat org3_update.json)’}}}’ | jq . > org3_update_in_envelope.json
使用我们格式化好的 JSON – org3_update_in_envelope.json
– 我们最后一次使用
configtxlator
工具将他转换为 Fabric 需要的完整独立的 protobuf 格式。我们将最
后的更新对象命名为 org3_update_in_envelope.pb
。
configtxlator proto_encode --input org3_update_in_envelope.json --type common.Envelope --output org3_update_in_envelope.pb
签名并提交配置更新¶
差不多大功告成了!
我们现在有一个 protobuf 二进制文件 – org3_update_in_envelope.pb
– 在我们的 CLI 容
器内。但是,在配置写入到账本前,我们需要来自必要的 Admin 用户的签名。我们通道应用组的修
改策略(mod_policy)设置为默认值 “MAJORITY”,这意味着我们需要大多数已经存在的组织管理员
去签名这个更新。因为我们只有两个组织 – Org1 和 Org2 – 所以两个的大多数也还是两个,我们
需要它们都签名。没有这两个签名,排序服务会因为不满足策略而拒绝这个交易。
首先,让我们以 Org1 管理员来签名这个更新 proto 。因为 CLI 容器是以 Org1 MSP 材料启动的,
所以我们只需要简单地执行 peer channel signconfigtx
命令:
peer channel signconfigtx -f org3_update_in_envelope.pb
最后一步,我们将 CLI 容器的身份切换为 Org2 管理员。为此,我们通过导出和 Org2 MSP 相 关的 4 个环境变量。
注解
切换不同的组织身份为配置交易签名(或者其他事情)不能反映真实世界里 Fabric 的操作。 一个单一容器不可能挂载了整个网络的加密材料。相反地,配置更新需要在网络外安全地递交 给 Org2 管理员来审查和批准。
导出 Org2 的环境变量:
# you can issue all of these commands at once
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=peer0.org2.example.com:9051
最后,我们执行 peer channel update
命令。Org2 管理员在这个命令中会附带签名,因
此就没有必要对 protobuf 进行两次签名。
注解
将要做的对排序服务的更新调用,会经历一系列的系统级签名和策略检查。你会发现
通过检视排序节点的日志流会非常有用。在另外一个终端执行
docker logs -f orderer.example.com
命令就能展示它们了。
发起更新调用:
peer channel update -f org3_update_in_envelope.pb -c $CHANNEL_NAME -o orderer.example.com:7050 --tls --cafile $ORDERER_CA
如果你的更新提交成功,将会看到一个类似如下的摘要提示信息:
2018-02-24 18:56:33.499 UTC [msp/identity] Sign -> DEBU 00f Sign: digest: 3207B24E40DE2FAB87A2E42BC004FEAA1E6FDCA42977CB78C64F05A88E556ABA
你也会看到配置交易的提交: .. code:: bash
2018-02-24 18:56:33.499 UTC [channelCmd] update -> INFO 010 Successfully submitted channel update
成功的通道更新调用会返回一个新的区块 – 区块 5 – 给所有在这个通道上的节点。你是否
还记得,区块 0-2 是初始的通道配置,而区块 3 和 4 是链码 mycc
的实例化和调用。所
以,区块 5 就是带有 Org3 定义的最新的通道配置。
查看 peer0.org1.example.com
的日志:
docker logs -f peer0.org1.example.com
如果你想查看新的配置区块的内容,可以跟着示范的过程获取和解码配置区块
配置领导节点选举¶
注解
引入这个章节作为通用参考,是为了理解在完成网络通道配置初始化之后,增加
组织时,领导节点选举的设置。这个例子中,默认设置为动态领导选举,这是在
peer-base.yaml
文件中为网络中所有的节点设置的。
新加入的节点是根据初始区块启动的,初始区块是不包含通道配置更新中新加入的组织信息 的。因此新的节点无法利用 gossip 协议,因为它们无法验证从自己组织里其他节点发送过 来的区块,除非它们接收到将组织加入到通道的那个配置交易。新加入的节点必须有以下配 置之一才能从排序服务接收区块:
- 采用静态领导者模式,将节点配置为组织的领导者。
CORE_PEER_GOSSIP_USELEADERELECTION=false
CORE_PEER_GOSSIP_ORGLEADER=true
注解
这个配置对于新加入到通道中的所有节点必须一致。
- 采用动态领导者选举,配置节点采用领导选举的方式:
CORE_PEER_GOSSIP_USELEADERELECTION=true
CORE_PEER_GOSSIP_ORGLEADER=false
注解
因为新加入组织的节点,无法生成成员关系视图,这个选项和静态配置类似,每 个节点启动时宣称自己是领导者。但是,一旦它们更新到了将组织加入到通道的 配置交易,组织中将只会有一个激活状态的领导者。因此,如果你想最终组织的 节点采用领导选举,建议你采用这个配置。
将 Org3 加入通道¶
此时,通道的配置已经更新并包含了我们新的组织 – Org3
– 意味者这个组织下的节点可以加入
到 mychannel
。
首先,让我们部署 Org3 节点容器和 Org3-specific CLI容器。
打开一个新的终端并从 first-network
目录启动 Org3 docker compose :
docker-compose -f docker-compose-org3.yaml up -d
这个新的 compose 文件配置为桥接我们的初始网络,因此两个节点容器和 CLI 容器可以连 接到已经存在的节点和排序节点。当三个容器运行后,进入 Org3-specific CLI 容器:
docker exec -it Org3cli bash
和我们之前初始化 CLI 容器一样,导出两个关键环境变量: ORDERER_CA
和
CHANNEL_NAME
:
export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem && export CHANNEL_NAME=mychannel
检查确保环境变量已经正确设置:
echo $ORDERER_CA && echo $CHANNEL_NAME
现在,我们向排序服务发送一个获取 mychannel
初始区块的请求。如果通道更新成
功执行,排序服务会成功校验这个请求中 Org3 的签名。如果 Org3 没有成功地添加到通
道配置中,排序服务会拒绝这个请求。
注解
再次提醒,你会发现查看排序节点的签名和验签逻辑和策略检查的日志是 很有用的
使用 peer channel fetch
命令来获取这个区块:
peer channel fetch 0 mychannel.block -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA
注意,我们传递了 0
去索引我们在这个通道账本上想要的区块(例如,初始区块)。如
果我们简单地执行 peer channel fetch config
命令,我们将会收到区块 5 – 那个带
有 Org3 定义的更新后的配置。然而,我们的账本不能从一个下游的区块开始 – 我们必须
从区块 0 开始。
执行 peer channel join
命令并指定初始区块 – mychannel.block
:
peer channel join -b mychannel.block
如果你想将第二个节点加入到 Org3 中,导出 TLS
和 ADDRESS
变量,再重新执
行 peer channel join command
。
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org3.example.com/peers/peer1.org3.example.com/tls/ca.crt && export CORE_PEER_ADDRESS=peer1.org3.example.com:12051
peer channel join -b mychannel.block
升级和调用链码¶
这个智力游戏的最后一部分是升级链码的版本,并升级背书策略以加入 Org3 。因为我们知 道马上要做的是升级,将无关紧要的安装版本 1 的链码的过程抛诸脑后吧。我们只关心新版 本,在新版本中 Org3 会成为背书策略的一部分,因此我们直接跳到链码的版本 2 。
从 Org3 CLI 执行:
peer chaincode install -n mycc -v 2.0 -p github.com/chaincode/chaincode_example02/go/
如果你要在 Org3 的第二个节点上安装链码,请相应地修改环境变量并再次执行命令。注意第 二次安装并不是强制的,因为你只需要在背书节点或者和账本有交互行为(比如,只做查询)节 点上安装链码。即使没有运行链码容器,节点作为提交节点仍然会运行检验逻辑。
现在回到 原始 CLI 容器,在 Org1 和 Org2 节点上安装新版本链码。我们使用 Org2 管理 员身份提交通道更新请求,所以容器仍然是代表 “peer0.Org2” :
peer chaincode install -n mycc -v 2.0 -p github.com/chaincode/chaincode_example02/go/
切回 peer0.org1
身份:
export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051
然后再次安装:
peer chaincode install -n mycc -v 2.0 -p github.com/chaincode/chaincode_example02/go/
现在我们已经准备好升级链码。底层的源代码没有任何变化,我们只是简单地在 mychannel
通道上的链码 – mycc
– 的背书策略中增加了 Org3 。
注解
任何满足链码实例化策略的身份都可以执行升级调用。这些身份默认就是通道的管理者。
发送调用:
peer chaincode upgrade -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA -C $CHANNEL_NAME -n mycc -v 2.0 -c '{"Args":["init","a","90","b","210"]}' -P "OR ('Org1MSP.peer','Org2MSP.peer','Org3MSP.peer')"
你可以看到上面的命令,我们用 v
标志指定了新的版本号。你也能看到背书策略修改为
-P "OR ('Org1MSP.peer','Org2MSP.peer','Org3MSP.peer')"
,说明 Org3 要被添加到
策略中。最后一部分注意的是我们的构造请求(用 c
标志指定)。
链码升级和实例化一样需要用到 init
方法。 如果 你的链码需要传递参数给 init
方法,那你需要在这里添加。
升级调用使得通道的账本添加一个新的区块 – 区块 6 – 来允许 Org3 的节点在背书阶段执行
交易。回到 Org3 CLI 容器,并执行对 a
的查询。这需要花费一点时间,因为需要为目标节
点构建链码镜像,链码容器需要运行:
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
我们能看到 Query Result:90
的响应。
现在执行调用,从 a
转移 10
到 b
:
peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile $ORDERER_CA -C $CHANNEL_NAME -n mycc -c '{"Args":["invoke","a","b","10"]}'
最后查询一次:
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["query","a"]}'
我们能看到一个 Query Result: 80
的响应,准确反映了链码的世界状态的更新。
总结¶
通道配置的更新过程是非常复杂的,但是仍然有一个诸多步骤对应的逻辑方法。终局就是为了构建 一个用 protobuf 二进制表达的差异化的交易对象,然后获取必要数量的管理员签名来满足通道的 修改策略。
configtxlator
和 jq
工具,和不断使用的 peer channel
命令,为我们提供了完成
这个任务的基本功能。
Updating the Channel Config to include an Org3 Anchor Peer (Optional)¶
The Org3 peers were able to establish gossip connection to the Org1 and Org2 peers since Org1 and Org2 had anchor peers defined in the channel configuration. Likewise newly added organizations like Org3 should also define their anchor peers in the channel configuration so that any new peers from other organizations can directly discover an Org3 peer.
Continuing from the Org3 CLI, we will make a channel configuration update to define an Org3 anchor peer. The process will be similar to the previous configuration update, therefore we’ll go faster this time.
As before, we will fetch the latest channel configuration to get started.
Inside the CLI container for Org3 fetch the most recent config block for the channel,
using the peer channel fetch
command.
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CHANNEL_NAME --tls --cafile $ORDERER_CA
After fetching the config block we will want to convert it into JSON format. To do this we will use the configtxlator tool, as done previously when adding Org3 to the channel. When converting it we need to remove all the headers, metadata, and signatures that are not required to update Org3 to include an anchor peer by using the jq tool. This information will be reincorporated later before we proceed to update the channel configuration.
configtxlator proto_decode --input config_block.pb --type common.Block | jq .data.data[0].payload.data.config > config.json
The config.json
is the now trimmed JSON representing the latest channel configuration
that we will update.
Using the jq tool again, we will update the configuration JSON with the Org3 anchor peer we want to add.
jq '.channel_group.groups.Application.groups.Org3MSP.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "peer0.org3.example.com","port": 11051}]},"version": "0"}}' config.json > modified_anchor_config.json
We now have two JSON files, one for the current channel configuration,
config.json
, and one for the desired channel configuration modified_anchor_config.json
.
Next we convert each of these back into protobuf format and calculate the delta between the two.
Translate config.json
back into protobuf format as config.pb
configtxlator proto_encode --input config.json --type common.Config --output config.pb
Translate the modified_anchor_config.json
into protobuf format as modified_anchor_config.pb
configtxlator proto_encode --input modified_anchor_config.json --type common.Config --output modified_anchor_config.pb
Calculate the delta between the two protobuf formatted configurations.
configtxlator compute_update --channel_id $CHANNEL_NAME --original config.pb --updated modified_anchor_config.pb --output anchor_update.pb
Now that we have the desired update to the channel we must wrap it in an envelope message so that it can be properly read. To do this we must first convert the protobuf back into a JSON that can be wrapped.
We will use the configtxlator command again to convert anchor_update.pb
into anchor_update.json
configtxlator proto_decode --input anchor_update.pb --type common.ConfigUpdate | jq . > anchor_update.json
Next we will wrap the update in an envelope message, restoring the previously
stripped away header, outputting it to anchor_update_in_envelope.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"mychannel", "type":2}},"data":{"config_update":'$(cat anchor_update.json)'}}}' | jq . > anchor_update_in_envelope.json
Now that we have reincorporated the envelope we need to convert it to a protobuf so it can be properly signed and submitted to the orderer for the update.
configtxlator proto_encode --input anchor_update_in_envelope.json --type common.Envelope --output anchor_update_in_envelope.pb
Now that the update has been properly formatted it is time to sign off and submit it. Since this
is only an update to Org3 we only need to have Org3 sign off on the update. As we are
in the Org3 CLI container there is no need to switch the CLI containers identity, as it is
already using the Org3 identity. Therefore we can just use the peer channel update
command
as it will also sign off on the update as the Org3 admin before submitting it to the orderer.
peer channel update -f anchor_update_in_envelope.pb -c $CHANNEL_NAME -o orderer.example.com:7050 --tls --cafile $ORDERER_CA
The orderer receives the config update request and cuts a block with the updated configuration. As peers receive the block, they will process the configuration updates.
Inspect the logs for one of the peers. While processing the configuration transaction from the new block, you will see gossip re-establish connections using the new anchor peer for Org3. This is proof that the configuration update has been successfully applied!
docker logs -f peer0.org1.example.com
2019-06-12 17:08:57.924 UTC [gossip.gossip] learnAnchorPeers -> INFO 89a Learning about the configured anchor peers of Org1MSP for channel mychannel : [{peer0.org1.example.com 7051}]
2019-06-12 17:08:57.926 UTC [gossip.gossip] learnAnchorPeers -> INFO 89b Learning about the configured anchor peers of Org2MSP for channel mychannel : [{peer0.org2.example.com 9051}]
2019-06-12 17:08:57.926 UTC [gossip.gossip] learnAnchorPeers -> INFO 89c Learning about the configured anchor peers of Org3MSP for channel mychannel : [{peer0.org3.example.com 11051}]
Congratulations, you have now made two configuration updates — one to add Org3 to the channel, and a second to define an anchor peer for Org3.
升级你的网络组件¶
注解
在本文中所说的“升级”,是指改变组件的版本(比如,将 v1.3 的二进制文件升 级到 v1.4 )。另外,“更新”不是指版本,而是指改变配置,比如更新一个通道 配置或者部署脚本。因为在 Fabric 中没有技术层面所说的数据迁移,所以我们 不用“迁移”的说法。
注解
另外,如果你的网络不是使用 Fabric v1.3 ,参照教程 Upgrading Your Network to v1.3 。 本文仅适用与从 v1.3 到 v1.4 的升级,并不适用其他版本到 v1.4 。
概览¶
While upgrade to v1.4.0 does not require any capabilities to be enabled, v1.4.2 offers new capabilities at the orderer, channel, and application levels. Specifically, the new v1.4.2 capabilities enable the following features:
- Migration from Kafka to Raft consensus (requires v1.4.2 orderer and channel capabilities)
- Ability to specify orderer endpoints per organization (requires v1.4.2 channel capability)
- Ability to store private data for invalidated transactions (requires v1.4.2 application capability)
Because not all users need these new features, enabling the v1.4.2 capabilities is considered optional (though recommended), and will be detailed in a section after the main body of this tutorial.
因为 构建你的第一个网络 (BYFN)教程默认使用的是“最新”的程序,如果你是在 v1.4 发 布之后运行的,那你的机器上就运行的是 v1.4 的程序和工具,就不用再升级他们了。
所以,本教程将提供一个基于 Hyperledger Fabric v1.3 程序的网络,然后来升级到 v1.4。
整体来看,我们的升级教程有如下步骤:
- Backup the ledger and MSPs.
- Upgrade the orderer binaries to Fabric v1.4.x.
- Upgrade the peer binaries to Fabric v1.4.x.
- Update channel capabilities to 1.4.2 (optional).
This tutorial will demonstrate how to perform each of these steps individually with CLI commands. Instructions for both scripted execution and manual execution are included.
注解
因为 BYFN 使用的是 “SOLO” 排序服务(只有一个排序节点),我们的脚本会下载 整个网络。但是在生产环境中,排序节点和节点可以同时进行滚动升级。也就是说, 我们可以在任何一个排序节点升级程序而不需要关闭网络。
因为 BYFN 不包含下边的组件,所以我们升级 BYFN 的脚本也不包含他们:
- Fabric CA
- Kafka
- CouchDB
- SDK
这些组件的更新过程 — 如果必要的话 — 将会包含在本教程后边的章节中。 我们还会演示怎么升级 Node 链码。
从操作的角度来说,值得注意的是 v1.4 改变了收集日志的方式,从 CORE_LOGGING_LEVEL
(节点的)和 ORDERER_GENERAL_LOGLEVEL
(排序节点的) 变成了 FABRIC_LOGGING_SPEC
(新的操作服务的)。更多信息请查阅 Fabric release notes 。
启动一个 v1.3 的网络¶
在你要升级到 v1.4.x 之前,你必须先准备一个运行 Fabric v1.3 镜像的网络。
就像 BYFN 教程那样,我们将在你克隆到本地的 fabric-samples
的子目录 first-network
中进行操作。现在要切换到那个目录。你也需要打开几个终端以备使用。
生成密钥并启动网络¶
在一个干净的环境中,使用如下四个命令启动我们的 v1.3 BYFN 网络:
git fetch origin
git checkout v1.3.0
./byfn.sh generate
./byfn.sh up -t 3000 -i 1.3.0
注解
如果你本地已编译 v1.3 的镜像,它们将被示例程序直接使用。如果你遇到了错误,请清 除你本地编译的 v1.3 的镜像,并重新运行示例程序。这将从 docker hub 下载 v1.3 的 镜像。
如果 BYFN 正常启动,你将看到:
===================== All GOOD, BYFN execution completed =====================
我们现在就可以准备升级我们的网络到 Hyperledger Fabric v1.4.x 了。
获取最新的示例程序¶
注解
The instructions below pertain to whatever is the most recently published version of v1.4.x. Please substitute 1.4.x with the version identifier of the published release that you are testing, for example, replace ‘1.4.x’ with ‘1.4.2’.
Before completing the rest of the tutorial, it’s important to switch to the v1.4.x (for example, 1.4.2) version of the samples you are upgrading to. For v1.4.2, this would be:
git checkout v1.4.2
想现在升级么?
我们有一个脚本可以升级 BYFN 的所有并开启所有能力(注意,在 v1.4 中不需要新的 能力)。如果你在运行一个生产环境的网络,或者你是网络中一些部分的管理员,这个 脚本可以为你的升级工作提供一个模板。
接下来,我们将带你熟悉脚本的每一步,并讲解代码中的每一部分是如何完成升级操作的。
To run the script to upgrade from v1.3 to v1.4.x, issue this command (substituting
your preferred release number for x
). Note that the script to upgrade to v1.4.2
will also upgrade the channel capabilities.
./byfn.sh upgrade -i 1.4.2
如果升级成功,你将看到如下信息:
===================== All GOOD, End-2-End UPGRADE Scenario execution completed =====================
If you want to upgrade the network manually, simply run ./byfn.sh down
again
and perform the steps up to — but not including — the ./byfn.sh upgrade
step. Then proceed to the next section.
Note that many of the commands you’ll run in this section will not result in any output. In general, assume no output is good output.
Upgrade the orderer containers¶
排序容器应该以滚动方式升级(每次升级一个)。从上层来说,排序的升级过程如下:
- 停止排序节点。
- 备份排序节点的账本和 MSP 。
- 使用最新镜像重启排序节点。
- 验证升级完整性。
As a consequence of leveraging BYFN, we have a Solo orderer setup, therefore, we will only perform this process once. In a Kafka or Raft setup, however, this process will have to be repeated on each orderer.
注解
本教程使用 docker 部署。对于原生的部署,需要将 orderer
文件替换为
新发布的。备份 orderer.yaml
,并使用新发布的构件中的 orderer.yaml
替换。然后使用旧 orderer.yaml
文件中的变量替换新文件。你可以使用
diff
之类的工具帮你比较。
现在我们从 关闭排序节点 开始升级过程:
docker stop orderer.example.com
export LEDGERS_BACKUP=./ledgers-backup
# Note, replace '1.4.x' with a specific version, for example '1.4.2'.
# Set IMAGE_TAG to 'latest' if you prefer to default to the images tagged 'latest' on your system.
export IMAGE_TAG=$(go env GOARCH)-1.4.x
我们创建了一个存放备份文件的目录的环境变量,并导出了我们想到升级到的 IMAGE_TAG
。
当排序节点关闭之后,你就需要 备份账本和 MSP :
mkdir -p $LEDGERS_BACKUP
docker cp orderer.example.com:/var/hyperledger/production/orderer/ ./$LEDGERS_BACKUP/orderer.example.com
在生产环境中,这个过程需要在每一个基于 Raft 的排序节点上以滚动的方式重复。
现在 下载并重启排序节点 的新 Fabric 镜像:
docker-compose -f docker-compose-cli.yaml up -d --no-deps orderer.example.com
因为我们的示例中使用的是 “solo” 类型的排序服务,所以在网络中没有其他的排序节点需要和重启后
的排序节点进行同步。然而,在使用 Kafka 的生产网络中,最好先执行 peer channel fetch <blocknumber>
,
以验证排序节点在重启后是否同步到了其他排序节点上的数据。
升级节点容器¶
下一步,我们来看一下怎么将节点容器升级到 Fabric v1.4.x 。节点容器和排序节点容器一样也需要以 滚动的方式升级(每次升级一个)。就像在升级排序节点时提要的一样,排序节点和节点可以同时升级, 但是本教程我们单独来做。从上层来说,我们的操作步骤如下:
- 停止节点。
- 备份节点账本和 MSP 。
- 删除链码容器和镜像。
- 使用最新的镜像重启节点。
- 验证升级完整性。
我们的网络中运行了四个节点。我们将在每一个节点上进行一次操作,一共四次升级。
注解
再说一次,本教程使用了 docker 部署。对于 原生 的部署,需要将 peer
文件替换为发布版构件。备份 core.yaml
,并使用新发布的构件中的 core.yaml
替换。然后使用旧 core.yaml
文件中的变量替换新文件。你可以使用
diff
之类的工具帮你比较。
我们使用如下命令 关闭第一个节点 :
export PEER=peer0.org1.example.com
docker stop $PEER
然后 备份节点的账本和 MSP
mkdir -p $LEDGERS_BACKUP
docker cp $PEER:/var/hyperledger/production ./$LEDGERS_BACKUP/$PEER
当节点停止并备份好账本之后, 删除节点链码容器 :
CC_CONTAINERS=$(docker ps | grep dev-$PEER | awk '{print $1}')
if [ -n "$CC_CONTAINERS" ] ; then docker rm -f $CC_CONTAINERS ; fi
和节点链码镜像:
CC_IMAGES=$(docker images | grep dev-$PEER | awk '{print $1}')
if [ -n "$CC_IMAGES" ] ; then docker rmi -f $CC_IMAGES ; fi
我们将重新使用 v1.4.x 镜像标签重启节点:
docker-compose -f docker-compose-cli.yaml up -d --no-deps $PEER
注解
而且 BYFN 支持使用 CouchDB,本教程的操作仅仅是一个简单的示例。如果 你使用 CouchDB,请执行下边的命令:
docker-compose -f docker-compose-cli.yaml -f docker-compose-couch.yaml up -d --no-deps $PEER
注解
你不需要重启链码容器。当节点获得一个链码请求的时候( invoke 或者 query ) ,它会先检查是否运行了链码的拷贝。如果是,就使用它。反之, 就像本例中一样,节点会重新加载链码(需要的话会重新编译镜像)。
验证节点升级完整性¶
我们已经升级完了我们的第一个节点,但是在继续之前,我们要执行一下链码以确保升级成功。
注解
在我们尝试这个之前,你可能需要升级足够组织的节点以满足背书策略。而且,如 果你升级的过程中更新了链码,这就是必须的。如果你升级的过程中没有更新链码, 运行在不同 Fabric 版本上的节点也可以背书成功。
在我们进入 CLI 容器并执行 invoke 之前,使用以下命令确定 CLI 更新到了当前版本:
docker-compose -f docker-compose-cli.yaml stop cli
docker-compose -f docker-compose-cli.yaml up -d --no-deps cli
Then, get back into the CLI container:
docker exec -it cli bash
现在你需要设置两个环境变量 — 通道名和 ORDERER_CA
名:
CH_NAME=mychannel
ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
现在你可以执行 invoke :
peer chaincode invoke -o orderer.example.com:7050 --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:9051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --tls --cafile $ORDERER_CA -C $CH_NAME -n mycc -c '{"Args":["invoke","a","b","10"]}'
我们之前查询 a
的结果是 90
,而且我们在 invoke 的时候转移了 10
,所以
a
的查询结果应该是 80
。我们看一下:
peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'
你应该看到如下:
Query Result: 80
当成功验证节点的升级候,继续执行升级节点前,请确认执行了 exit
离开容器。
你可以通过导出不同的节点名字来重复执行上边的步骤。
export PEER=peer1.org1.example.com
export PEER=peer0.org2.example.com
export PEER=peer1.org2.example.com
Update channel capabilities to v1.4.2 (optional)¶
注解
A reminder that while we show how to enable v1.4.2 capabilities as part of this tutorial, this is an optional step UNLESS you are leveraging the v1.4.2 features that require the capabilities.
Although Fabric binaries can and should be upgraded in a rolling fashion, it is important to finish upgrading binaries before enabling capabilities. Any binaries which are not upgraded to v1.4.2 before enabling the new v1.4.2 capabilities may intentionally crash to indicate a misconfiguration which could otherwise result in a forked blockchain.
Once a capability has been enabled, it becomes part of the permanent record for that channel. This means that even after disabling the capability, old binaries will not be able to participate in the channel because they cannot process beyond the block which enabled the capability to get to the block which disables it. As a result, once a capability has been enabled, disabling it is neither recommended nor supported.
For this reason, think of enabling channel capabilities as a point of no return. Please experiment with the new capabilities in a test setting and be confident before proceeding to enable them in production.
Capabilities are enabled through a channel configuration transaction. For more information on updating channel configs, check out 向通道添加组织 or the doc on 更新通道配置.
To learn about what the new capabilities are in v1.4.2 and what they enable, refer back to the Overview_.
We will enable these capabilities in the following order:
- Orderer System Channel
- Orderer Group
- Channel Group
- Individual Channels
- Orderer Group
- Channel Group
- Application Group
Updating a channel configuration is a three step process:
- Get the latest channel config
- Create a modified channel config
- Create a config update transaction
注解
In a real world production network, these channel config updates would be handled by the admins for each channel. Because BYFN all exists on a single machine, it is possible for us to update each of these channels.
For more information on updating channel configs, click on 向通道添加组织 or the doc on 更新通道配置.
Orderer System Channel Capabilities¶
Because only ordering organizations admins can update the ordering system channel, we need set environment variables for the system channel that will allow us to carry out these tasks. Issue each of these commands:
CORE_PEER_LOCALMSPID="OrdererMSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/users/Admin@example.com/msp
ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
If we’re upgrading from v1.3 to v1.4.2, we need to set the system channel name
to testchainid
:
CH_NAME=testchainid
If we’re upgrading from v1.4.1 to v1.4.2, we need to set the system channel name
to byfn-sys-channel
:
CH_NAME=byfn-sys-channel
Orderer Group¶
The first step in updating a channel configuration is getting the latest config block:
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CH_NAME –tls –cafile $ORDERER_CA
configtxlator proto_decode –input config_block.pb –type common.Block –output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
Next, add capabilities to the orderer group. The following command will create a copy of the config file and change the capability level:
jq -s '.[0] * {"channel_group":{"groups":{"Orderer": {"values": {"Capabilities": .[1]}}}}}' config.json ./scripts/capabilities.json > modified_config.json
Now we can create the config update:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb
Package the config update into a transaction:
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
Submit the config update transaction:
peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA
Our config update transaction represents the difference between the original config and the modified one, but the ordering service will translate this into a full channel config.
Channel Group¶
Now let’s move on to updating the capability level for the channel group at the orderer system level.
The first step, as before, is to get the latest channel configuration.
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CH_NAME --tls --cafile $ORDERER_CA
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
Next, create a modified channel config:
jq -s '.[0] * {"channel_group":{"values": {"Capabilities": .[1]}}}' config.json ./scripts/capabilities.json > modified_config.json
Create the config update transaction:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb
Package the config update into a transaction:
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
Submit the config update transaction:
peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA
Enabling Capabilities on Existing Channels¶
Now that we have updating the capabilities on the ordering system channel, we
need to updating the configuration of any existing application channels. We only
have one application channel: mychannel
. So let’s set that name as an
environment variable.
CH_NAME=mychannel
Orderer Group¶
Like the ordering system channel, our application channel also has an orderer group.
Get the channel config:
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CH_NAME --tls --cafile $ORDERER_CA
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
Change the capability level of the orderer group:
jq -s '.[0] * {"channel_group":{"groups":{"Orderer": {"values": {"Capabilities": .[1]}}}}}' config.json ./scripts/capabilities.json > modified_config.json
Create the config update:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb
Package the config update into a transaction:
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
Submit the config update transaction:
peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA
Channel Group¶
Now we need to change the capability of the channel
group of our application
channel.
As before, fetch, decode, and scope the config:
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CH_NAME --tls --cafile $ORDERER_CA
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
Create a modified config:
jq -s '.[0] * {"channel_group":{"values": {"Capabilities": .[1]}}}' config.json ./scripts/capabilities.json > modified_config.json
Create the config update:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb
Package the config update into a transaction:
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
Because we’re updating the config of the channel
group, the relevant orgs —
Org1, Org2, and the OrdererOrg — need to sign it. This task would usually
be performed by the individual org admins, but in BYFN, as we’ve said, this task
falls to us.
First, switch into Org1 and sign the update:
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
peer channel signconfigtx -f config_update_in_envelope.pb
And do the same as Org2:
CORE_PEER_LOCALMSPID="Org2MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
peer channel signconfigtx -f config_update_in_envelope.pb
And as the OrdererOrg:
CORE_PEER_LOCALMSPID="OrdererMSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/users/Admin@example.com/msp
peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA
Application Group¶
For the application group, we will need to reset the environment variables as one organization:
CORE_PEER_LOCALMSPID="Org1MSP"
CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
Now, get the latest channel config (this process should be very familiar by now):
peer channel fetch config config_block.pb -o orderer.example.com:7050 -c $CH_NAME --tls --cafile $ORDERER_CA
configtxlator proto_decode --input config_block.pb --type common.Block --output config_block.json
jq .data.data[0].payload.data.config config_block.json > config.json
Create a modified channel config:
jq -s '.[0] * {"channel_group":{"groups":{"Application": {"values": {"Capabilities": .[1]}}}}}' config.json ./scripts/capabilities.json > modified_config.json
Note what we’re changing here: Capabilities
are being added as a value
of the Application
group under channel_group
(in mychannel
).
Create a config update transaction:
configtxlator proto_encode --input config.json --type common.Config --output config.pb
configtxlator proto_encode --input modified_config.json --type common.Config --output modified_config.pb
configtxlator compute_update --channel_id $CH_NAME --original config.pb --updated modified_config.pb --output config_update.pb
Package the config update into a transaction:
configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate --output config_update.json
echo '{"payload":{"header":{"channel_header":{"channel_id":"'$CH_NAME'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json
configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope --output config_update_in_envelope.pb
Org1 signs the transaction:
peer channel signconfigtx -f config_update_in_envelope.pb
Set the environment variables as Org2:
export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=peer0.org2.example.com:7051
Org2 submits the config update transaction with its signature:
peer channel update -f config_update_in_envelope.pb -c $CH_NAME -o orderer.example.com:7050 --tls true --cafile $ORDERER_CA
Congratulations! You have now enabled capabilities on all of your channels.
Verify a transaction after Capabilities have been Enabled¶
But let’s test just to make sure by moving 10
from a
to b
, as before:
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n mycc -c '{"Args":["invoke","a","b","10"]}'
And then querying the value of a
, which should reveal a value of 70
.
Let’s see:
peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'
We should see the following:
Query Result: 70
In which case we have successfully added capabilities to all of our channels.
Upgrading components BYFN does not support¶
Although this is the end of our update tutorial, there are other components that exist in production networks that are not covered in this tutorial. In this section, we’ll talk through the process of updating them.
Fabric CA container¶
To learn how to upgrade your Fabric CA server, click over to the CA documentation.
Upgrade Node SDK clients¶
注解
Upgrade Fabric and Fabric CA before upgrading Node SDK clients. Fabric and Fabric CA are tested for backwards compatibility with older SDK clients. While newer SDK clients often work with older Fabric and Fabric CA releases, they may expose features that are not yet available in the older Fabric and Fabric CA releases, and are not tested for full compatibility.
Use NPM to upgrade any Node.js
client by executing these commands in the
root directory of your application:
npm install fabric-client@latest
npm install fabric-ca-client@latest
These commands install the new version of both the Fabric client and Fabric-CA
client and write the new versions package.json
.
升级 Kafka 集群¶
注解
If you intend to migrate from a Kafka-based ordering service to a Raft-based ordering service, check out 从 Kafka 迁移到 Raft.
这并不是必须的,但是建议将 Kafka 集群升级并和其他 Fabric 保持一致。新版本的 Kafka 支持旧版本的协议,所以你可以在升级完其他 Fabric 之前或之后再升级 Kafka。
如果你学习了 Upgrading Your Network to v1.3 tutorial , 你的 Kafka 集群应该是 v1.0.0 。如果不是,参考官方 Apache Kafka 文档 `upgrading Kafka from previous versions`__ 来升级 Kafka 集群的 brokers 。
升级 Zookeeper¶
一个 Apache Kafka 集群需要一个 Apache Zookeeper 集群。Zookeeper API 在很长一段时 间内都很稳定,并且 Kafka 几乎兼容所有版本的 Zookeeper 。参考 `Apache Kafka upgrade`__ 文档中升级 Zookeeper 到指定版本的依赖。如果你想升级你的 Zookeeper 集群,可以在 `Zookeeper FAQ`__ 上找到升级 Zookeeper 集群的一些信息。
升级 CouchDB¶
如果你使用 CouchDB 作为状态数据库,你需要在升级节点的同时升级节点的 CouchDB 。 CouchDB v2.2.0 在 Fabric v1.4.x 中已经被测试过了。
升级 CouchDB :
- 停止 CouchDB 。
- 备份 CouchDB 数据目录。
- 安装 CouchDB v2.2.0 二进制或者更新部署脚本来使用新的 Docker 镜像 (Fabric v1.4 中提供 了预配置 CouchDB v2.2.0 的 Docker 镜像)。
- 重启 CouchDB 。
升级 Node 链码 shim¶
为了更新到新版本的 Node 链码 shim ,开发者需要:
- 在链码的
package.json
中将fabric-shim
级别从 1.3 改为 1.4.x 。 - 重新打包新的链码包,并在通道中所有的背书节点安装。
- 执行升级链码。如何升级链码,请参考 节点链码 。
注解
这个流程并不针对从 1.3 升级到 1.4.x 。它同样适用与将 node Fabric shim 升 级到任何新增版本。
使用 vendored shim 升级链码¶
注解
v1.4 节点兼容 v1.3.0 shim ,但是,最好将链码 shim 升级到匹配当前级别的 节点的版本。
有很多第三方工具可以让你 vendor 链码的 shim 。如果你使用了这些工具,就在更新和重打包 链码的时候使用同一个工具。
如果你的链码在升级 shim 之后引用了 shim,你必须在所有已经有了链码的节点上安装它。使用同 样的名字和新的版本安装。然后你要在每一个部署了这个链码的通道上执行链码升级,才可以升级到 新版本。
如果你没有 vendor 你的链码,你完全可以跳过这一步。
在 Fabric 中使用私有数据¶
本教程将演示收集器(collection)的使用,收集器为区块链网络上已授权的组织节点 提供私有数据的存储和检索。
本教程假设您已了解私有数据的存储和他们的用例。更多的信息请参阅 私有数据 。
本教程将带你通过以下步骤练习在 Fabric 中定义、配置和使用私有数据:
- 创建一个收集器的 JSON 定义文件
- 使用链码 API 读写私有数据
- 安装和初始化带有收集器的链码
- 存储私有数据
- 使用一个授权节点查询私有数据
- 以授权节点的身份查询私有数据
- 清除私有数据
- 使用私有数据索引
- 其他资源
本教程将使用 弹珠私有数据示例(marbles private data sample) — 运行在“构建你的第一个网络(BYFN)”教程的网络上 — 来演示创建、部署和使用私有数 据收集器。弹珠私有数据示例将部署在 构建你的第一个网络 (BYFN)教程的网络上。你 需要先完成 安装示例、二进制文件和 Docker 镜像 任务;但是在本教程中不需要运行 BYFN 教程。除了本教程中提 供的使用网络所必须的命令,我们还会讲解每一步都发生了什么,让你不运行示例也可以理解 每一步的意义。
创建一个收集器的 JSON 定义文件¶
在通道中数据私有化的第一步是创建一个定义了私有数据权限的收集器。
收集器定义描述了谁可以持有数据、数据要分发到多少个节点上、多少节点可以传播私有数据
和私有数据要在私有数据库中存放多久。然后,我们将演示链码 API PutPrivateData
和
GetPrivateData
是如何将收集器映射到受保护的私有数据的。
收集器的定义包括一下属性:
name
: 收集器的名字。policy
: 定义了可以持有数据收集器的组织节点。requiredPeerCount
: 作为链码的背书条件,需要将私有数据传播到的节点数量。maxPeerCount
: 为了数据冗余,现有背书节点需要尝试将数据分发到其他节点的数量。如 果背书节点发生故障,当有请求提取私有数据时,则其他节点在提交时可用。blockToLive
: 对于非常敏感的信息,比如价格或者个人信息,这个值表示在数据要以区块 的形式在私有数据库中存放的时间。数据将在私有数据库中存在指定数量的区块数然后会被清除, 也就是数据会从网络中废弃。要永久保存私有数据,永远不被清除,就设置blockToLive
为0
。memberOnlyRead
: 值为true
则表示节点会自动强制只有属于收集器成员组织的客户端才 有读取私有数据的权限。
为了说明私有数据的用法,弹珠私有数据示例包含了两个私有数据收集器的定义: collectionMarbles
和 collectionMarblePrivateDetails
。在 collectionMarbles
中的 policy
属性
定义了允许通道中(Org1 和 Org2)所有成员使用私有数据库中的私有数据。 collectionMarblePrivateDetails
收集器只允许 Org1 的成员使用私有数据库中的私有数据。
创建策略定义的更多信息请参考 背书策略 主题。
// collections_config.json
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":1000000,
"memberOnlyRead": true
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":3,
"memberOnlyRead": true
}
]
被这些策略保护的数据会被映射到链码,教程的后边会进行介绍。
当和它关联的链码在通道上参照 节点链码初始化命令(peer chaincode instantiate command) 初始化以后,这个收集器定义文件会被部署到通道上。更多的细节会在下边的三个部分讲解。
使用链码 API 读写私有数据¶
理解如何在通道上私有化数据的下一步工作是构建链码的数据定义。弹珠私有数据示例根据数 据的使用权限将私有数据分成了两个部分。
// Peers in Org1 and Org2 will have this private data in a side database
type marble struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
// Only peers in Org1 will have this private data in a side database
type marblePrivateDetails struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Price int `json:"price"`
}
私有数据的特定权限将会被限制为如下:
name, color, size, and owner
通道中所有成员可见(Org1 and Org2)price
只有 Org1 的成员可见
在弹珠私有数据示例中定义了两个不同的私有数据收集器。数据映射到收集器策略(权
限限制)是通过链码 API 控制的。特别地,使用收集器定义进行读和写私有数据是通过调用
GetPrivateData()
和 PutPrivateData()
来实现的,你可以在
这里
找到。
下边的图片阐明了弹珠私有数据示例所使用的私有数据模型。
读取收集器数据¶
使用链码 API GetPrivateData()
来查询数据库中的私有数据。 GetPrivateData()
需要两个参数, 收集器名 和数据的键值。再说一下收集器 collectionMarbles
允许
Org1 和 Org2 的成员使用侧数据库中的私有数据,收集器 collectionMarblePrivateDetails
只允许 Org1 的成员使用侧数据库中的私有数据。详情请参阅下边的两个
弹珠私有数据函数(marbles private data functions) :
- readMarble 用于查询
name, color, size and owner
属性的值- readMarblePrivateDetails 用于查询
price
属性的值
本教程后边在节点上执行数据库查询的命令时,我们就是调用这两个函数。
写入私有数据¶
使用链码 API PutPrivateData()
将私有数据存入私有数据库。这个 API 同样需要收集器的
名字。因为弹珠私有数据示例包含两个不同的收集器,它在链码中会被调用两次:
- 使用名为
collectionMarbles
的收集器写入私有数据name, color, size and owner
。 - 使用名为
collectionMarblePrivateDetails
的收集器写入私有数据price
。
例如,在下边的 initMarble
函数片段中, PutPrivateData()
被调用了两次,
每个私有数据集合各一次。
// ==== Create marble object, marshal to JSON, and save to state ====
marble := &marble{
ObjectType: "marble",
Name: marbleInput.Name,
Color: marbleInput.Color,
Size: marbleInput.Size,
Owner: marbleInput.Owner,
}
marbleJSONasBytes, err := json.Marshal(marble)
if err != nil {
return shim.Error(err.Error())
}
// === Save marble to state ===
err = stub.PutPrivateData("collectionMarbles", marbleInput.Name, marbleJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
// ==== Create marble private details object with price, marshal to JSON, and save to state ====
marblePrivateDetails := &marblePrivateDetails{
ObjectType: "marblePrivateDetails",
Name: marbleInput.Name,
Price: marbleInput.Price,
}
marblePrivateDetailsBytes, err := json.Marshal(marblePrivateDetails)
if err != nil {
return shim.Error(err.Error())
}
err = stub.PutPrivateData("collectionMarblePrivateDetails", marbleInput.Name, marblePrivateDetailsBytes)
if err != nil {
return shim.Error(err.Error())
}
总结一下,上边我们为 collection.json
定义的策略允许 Org1 和 Org2 的所有
节点在他们的私有数据库中存储和交易弹珠的私有数据 name, color, size, owner
。
但是只有 Org1 的节点可以在他的私有数据库中存储和交易 price
私有数据。
数据私有的一个额外的好处是,当使用了收集器以后,只有私有数据的哈希会通过排序节点, 而不是私有数据本身,从排序方面保证了私有数据的机密性。
启动网络¶
现在我们准备通过一些命令来演示使用私有数据。
Try it yourself
在安装和初始化弹珠私有数据链码之前,我们需要启动 BYFN 网络。为了本教程,我们需要 在一个已知的初始化环境下操作。下边的命令会关闭所有活动状态的或者存在的 docker 容 器并删除之前生成的构件。让我们运行下边的命令来清理之前的环境:
cd fabric-samples/first-network ./byfn.sh down如果你之前运行过本教程,你需要删除弹珠私有数据链码的 docker 容器。让我们运行下边 的命令清理之前的环境:
docker rm -f $(docker ps -a | awk '($2 ~ /dev-peer.*.marblesp.*/) {print $1}') docker rmi -f $(docker images | awk '($1 ~ /dev-peer.*.marblesp.*/) {print $3}')运行下边的命令来启动使用了 CouchDB 的 BYFN 网络:
./byfn.sh up -c mychannel -s couchdb这会创建一个简单的 Fabric 网络,包含一个名为
mychannel
的通道,其中有两个组织 (每个组织有两个 peer 节点)和一个排序服务,同时使用 CouchDB 作为状态数据库。LevelDB 或者 CouchDB 都可以使用收集器。这里使用 CouchDB 来演示如何对私有数据进行索引。注解
为了让收集器能够工作,正确配置跨组织的 gossip 是很重要的。参考文档 Gossip 数据传播协议 , 重点关注 “锚节点” 部分。我们的教程不关注 gossip ,它已经在 BYFN 示例中配置过了, 但是当配置通道的时候,gossip 锚节点的配置对于收集器的正常工作是很重要的。
安装和初始化带有收集器的链码¶
客户端应用通过链码和区块链账本交互。所以我们需要在每一个要执行和背书交易的节点 上安装和初始化链码。链码安装在节点上然后在通道上使用 peer-commands 进行初始化。
在所有节点上安装链码¶
就像上边讨论的,BYFN 网络包含两个组织, Org1 和 Org2 ,每个组织有两个节点。所以 链码需要安装在四个节点上:
- peer0.org1.example.com
- peer1.org1.example.com
- peer0.org2.example.com
- peer1.org2.example.com
使用 peer chaincode install 命令在每一个节点上安装弹珠链码。
Try it yourself
如果你已经启动了 BYFN 网络,进入 CLI 容器。
docker exec -it cli bash你的终端会变成类似这样的:
root@81eac8493633:/opt/gopath/src/github.com/hyperledger/fabric/peer#
使用下边的命令在 BYFN 网络上,安装 git 仓库的弹珠链码到节点
peer0.org1.example.com
(默认情况下,启动 BYFN 网络以后,激活的节点被设置成了CORE_PEER_ADDRESS=peer0.org1.example.com:7051
):peer chaincode install -n marblesp -v 1.0 -p github.com/chaincode/marbles02_private/go/当完成之后,你会看到类似输出:
install -> INFO 003 Installed remotely response:<status:200 payload:"OK" >利用 CLI 切换当前节点为 Org1 的第二个节点并安装链码。复制和粘贴下边的命令 到 CLI 容器并运行他们。
export CORE_PEER_ADDRESS=peer1.org1.example.com:8051 peer chaincode install -n marblesp -v 1.0 -p github.com/chaincode/marbles02_private/go/利用 CLI 切换到 Org2 。复制和粘贴下边的一组命令到节点容器并执行。
export CORE_PEER_LOCALMSPID=Org2MSP export PEER0_ORG2_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp切换当前节点为 Org2 的第一个节点并安装链码:
export CORE_PEER_ADDRESS=peer0.org2.example.com:9051 peer chaincode install -n marblesp -v 1.0 -p github.com/chaincode/marbles02_private/go/切换当前节点为 Org2 的第二个节点并安装链码:
export CORE_PEER_ADDRESS=peer1.org2.example.com:10051 peer chaincode install -n marblesp -v 1.0 -p github.com/chaincode/marbles02_private/go/
在通道上初始化链码¶
使用 peer chaincode instantiate
命令在通道上初始化弹珠链码。为了在通道上配置链码收集器,使用 --collections-config
标识来指定收集器的 JSON 文件,我们的示例中是 collections_config.json
。
Try it yourself
在 BYFN 的
mychannel
通道上运行下边的命令来初始化弹珠私有数据链码。export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile $ORDERER_CA -C mychannel -n marblesp -v 1.0 -c '{"Args":["init"]}' -P "OR('Org1MSP.member','Org2MSP.member')" --collections-config $GOPATH/src/github.com/chaincode/marbles02_private/collections_config.json注解
当指定了
--collections-config
的时候,你需要指明 collections_config.json 文件完整清晰的路径。 例如:--collections-config $GOPATH/src/github.com/chaincode/marbles02_private/collections_config.json
当成功初始化完成的时候,你可能看到类似下边这些:
[chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
存储私有数据¶
以 Org1 成员的身份操作,Org1 的成员被授权可以交易弹珠私有数据示例中的所有私有数据,切换 回 Org1 的节点并提交一个增加一个弹珠的请求:
Try it yourself
复制并粘贴下边的一组命令到 CLI 命令行。
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051 export CORE_PEER_LOCALMSPID=Org1MSP export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export PEER0_ORG1_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt调用
initMarble
函数来创建一个带有私有数据的弹珠 — 名字为marble1
, 拥有者为tom
,颜色为blue
,尺寸为35
,价格为99
。重申一下,私 有数据 price 将会和私有数据 name, owner, color, size 分开存储。因为这个原 因,initMarble
函数存储私有数据的时候调用两次PutPrivateData()
API ,每个 收集器一次。同样要注意到,私有数据传输的时候使用了--transient
标识。为了保证 数据的隐私性,作为临时数据传递的输入不会保存在交易中。临时数据以二进制的方式传输, 但是当使用 CLI 的时候,必须先进行 base64 编码。我们使用一个环境变量来获得 base64 编码的值。 .. code:: bashexport MARBLE=$(echo -n “{“name”:”marble1”,”color”:”blue”,”size”:35,”owner”:”tom”,”price”:99}” | base64 | tr -d \n) peer chaincode invoke -o orderer.example.com:7050 –tls –cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c ‘{“Args”:[“initMarble”]}’ –transient “{“marble”:”$MARBLE”}”你应该会看到类似下边的结果:
[chaincodeCmd] chaincodeInvokeOrQuery->INFO 001 Chaincode invoke successful. result: status:200
使用一个授权节点查询私有数据¶
我们收集器的定义允许 Org1 和 Org2 的所有成员在他们的侧数据库中使用 name, color,
size, owner
私有数据,但是只有 Org1 的节点可以在他们的侧数据库中保存 price
私有数据。作为一个 Org1 中的授权节点,我们将查询两个私有数据集合。
第一个 query
命令调用传递了 collectionMarbles
作为参数的 readMarble
函数。
// ===============================================
// readMarble - read a marble from chaincode state
// ===============================================
func (t *SimpleChaincode) readMarble(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
}
name = args[0]
valAsbytes, err := stub.GetPrivateData("collectionMarbles", name) //get the marble from chaincode state
if err != nil {
jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"Marble does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}
return shim.Success(valAsbytes)
}
第二个 query
命令调用传递了 collectionMarblePrivateDetails
作为参数
的 readMarblePrivateDetails
函数。
// ===============================================
// readMarblePrivateDetails - read a marble private details from chaincode state
// ===============================================
func (t *SimpleChaincode) readMarblePrivateDetails(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var name, jsonResp string
var err error
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the marble to query")
}
name = args[0]
valAsbytes, err := stub.GetPrivateData("collectionMarblePrivateDetails", name) //get the marble private details from chaincode state
if err != nil {
jsonResp = "{\"Error\":\"Failed to get private details for " + name + ": " + err.Error() + "\"}"
return shim.Error(jsonResp)
} else if valAsbytes == nil {
jsonResp = "{\"Error\":\"Marble private details does not exist: " + name + "\"}"
return shim.Error(jsonResp)
}
return shim.Success(valAsbytes)
}
Now Try it yourself
以 Org1 成员的身份查询
marble1
的私有数据name, color, size and owner
。 注意,由于查询动作不记录在账本上,所以没必要将弹珠名作为临时输入传递。peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarble","marble1"]}'你应该会看到如下结果:
{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}以 Org1 成员的身份查询
marble1
的私有数据price
。peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'你应该会看到如下结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
以授权节点的身份查询私有数据¶
现在我们将切换到 Org2 成员,在它的侧数据库中有弹珠私有数据的 name, color,
size, owner
,但是没有私有数据 price
。我们将查询两个私有数据集合。
切换到 Org2 的节点¶
在 docker 容器内,运行下边的命令切换到有权限访问弹珠私有数据 price
的节点。
Try it yourself
export CORE_PEER_ADDRESS=peer0.org2.example.com:9051 export CORE_PEER_LOCALMSPID=Org2MSP export PEER0_ORG2_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt export CORE_PEER_TLS_ROOTCERT_FILE=$PEER0_ORG2_CA export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
查询 Org2 有权访问的私有数据¶
Org2 的节点在它们的数据库中有弹珠私有数据的第一个集合 ( name, color, size and owner
)
并且有权限使用 readMarble()
函数和 collectionMarbles
参数访问它。
Try it yourself
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarble","marble1"]}'你应该会看到类似下边的输出结果:
{"docType":"marble","name":"marble1","color":"blue","size":35,"owner":"tom"}
查询 Org2 没有权限的私有数据¶
在 Org2 的节点侧数据库中没有弹珠的私有数据 price
。当它们尝试查询这个数据的时候,
它们会得到符合公共状态键的哈希但是得不到私有数据。
Try it yourself
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'你应该会看到如下结果:
{"Error":"Failed to get private details for marble1: GET_STATE failed: transaction ID: b04adebbf165ddc90b4ab897171e1daa7d360079ac18e65fa15d84ddfebfae90: Private data matching public hash version is not available. Public hash version = &version.Height{BlockNum:0x6, TxNum:0x0}, Private data version = (*version.Height)(nil)"}
Org2 的成员只能看到私有数据的公共哈希。
清除私有数据¶
对于一些案例,私有数据仅需在账本上保存到在链下数据库复制之后就可以了,我们可以将 数据在过了一定数量的区块后进行 “清除”,仅仅把数据的哈希作为不可篡改的证据保存下来。
私有数据可能会包含私人的或者机密的信息,比如我们例子中的价格数据,这是交易伙伴不想 让通道中的其他组织知道的。但是,它具有有限的生命周期,就可以根据收集器定义中的,在 固定的区块数量之后清除。
我们的 collectionMarblePrivateDetails
中定义 blockToLive
属性的值为 3 ,
表明这个数据会在侧数据库中保存三个区块的时间,之后它就会被清除。将所有内容放在一
起,回想一下绑定了私有数据 price
的收集器 collectionMarblePrivateDetails
,
在函数 initMarble()
中,当调用 PutPrivateData()
API 并传递了参数
collectionMarblePrivateDetails
。
我们将从在链上增加区块,然后来通过执行四笔新交易(创建一个新弹珠,然后转移三个 弹珠)看一看价格信息被清除的过程,增加新交易的过程中会在链上增加四个新区块。在 第四笔交易完成之后(第三个弹珠转移后),我们将验证一下价格数据是否被清除了。
Try it yourself
使用如下命令切换到 Org1 的 peer0 。复制和粘贴下边的一组命令到节点容器并执行:
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051 export CORE_PEER_LOCALMSPID=Org1MSP export CORE_PEER_TLS_ROOTCERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp export PEER0_ORG1_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt打开一个新终端窗口,通过运行如下命令来查看这个节点上私有数据日志:
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'你将看到类似下边的信息。注意列表中最高的区块号。在下边的例子中,最高的区块高度是
4
。[pvtdatastorage] func1 -> INFO 023 Purger started: Purging expired private data till block number [0] [pvtdatastorage] func1 -> INFO 024 Purger finished [kvledger] CommitWithPvtData -> INFO 022 Channel [mychannel]: Committed block [0] with 1 transaction(s) [kvledger] CommitWithPvtData -> INFO 02e Channel [mychannel]: Committed block [1] with 1 transaction(s) [kvledger] CommitWithPvtData -> INFO 030 Channel [mychannel]: Committed block [2] with 1 transaction(s) [kvledger] CommitWithPvtData -> INFO 036 Channel [mychannel]: Committed block [3] with 1 transaction(s) [kvledger] CommitWithPvtData -> INFO 03e Channel [mychannel]: Committed block [4] with 1 transaction(s)你将看到类似下边的信息。注意列表中最高的区块号。在下边的例子中,最高的区块高度是
4
。peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'你将看到类似下边的信息:
{"docType":"marblePrivateDetails","name":"marble1","price":99}
price
数据仍然在私有数据账本上。通过执行如下命令创建一个新的 marble2 。这个交易将在链上创建一个新区块。
export MARBLE=$(echo -n "{\"name\":\"marble2\",\"color\":\"blue\",\"size\":35,\"owner\":\"tom\",\"price\":99}" | base64 | tr -d \\n) peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["initMarble"]}' --transient "{\"marble\":\"$MARBLE\"}"再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 。
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'返回到节点容器,再次运行如下命令查询 marble1 的价格数据:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'私有数据没有被清除,之前的查询也没有改变查询结果:
{"docType":"marblePrivateDetails","name":"marble1","price":99}运行下边的命令将 marble2 转移给 “joe” 。这个交易将使链上增加第二个区块。
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"joe\"}" | base64 | tr -d \\n) peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["transferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 。
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'返回到节点容器,再次运行如下命令查询 marble1 的价格数据:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'你将看到价格私有数据。
{"docType":"marblePrivateDetails","name":"marble1","price":99}运行下边的命令将 marble2 转移给 “tom” 。这个交易将使链上增加第三个区块。
export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"tom\"}" | base64 | tr -d \\n) peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["transferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 。
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'返回到节点容器,再次运行如下命令查询 marble1 的价格数据:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'你将看到价格数据。
{"docType":"marblePrivateDetails","name":"marble1","price":99}最后,运行下边的命令将 marble2 转移给 “jerry” 。这个交易将使链上增加第四个区块。在 此次交易之后,
price
私有数据将会被清除。export MARBLE_OWNER=$(echo -n "{\"name\":\"marble2\",\"owner\":\"jerry\"}" | base64 | tr -d \\n) peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C mychannel -n marblesp -c '{"Args":["transferMarble"]}' --transient "{\"marble_owner\":\"$MARBLE_OWNER\"}"再次切换回终端窗口并查看节点的私有数据日志。你将看到区块高度增加了 1 。
docker logs peer0.org1.example.com 2>&1 | grep -i -a -E 'private|pvt|privdata'返回到节点容器,再次运行如下命令查询 marble1 的价格数据:
peer chaincode query -C mychannel -n marblesp -c '{"Args":["readMarblePrivateDetails","marble1"]}'因为价格数据已经被清除了,你就查询不到了。你应该会看到类似下边的结果:
Error: endorsement failure during query. response: status:500 message:"{\"Error\":\"Marble private details does not exist: marble1\"}"
链码教程¶
链码开发者教程¶
链码是什么?¶
链码是一个程序,由 Go 、 node.js 、或者 Java 编写,来实现一些预定义的接口。链码运行在一个和背书节点进 程隔离的一个安全的 Docker 容器中。链码的初始化和账本状态的管理通过应用提交的交易来实现。
链码一般处理网络中的成员一致认可的商业逻辑,所以它类似于“智能合约”。链码在交易提案中被调 用来升级或者查询账本。赋予适当的权限,链码就可以调用其他链码来访问它的状态,不管是在同一 个通道还是不同的通道。注意,如果被调用的链码和当前链码在不同的通道,就只能执行只读的查询。 就是说,调用不同通道的链码只能进行“查询”,在提交的子语句中不能参与状态的合法性检查。
在下边的章节中,我们站在应用开发者的角度来介绍链码。我们将演示一个简单的链码应用,并且逐个 查看链码 Shim API 中每一个方法的作用。
链码 API¶
Every chaincode program must implement the Chaincode
interface whose methods
are called in response to received transactions. You can find the reference
documentation of the Chaincode Shim API for different languages below:
In each language, the Invoke
method is called by clients to submit transaction
proposals. This method allows you to use the chaincode to read and write data on
the channel ledger.
You also need to include an Init
method that will serve as the initialization
function for your chaincode. This method will be called in order to initialize
the chaincode when it is started or upgraded. By default, this function is never
executed. However, you can use the chaincode definition to request that the Init
function be executed. If execution of Init
is requested, fabric will ensure
that Init
is invoked before any other function and is only invoked once.
This option provides you additional control over which users can initialize the
chaincode and the ability to add initial data to the ledger. If you are using
the peer CLI to approve the chaincode definition, use the --init-required
flag to request the execution of the Init
function. Then call the Init
function by using the peer chaincode invoke command and passing the
--isInit
flag. If you are using the Fabric SDK for Node.js, visit
How to install and start your chaincode. For more information, see 链码操作者教程.
链码 “shim” API 中的其他接口是 ChaincodeStubInterface
:
用来访问和修改账本,并且可以调用链码。
在本教程中使用 Go 链码,我们将通过实现一个管理简单“资产”的示例链码应用来演示如何使用这些 API 。
简单资产链码¶
我们的应用程序是一个基本的示例链码,用来在账本上创建资产(键-值对)。
选择一个位置存放代码¶
如果你没有写过 Go 的程序,你可能需要确认一下你是否安装了 Go 语言 并且你的系统上的配 置是否合适。
现在你需要在 $GOPATH/src/
子目录为你的链码应用程序创建一个目录。
简单起见,我们使用如下命令:
mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc
现在,我们创建一个用于编写代码的源文件:
touch sacc.go
家务¶
首先,我们先做一些家务。每一个链码都要实现 Chaincode interface
中的 Init
和 Invoke
方法。所以,我们先使用 Go import 语句来导入链码必要的依赖。
我们将导入链码 shim 包和 peer protobuf package 。
然后,我们加入一个 SimpleAsset
结构体来作为链码 shim 方法的接受者。
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}
初始化链码¶
然后,我们将实现 Init
方法。
// Init is called during chaincode instantiation to initialize any data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
}
注解
注意,链码升级的时候也要条用这个方法。当写用来升级一个已存在的链码的时候,
请确保合理更改 Init
方法。特别地,当没有“迁移”或者初始化不是升级的一部
分时,可以提供一个空的 Init
方法。
然后,我们将使用 ChaincodeStubInterface.GetStringArgs
方法取回调用 Init
的参数,并且检查合法性。在我们的用例中,我们希望得到一个键-值对。
// Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data, so be careful to avoid a scenario where you // inadvertently clobber your ledger's data! func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // Get the args from the transaction proposal args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } }
然后,我们已经确定了调用是合法的,我们将把初始状态存入账本中。我们将调用 ChaincodeStubInterface.PutState 并将键和值作为参数传递给它。假设一切正常,将返回一个 peer.Response 对象,表明初始化成功。
// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data, so be careful to avoid a scenario where you
// inadvertently clobber your ledger's data!
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
// Get the args from the transaction proposal
args := stub.GetStringArgs()
if len(args) != 2 {
return shim.Error("Incorrect arguments. Expecting a key and a value")
}
// Set up any variables or assets here by calling stub.PutState()
// We store the key and the value on the ledger
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
}
return shim.Success(nil)
}
调用链码¶
首先,我们增加一个 Invoke
函数的签名。
// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The 'set'
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
}
就像上边的 Init
函数一样,我们需要从 ChaincodeStubInterface
中解析参数。
Invoke
函数的参数是将要调用的链码应用程序的函数名。在我们的用例中,我们的应
用程序将有两个方法: set
和 get
,用来设置或者获取资产当前的状态。我们先调用
ChaincodeStubInterface.GetFunctionAndParameters
来解析链码应用程序方法的方法名和参数。
// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// Extract the function and args from the transaction proposal
fn, args := stub.GetFunctionAndParameters()
}
然后,我们将验证函数名是否为 set
或者 get
,并执行链码应用程序的方法,通过
shim.Success
或 shim.Error
返回一个适当的响应,这个响应将被序列化为
gRPC protobuf 消息。
// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// Extract the function and args from the transaction proposal
fn, args := stub.GetFunctionAndParameters()
var result string
var err error
if fn == "set" {
result, err = set(stub, args)
} else {
result, err = get(stub, args)
}
if err != nil {
return shim.Error(err.Error())
}
// Return the result as success payload
return shim.Success([]byte(result))
}
实现链码应用程序¶
就像我们说的,我们的链码应用程序实现了两个功能,它们可以通过 Invoke
方
法调用。我们现在来实现这写方法。注意我们之前提到的,要访问账本状态,我们需要使用
链码 shim API 中的
ChaincodeStubInterface.PutState
和
ChaincodeStubInterface.GetState
方法。
// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 2 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
}
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return "", fmt.Errorf("Failed to set asset: %s", args[0])
}
return args[1], nil
}
// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key")
}
value, err := stub.GetState(args[0])
if err != nil {
return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
}
if value == nil {
return "", fmt.Errorf("Asset not found: %s", args[0])
}
return string(value), nil
}
把它们组合在一起¶
最后,我们增加一个 main
方法,它将被
shim.Start
函数调用。下边是我们链码程序的完整源码。
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
// SimpleAsset implements a simple chaincode to manage an asset
type SimpleAsset struct {
}
// Init is called during chaincode instantiation to initialize any
// data. Note that chaincode upgrade also calls this function to reset
// or to migrate data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
// Get the args from the transaction proposal
args := stub.GetStringArgs()
if len(args) != 2 {
return shim.Error("Incorrect arguments. Expecting a key and a value")
}
// Set up any variables or assets here by calling stub.PutState()
// We store the key and the value on the ledger
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
}
return shim.Success(nil)
}
// Invoke is called per transaction on the chaincode. Each transaction is
// either a 'get' or a 'set' on the asset created by Init function. The Set
// method may create a new asset by specifying a new key-value pair.
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// Extract the function and args from the transaction proposal
fn, args := stub.GetFunctionAndParameters()
var result string
var err error
if fn == "set" {
result, err = set(stub, args)
} else { // assume 'get' even if fn is nil
result, err = get(stub, args)
}
if err != nil {
return shim.Error(err.Error())
}
// Return the result as success payload
return shim.Success([]byte(result))
}
// Set stores the asset (both key and value) on the ledger. If the key exists,
// it will override the value with the new one
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 2 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
}
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return "", fmt.Errorf("Failed to set asset: %s", args[0])
}
return args[1], nil
}
// Get returns the value of the specified asset key
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key")
}
value, err := stub.GetState(args[0])
if err != nil {
return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
}
if value == nil {
return "", fmt.Errorf("Asset not found: %s", args[0])
}
return string(value), nil
}
// main function starts up the chaincode in the container during instantiate
func main() {
if err := shim.Start(new(SimpleAsset)); err != nil {
fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
}
}
编译链码¶
现在我们编译你的链码。
go get -u github.com/hyperledger/fabric/core/chaincode/shim
go build
假设没有错误,现在你可以进行下一步操作,测试你的链码。
使用开发模式测试¶
一般链码是通过节点执行和维护的。然而在“开发模式”下,链码通过用户编译和执 行。这个模式在链码“编码/编译/运行/调试”的开发生命周期中很有用。
我们通过一个示例开发网络预先生成的排序和通道构件来启动“开发模式”。这样用户 就可以快速的进入编译链码和调用的过程。
装 Hyperledger Fabric 示例
如果你还没有完成这些,请参考 安装示例、二进制文件和 Docker 镜像 。
克隆如下命令导航至 fabric-samples
目录下的 chaincode-docker-devmode
:
cd chaincode-docker-devmode
现在打开三个终端,并且每个终端都导航至 chaincode-docker-devmode
目录。
终端1 - 启动网络¶
docker-compose -f docker-compose-simple.yaml up
上边的命令启动了一个网络,网络的排序模式为 SingleSampleMSPSolo
,并且以“开发模式”
启动了 peer 节点。它还启动了另外两个容器 - 一个是链码环境,另一个是和链码交互的 CLI。
创建和加入通道的命令在 CLI 容器中,所以我们直接跳入了链码调用。
终端2 - 编译并启动链码
docker exec -it chaincode bash
你应该看到如下内容:
root@d2629980e76b:/opt/gopath/src/chaincode#
现在,编译你的链码:
cd sacc
go build
现在运行链码:
CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./sacc
链码从 peer 节点启动并且日志表示链码成功注册到了 peer 节点上。注意,在这个阶段链码
没有关联任何通道。这个过程通过 instantiate
命令的之后的步骤完成。
终端3 - 使用链码¶
即使你在 --peer-chaincodedev
模式下,你仍然需要安装链码,这样链码才能正常地通生
命周期系统链码的检查。这个需求能会在未来的版本中移除。
我们将进入 CLI 容器来执行这些调用。
docker exec -it cli bash
peer chaincode install -p chaincodedev/chaincode/sacc -n mycc -v 0
peer chaincode instantiate -n mycc -v 0 -c '{"Args":["a","10"]}' -C myc
现在执行一个调用来将 “a” 的值改为 20 。
peer chaincode invoke -n mycc -c '{"Args":["set", "a", "20"]}' -C myc
最后,查询 a
。我们将看到一个为 20
的值。
peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc
测试新链码¶
默认地,我们只挂载 sacc
。然而,你可以很容易地通过将他们加入 chaincode
子目录
并重启你的网络来测试不同的链码。这时,它们在你的 chaincode
容器中是可访问的。
链码访问控制¶
链码可以通过调用 getCreator() 函数来使用客户端(提交者)证书进行访问控制决策。另外, Go shim 提供了扩展 API ,用于从提交者的证书中提取客户端标识,该证书可用于访问控制决 策,无论是基于客户端标识本身,还是基于组织标识,还是基于客户端标识属性。
例如,一个以键或值对表示的资产可以将客户端的身份作为值的一部分保存其中(比如作为代表资产主人 的 JSON 属性),以后就只有被授权的客户端才可以更新键或值。
更多详情请查阅 client identity (CID) library documentation
To add the client identity shim extension to your chaincode as a dependency, see 管理 Go 链码的扩展依赖.
链码加密¶
在一些场景中,将一个键的全部或者部分值进行加密是有必要的。例如,如果把一个人的社会安全吗或者 地址写入账本中了账本中,你是不会想让这些数据显示为明文的。链码的加密功能由 entities extension 提供,它是一个 BCCSP 包装器,包含了通用的工厂和函数函来进行加密操作,比如加密和椭圆曲线数字签 名。例如,要实现加密,链码的调用者就要通过传输域传入一个密钥。同样的密钥可能也要用于子查询操 作,以允许对加密的状态数据进行解密。
更多的信息和示例请参阅 fabric/example
文件夹中的
Encc Example 。
重点关注 utils.go
帮助程序。这个工具加载了链码 shim API 和实体扩展,并构件了一个新类和其中
的方法(比如 encryptAndPutState
和 getStateAndDecrypt
),这些方法会被示例加密链码调用。
像这样,链码现在可以使用增加了 Encryp
和 Decrypt
的基础 shim API 中的 Get
和 Put
。
要把加密扩展作为一个以来增加到你的链码中,请参考 管理 Go 链码的扩展依赖 。
管理 Go 链码的扩展依赖¶
如果你的链码需要 Go 标准库之外的以来的话,你需要在你的链码中引入这些包。这也是一个很好的做法, 把 shim 和任何扩展库作为依赖加入到你的链码中。
有很多 可用工具 来管理这些依赖。
下面演示如何使用 govendor
:
govendor init
govendor add +external // Add all external package, or
govendor add github.com/external/pkg // Add specific external package
这就把扩展依赖导入了本地的 vendor
目录。如果你要引用 Fabric shim 或者 shim 的扩展,在执行
govendor 命令之前,先把 Fabric 仓库复制到 $GOPATH/src/github.com/hyperledger 目录。
当依赖都引入到你的链码目录后, peer chaincode package
和 peer chaincode install
操作将
把这些依赖一起放入链码包中。
链码操作者教程¶
什么是链码?¶
链码是一个程序,由 Go 、 node.js 、或者 Java 编写,来实现一些预定义的接口。链码运行在一个和背书节点进 程隔离的一个安全的 Docker 容器中。链码的实例化和账本状态的管理通过应用提交的交易来实现。
链码一般处理网络中的成员一致认可的商业逻辑,所以它类似于“智能合约”。链码创建的状态是被唯 一绑定在该链码上的,其他链码不能直接访问。然而,在同一个网络中,赋予适当的权限,一个链码 也可以调用其他链码来访问他的状态。
在下边的章节中,我们将以区块链操作员 Noah 的视角来解释链码。为了 Noah 的兴趣,我们将关注 链码操作的生命周期;打包、安装、实例化和升级链码的过程是区块链网络中链码操作的生命周期中的 方法。
链码生命周期¶
Hyperledger Fabric API 允许和区块链网络中的多种节点进行交互包括 peer 节点、排序节点、MSP, 它还允许在背书节点打包、安装、实例化和升级链码。 Hyperledger Fabric 特定语言的 SDK 将 Hyperledger Fabric API 的功能抽象出来以方便应用开发,同样也可以用来管理链码生命周期。另外, Hyperledger Fabric API 还可以直接通过 CLI 访问,本文档将使用这种方式。
我们提供了四个命令来管理链码的生命周期: package
、 install
、 instantiate
和
upgrade
。将来我们还打算引入 stop
和 start
交易来在不需要真正卸载的情况下取消
或者重启链码。在一个链码成功地安装和实例化之后,链码就在运行了,它可以通过 invoke
来处
理交易。链码可以在安装之的任何时间进行升级。
打包¶
链码包包含三部分:
- 链码,定义为
ChaincodeDeploymentSpec
或者 CDS 。 CDS 定义了链码包中的链码和其他属性,比如名字和版本,- 一个可选的实例化策略,它的语法和背书策略的语法一样,在 背书策略 中有描述,
- 链码拥有者的签名集合。
签名的目的如下:
- 建立链码的所有权,
- 允许包内容的验证,
- 允许验证包的篡改。
在一个通道上链码的实例化交易的创建者,会被进行链码实例化策略验证。
创建包¶
打包链码有两个建议。一个是当你想有一个多个所有者的链码时,链码包需要多个身份签名。
这个工作流需要我们初始创建一个签名的链码包(一个 SingedCDS
),然后把它发送给
其他所有者签名。
另外一个简单的工作流是,当你部署一个 SingedCDS 时,链码包仅包含要执行 安装
交易的节点个体的签名。
我们将先处理更复杂的事情。你如果目前还不关心多所有者,可以调到 安装链码 章节。
使用下边的命令创建签名链码包:
peer chaincode package -n mycc -p github.com/hyperledger/fabric/examples/chaincode/go/example02/cmd -v 0 -s -S -i "AND('OrgA.admin')" ccpack.out
-s
选项创建一个可由多个所有者签名的包,而不是简单地创建原始 CDS。如果指定了
-s
,如果其他所有者要继续签名就必须也指定 -S
选项。
-S
选项指明了包要使用 core.yaml
中的 localMspid
属性的 MSP 主体签名。
-S
选项是可选的。但是,如果一个包创建的时候没有签名,其他所有者就也不能使用
signpackage
命令签名。
选项 -i
可以让用户给链码指定一个实例化的策略。初始策略和背书策略有相同的格式,
指明那些身份可以实例化链码。在上边的例子中,只有 OrgA 的管理员可以实例化链码。如
果没有提供策略,就是用默认策略,默认策略是只允许 peer 节点的管理员实例化链码。
包签名¶
一个创建时被签名的链码包可以被其他所有者查看和签名。工作流支持链码包的带外签名。
ChaincodeDeploymentSpec 可以可选地通过一个所有者集合的签名来创建一个 SignedChaincodeDeploymentSpec (简称 SingedCDS)。SignedCDS 包含 3 个元素:
- CDS 包含源码、名字和链码版本。
- 链码的实例化策略,也就是背书策略。
- 链码的所有者列表,也就是(背书者) Endorsement.
注解
注意,当链码在一些通道上实例化时,这个背书策略在带外确定,以提供适 当的 MSP 主体。如果没有指定实例化策略,默认策略是通道中的任意 MSP 管理员。
每个所有者通过结合他的身份(比如,证书)和签名融合结果来背书 ChaincodeDeploymentSpec。
链码所有者可以使用如下命令来签名之前创建的已经签过名的包:
peer chaincode signpackage ccpack.out signedccpack.out
ccpack.out
和 signedccpack.out
分别是输入和输出的包。 signedccpack.out
额外包含本地 MSP 对包的签名。
安装链码¶
install
交易,将链码源码打包至预定义格式 ChaincodeDeploymentSpec
(简称 CDS)
并安装到将要运行链码的 peer 节点。
注解
你必须将链码安装在通道中的 每一个 背书节点上。
当 install
API 收到一个 ChaincodeDeploymentSpec
的时候,它将使用默认
的实例化策略并包含一个空的所有者列表。
注解
链码应该只安装在所有者成员的背书节点上,以保护链码逻辑的机密性。没有 安装链码的成员,不能够对链码交易进行背书;也就是说他们不能执行链码。但 是他们仍然可以验证并向账本提交交易。
要安装链码,就要向 lifecycle system chaincode
(LSCC) 发送一个
`SignedProposal<https://github.com/hyperledger/fabric/blob/master/protos/peer/proposal.proto#L104>`_
,LSCC 的介绍在 System Chaincode 章节有介绍。例如,要使用 CLI 安装 简单资产链码 章节的
sacc 示例链码,命令如下:
peer chaincode install -n asset_mgmt -v 1.0 -p sacc
CLI 会在内部为 sacc 创建一个 SignedChaincodeDeploymentSpec 并将它发送到调用 LSCC
的 Install
方法的本地 peer 节点上。 -p
参数指定了链码的路径,该路径必须在用户
的 GOPATH
目录下,比如 $GOPATH/src/sacc
。注意,如果使用 -l node
或者
-l java
来指定 node 链码或者 java 链码, -p
使用链码的绝对路径。命令的完整
描述请参考 命令参考 。
注意,要在 peer 节点上安装链码, SignedProposal 必须是 peer 节点本地 MSP 的管理员之一。
实例化¶
instantiate
交易调用 lifecycle System Chaincode
(LSCC) 在通道上创建并
实例化链码。这是链码和通道绑定的过程:一个链码可以绑定在任意数量的通道上,并且在
每一个通道上的操作都是独立的。换句话说,无论链码在多少通道上安装并实例化了,状态
都值存在与交易所提交的通道。
instantiate
交易的创建者,必须满足 SignedCDS 中链码的实例化策略,并且必须是通
道的写入者,写入者在创建通道的时候配置的。这对通道的安全性很重要,可以避免骗子节点
部署链码和避免伪装的成员执行未在通道上绑定的链码。
例如,强调默认的实例化策略是通道上任一 MSP 管理员,所以链码实例化交易的创建者必须是 通道管理员之一。当交易提案到达背书节点,它就会检查创建者的签名是否符合实例化策略。这 个过程会在提交到账本之前的交易验证过程完成。
实例化交易也设置了链码在通道上的背书策略。背书策略描述了通道上的成员接受一笔交易所需 要满足的条件。
例如,使用 CLI 来实例化 sacc 链码并使用 john
和 0
初始化状态,命令如下:
peer chaincode instantiate -n sacc -v 1.0 -c '{"Args":["john","0"]}' -P "AND ('Org1.member','Org2.member')"
注解
注意一下背书策略,需要 Org1 和 Org2 对所有发送到 sacc 都进行背书。也就是 说,只有 Org1 和 Org2 都签名的 sacc 上 Invoke 的执行结果才是有效的。
实例化成功之后,链码在通道上就进入活跃状态,并且准备处理 ENDORSER_TRANSACTION 类型的交易提案。交易会在到达背书节点的时候被处理。
升级¶
链码可以在任何时间通过改变版本号来升级,版本号是 SignedCDS 的一部分。其他部分, 比如所有者和实例化策略是可选的。但是,链码名字必须一致,否则会被认为是不同的链 码。
在升级之前,新版本的链码必须被安装在必要的背书节点上。升级是和实例化类似的交易,
它是在通道上绑定一个新版本的链码。其他绑定旧版本链码的通道依旧运行旧的版本。换
句话说, upgrade
交易每次只影响提交交易的那一个通道。
注解
注意到,同一时间可能有多个版本的链码在运行,升级程序不自动删除旧版本, 所以暂时必须由用户管理。
和 instantiate
交易相比有一点不同: upgrade
交易检查当前链码的实例化策略,
而不是新的策略(如果指定的话)。这就确保了只有在当前实例化策略下的成员才可以升级
链码。
注解
注意,在升级的过程中,会调用链码的 Init
方法来更新或者重新实例化任何
值,所以必须注意避免在链码升级的过程中重新设置状态。
停止和启动¶
注意, stop
和 start
生命周期交易还没有实现。所以,你必须通过手动删除每一
个背书节点上的链码容器和 SignedCDS 包来停止链码。也就是通过删除每一个背书节点所运
行的主机或者虚拟机上的链码容器和 SignedCDS 来完成。
注解
TODO - 为了删除 peer 节点上的 CDS,你必须先进入到 peer 节点的容器中。 我们确实需要提供一个工具脚本实现这个功能。
docker rm -f <container id>
rm /var/hyperledger/production/chaincodes/<ccname>:<ccversion>
“停止”在以受控方式进行升级的工作流中非常有用,当执行升级之前,可以停止通道上所有 节点的链码。
系统链码¶
系统链码有着相同的编程模型,除了它运行在 peer 进程而不是像普通链码一样在一个隔离 的容器中。因此,系统链码被编译进了 peer 可执行程序中而不遵守上边所描述的生命周期。 特别地, install 、 instantiate 和 upgrade 不使用与系统链码。
系统链码的目的是减少 peer 节点和链码 gRPC 通信的消耗和权衡管理的灵活性。例如,一个 系统链码仅可以通过 peer 二进制文件来升级。它必须使用在编译时的 一组固定参数 进行注册,没有背书策略或者背书策略功能。
系统链码是 Hyperledger Fabric 中用来实现系统行为的,所以他们可以被系统集成人员替换 或修改。
现有的系统链码列表:
之前的背书和验证系统链码被可插拔背书和验证方法所取代,描述文档请参阅 可插拔交易背书与交易验证 。
当修改和替换这些系统链码的时候必须十分消息,特别是 LSCC 。
系统链码插件¶
系统链码是特殊的链码,作为 peer 进程的一部分运行,而不是像用户链码一样运行在独立的
docker 容器中。所以,它们有更高的权限访问 peer 中的资源来实现用户链码难以实现或者
不能实现的功能。示例系统链码包括 QSCC (Query System Chaincode, 查询系统链码)提供
账本或者 Fabric 相关的查询, CSCC (Configuration System Chaincode, 配置系统链码)
帮助管理访问控制,和 LSCC (Lifecycle System Chaincode,生命周期系统链码)。
System chaincodes are specialized chaincodes that run as part of the peer process
as opposed to user chaincodes that run in separate docker containers. As
such they have more access to resources in the peer and can be used for
implementing features that are difficult or impossible to be implemented through
user chaincodes. Examples of System Chaincodes include QSCC (Query System Chaincode)
for ledger and other Fabric-related queries, CSCC (Configuration System Chaincode)
which helps regulate access control, _lifecycle
(which regulates the Fabric
chaincode lifecycle), and the legacy LSCC (Lifecycle System Chaincode) which
regulated the previous chaincode lifecycle.
和用户链码不同,系统链码不使用 SDK 或者 CLI 的提案安装和实例化。它在 peer 节点启动 的时候注册和部署。
系统链码可以通过两种方式链接到 peer 节点:静态连接和使用 Go 插件动态连接。本教程将讲 解如何以插件方式开发和加载系统链码。
开发插件¶
系统链码的程序使用 Go 编写,并使用 plugin 包加载。
一个插件包含一个带有导出符号的 main 包,并通过 go build -buildmode=plugin
命令编译。
每一个系统链码必须实现 Chaincode Interface
并导出一个复合 mian 包中 func New() shim.Chaincode
签名的构造方法。可以在仓库中的
examples/plugin/scc
找到一个示例。
已有的链码,比如 QSCC 也可以作为一些特征的模板,比如访问控制,它们就是通过系统链码实现的。 已有的系统链码同样可以作为日志和测试的参考。
注解
在导入包的时候:插件导入的 Go 的标准包必须和主程序(在这里就是 Fabric)导入的包 的版本一致。
配置插件¶
插件在 core.yaml
的 chaincode.systemPlugin
部分配置:
chaincode:
systemPlugins:
- enabled: true
name: mysyscc
path: /opt/lib/syscc.so
invokableExternal: true
invokableCC2CC: true
系统链码也必须在 core.yaml
中 chaincode.system
的白名单中:
chaincode:
system:
mysyscc: enable
使用 CouchDB¶
本教程将讲述在 Hyperledger Fabric 中使用 CouchDB 作为状态数据库的步骤。现在, 你应该已经熟悉 Fabric 的概念并且已经浏览了一些示例和教程。
注解
The Fabric chaincode lifecycle that is being introduced in the v2.0 Alpha release does not support using indexes with CouchDB. As a result, this tutorial requires the previous lifecycle process to install and instantiate a chaincode that includes CouchDB indexes. Download the release-1.4 version of the Fabric Samples to use this tutorial. For more information, see 将索引添加到你的链码文件夹.
本教程将带你按如下步骤与学习:
- 在 Hyperledger Fabric 中启用 CouchDB
- 创建一个索引
- 将索引添加到你的链码文件夹
- 安装和初始化链码
- 查询 CouchDB 状态数据库
- Use best practices for queries and indexes
- 在 CouchDB 状态数据库查询中使用分页
- 升级索引
- 删除索引
想要更深入的研究 CouchDB 的话,请参阅 使用 CouchDB 作为状态数据库 ,关于 Fabric 账 本的跟多信息请参阅 Ledger 主题。下边的教程将详细讲述如何在你的区 块链网络中使用 CouchDB 。
本教程将使用 Marbles sample 作为演示在 Fabric 中使用 CouchDB 的用例,并且将会把 Marbles 部署在 构建你的第一个网络 (BYFN) 教程网络上。
为什么是 CouchDB ?¶
Fabric 支持两种类型的节点数据库。LevelDB 是默认嵌入在 peer 节点的状态数据库, 用于将链码数据存储为简单的键-值对,仅支持键、键范围和复合键查询。CouchDB 是一 个可选的状态数据库,当链码数据以 JSON 建模时,它支持富查询。当您要查询实际数据 内容而不是键时,富查询对于大型索引数据存储更加灵活和高效。CouchDB 是一个 JSON 文本数据存储,而不是一个纯键-值存储,并且支持数据库中文本数据的索引。
为了发挥 CouchDB 的优势,也就是说基于内容的 JSON 查询,你的数据必须以 JSON 格式 建模。你必须在设置你的网络之前确定使用 LevelDB 还是 CouchDB 。由于数据兼容性的问 题,不支持节点从 LevelDB 切换为 CouchDB 。网络中的所有节点必须使用相同的数据库类 型。如果你想 JSON 和二进制数据混合使用,你同样可以使用 CouchDB ,但是二进制数据只 能根据键、键范围和复合键查询。
在 Hyperledger Fabric 中启用 CouchDB¶
CouchDB 是独立于节点运行的一个数据库进程,所以在安装、管理和操作的时候有一些额外
的注意事项。有一个可用的 docker 镜像 CouchDB
并且我们建议它和节点运行在同一个服务器上。我们需要在每一个节点上安装一个 CouchDB
容器,并且更新每一个节点的配置文件 core.yaml
,将节点指向 CouchDB 容器。
core.yaml
文件的路径必须在环境变量 FABRIC_CFG_PATH 中指定:
- 对于 docker 的部署,在节点容器中
FABRIC_CFG_PATH
指定的文件夹中的core.yaml
是预先配置好的。如果你要使用 docker 环境,你可以通过重写docker-compose-couch.yaml
中的环境变量来覆盖 core.yaml - 对于原生的二进制部署,
core.yaml
包含在发布的构件中。
编辑 core.yaml
中的 stateDatabase
部分。将 stateDatabase
指定为 CouchDB
并且填写 couchDBConfig
相关的配置。在 Fabric 中配置 CouchDB 的更多细节,请参阅
这里 。
配置 CouchDB 的示例 core.yaml 文件,请查看 HyperLedger/fabric-samples/first-network
文件夹下 BYFN 的 docker-compose-couch.yaml
。
创建一个索引¶
为什么索引很重要?
索引可以让数据库不用在每次查询的时候都检查每一行,可以让数据库运行的更快和更高效。 一般来说,对频繁查询的数据进行索引可以使数据查询更高效。为了充分发挥 CouchDB 的优 势 – 对 JSON 数据进行富查询的能力 – 并不需要索引,但是为了性能考虑强烈建议建立 索引。另外,如果在一个查询中需要排序,CouchDB 需要在排序的字段有一个索引。
注解
没有索引的情况下富查询也是可以使用的,但是会在 CouchDB 的日志中抛出一个没 有找到索引的警告。如果一个富查询中包含了一个排序的说明,需要排序的那个字段 就必须有索引;否则,查询将会失败并抛出错误。
To demonstrate building an index, we will use the data from the Marbles sample. In this example, the Marbles data structure is defined as:
type marble struct {
ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
Name string `json:"name"` //the field tags are needed to keep case from bouncing around
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
在这个结构体中,( docType
, name
, color
, size
, owner
)属性
定义了和资产相关的账本数据。 docType
属性用来在链码中区分可能需要单独查询的
不同数据类型的模式。当时使用 CouchDB 的时候,建议包含 docType
属性来区分在链
码命名空间中的每一个文档。(每一个链码都需要有他们自己的 CouchDB 数据库,也就是
说,每一个链码都有它自己的键的命名空间。)
在 Marbles 数据结构的定义中, docType
用来识别这个文档或者资产是一个弹珠资产。
同时在链码数据库中也可能存在其他文档或者资产。数据库中的文档对于这些属性值来说都是
可查询的。
当为链码查询定义一个索引的时候,每一个索引都必须定义在一个扩展名为 *.json
的
文本文件中,并且索引定义的格式必须为 CouchDB 索引的 JSON 格式。
需要以下三条信息来定义一个索引:
- fields: 这些是常用的查询字段
- name: 索引名
- type: 它的内容一般是 json
例如,这是一个对字段 foo
的一个名为 foo-index
的简单索引。
{
"index": {
"fields": ["foo"]
},
"name" : "foo-index",
"type" : "json"
}
可选地,设计文档( design document )属性 ddoc
可以写在索引的定义中。
design document 是 CouchDB 结构,
用于包含索引。索引可以以组的形式定义在设计文档中以提升效率,但是 CouchDB 建议每一
个设计文档包含一个索引。
小技巧
当定义一个索引的时候,最好将 ddoc
属性和值包含在索引内。包含这个
属性以确保在你需要的时候升级索引,这是很重要的。它还使你能够明确指定
要在查询上使用的索引。
这里有另外一个使用 Marbles 示例定义索引的例子,在索引 indexOwner
使用了多个
字段 docType
和 owner
并且包含了 ddoc
属性:
{
"index":{
"fields":["docType","owner"] // Names of the fields to be queried
},
"ddoc":"indexOwnerDoc", // (optional) Name of the design document in which the index will be created.
"name":"indexOwner",
"type":"json"
}
在上边的例子中,如果设计文档 indexOwnerDoc
不存在,当索引部署的时候会自动创建
一个。一个索引可以根据字段列表中指定的一个或者多个属性构建,而且可以定义任何属性的
组合。一个属性可以存在于同一个 docType 的多个索引中。在下边的例子中, index1
只包含 owner
属性, index2
包含 owner 和 color
属性, index3
包含
owner、 color 和 size
属性。另外,注意,根据 CouchDB 的建议,每一个索引的定义
都包含一个它们自己的 ddoc
值。
.. code:: json
- {
- “index”:{
- “fields”:[“owner”] // Names of the fields to be queried
}, “ddoc”:”index1Doc”, // (optional) Name of the design document in which the index will be created. “name”:”index1”, “type”:”json”
}
- {
- “index”:{
- “fields”:[“owner”, “color”] // Names of the fields to be queried
}, “ddoc”:”index2Doc”, // (optional) Name of the design document in which the index will be created. “name”:”index2”, “type”:”json”
}
- {
- “index”:{
- “fields”:[“owner”, “color”, “size”] // Names of the fields to be queried
}, “ddoc”:”index3Doc”, // (optional) Name of the design document in which the index will be created. “name”:”index3”, “type”:”json”
}
一般来说,你为索引字段建模应该匹配将用于查询过滤和排序的字段。对于以 JSON 格式 构建索引的更多信息请参阅 CouchDB documentation 。
关于索引最后要说的是,Fabric 在数据库中为文档建立索引的时候使用一种成为 索引升温
(index warming)
的模式。 CouchDB 直到下一次查询的时候才会索引新的或者更新的
文档。Fabric 通过在每一个数据区块提交完之后请求索引更新的方式,来确保索引处于 ‘热
(warm)’ 状态。这就确保了查询速度快,因为在运行查询之前不用索引文档。这个过程保
持了索引的现状,并在每次新数据添加到状态数据的时候刷新。
将索引添加到你的链码文件夹¶
当你完成索引之后,你需要把它打包到你的链码中,以便于将它部署到合适的元数据文件夹。
如果你使用 Hyperledger Fabric Node SDK 安装和初始化链码,JSON 索引文件可以放在符
合这个 `目录结构<https://fabric-sdk-node.github.io/tutorial-metadata-chaincode.html>`__
的任意位置。在使用 client.installChaincode() API 安装链码的时候,需要包含在
安装请求
中的属性 ( metadataPath
)。
或者,如果你使用 peer-commands 安装和初始化链码, JSON 索引文件必须放在
链码所在目录的 META-INF/statedb/couchdb/indexes
路径下。
下边的 Marbles 示例 说明了如何使用 peer 命令将索引打包进将要安装的链码中。

This sample includes one index named indexOwnerDoc:
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
启动网络¶
注解
The following tutorial needs to be run using the release-1.4 version of the Fabric Samples. If you have already downloaded release-2.0 of the Fabric Samples, you can use the git checkout to download release-1.4. Navigate to the fabric-samples directory on your local machine. Then run the command git checkout v1.4.0.
Try it yourself
在安装和初始化弹珠链码之前,我们需要启动 BYFN 网络。考虑到本教程的目的, 我们需要在一个已知的初始状态操作。下边的命令将关闭所有激活状态或者存在 的 docker 容器,并删除之前生成的构建。然后,我们运行下边的命令来清除所 有之前的环境:
cd fabric-samples/first-network ./byfn.sh down现在使用下边的命令启动启用了 CouchDB 的 BYFN 网络:
./byfn.sh up -c mychannel -s couchdb这将创建一个简单的 Fabric 网络,其中包含一个叫 mychannel 的通道,通道中 有两个组织(每个组织两个 peer 节点)和一个排序服务,同时使用 CouchDB 作为 状态数据库。
安装和初始化链码¶
客户端应用通过链码和区块链账本交互。所以我们需要在每一个执行和背书交易的节点 上安装链码,并且在通道上初始化链码。在之前的章节中,我们演示了如何打包链码, 所以他们应该已经准备好部署了。
我们将使用 peer-commands 将链码安装到节点,然后在通道上初始化。
- 使用`peer chaincode install <http://hyperledger-fabric.readthedocs.io/en/master/commands/peerchaincode.html?%20chaincode%20instantiate#peer-chaincode-install>`__
命令将链码安装到节点上。Try it yourself
假设你已经启动了 BYFN 网路,使用下边的命令进入到 CLI 容器:
docker exec -it cli bash使用下边命令从 github 仓库将 Marbles 链码安装到你的 BYFN 网络。CLI 容器 默认使用 org1 的 peer0 节点:
peer chaincode install -n marbles -v 1.0 -p github.com/hyperledger/fabric-samples/chaincode/marbles02/go
- 执行 peer chaincode instantiate 命令在通道上初始化链码。
Try it yourself
使用下边的命令在 BYFN 通道
mychannel
上初始化 Marbles 示例:export CHANNEL_NAME=mychannel peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -v 1.0 -c '{"Args":["init"]}' -P "OR ('Org0MSP.peer','Org1MSP.peer')"
验证部署的索引¶
当链码在节点上安装并且在通道上实例化完成之后,索引会被部署到每一个节点的 CouchDB 状态数据库上。你可以通过检查 Docker 容器中的节点日志来确认 CouchDB 是否被创建成功。
Try it yourself
为了查看节点 docker 容器的日志,打开一个新的终端窗口,然后运行下边的命令来匹配索 引被创建的确认信息。
docker logs peer0.org1.example.com 2>&1 | grep "CouchDB index"
你将会看到类似下边的结果:
[couchdb] CreateIndex -> INFO 0be Created CouchDB index [indexOwner] in state database [mychannel_marbles] using design document [_design/indexOwnerDoc]注解
如果 Marbles 没有安装在 BYFN 的节点
peer0.org1.example.com
上,你可 能需要切换到其他的安装了 Marbles 的节点。
查询 CouchDB 状态数据库¶
现在索引已经在 JSON 中定义了并且和链码部署在了一起,链码函数可以对 CouchDB 状态数据 库执行 JSON 查询,同时 peer 命令可以调用链码函数。
在查询的时候指定索引的名字是可选的。如果不指定,同时索引已经在被查询的字段上存在了, 已存在的索引会自动被使用。
小技巧
在查询的时候使用 use_index
关键字包含一个索引名字是一个好的习惯。如果
不使用索引名,CouchDB 可能不会使用最优的索引。而且 CouchDB 也可能会不使用
索引,但是在测试期间数据少的化你很难意识到。只有在数据量大的时候,你才可能
会意识到因为 CouchDB 没有使用索引而导致性能较低。
在链码中构建一个查询¶
在链码中使用 CouchDB JSON 查询语言,你可以对链码数据进行复杂的富查询。就像上边所说,
marbles02 示例链码
在函数 - queryMarbles
和 queryMarblesByOwner
- 中包含了索引和富查询:
- 一个 富查询 示例。这是一个可以将一个(选择器)字符串传入函数的查询。
这个查询对于需要在运行时动态创建他们自己的选择器的客户端应用程序很有用。 跟多关于选择器的信息请参考 CouchDB selector syntax 。
queryMarblesByOwner –
一个查询逻辑保存在链码中的参数查询的示例。在这个例子中,函数值接受单个参数, 就是弹珠的主人。然后使用 JSON 查询语法查询状态数据库中匹配 “marble” 的 docType 和 拥有者 id 的 JSON 文档。
使用 peer 命令运行查询¶
在没有客户端应用程序测试链码中定义的富查询的时候,可以使用 peer 命令。 peer 命令
在 docker 容器的命令行中运行。我们可以自定义 peer chaincode query 命令来使用 Marbles 的索引 indexOwner
并且使用 queryMarbles
函数查询所有拥有者为 “Tom” 的弹珠。
Try it yourself
在查询数据库之前,我们应该添加一些数据。在节点容器中运行下边的命令来创建一个拥 有者为 “tom” 的弹珠:
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble1","blue","35","tom"]}'在链码初始化期间部署索引之后,索引就可以自动被链码的查询使用。CouchDB 可以根 据查询的字段决定使用哪个索引。如果这个查询准则存在索引,它就会被使用。但是建 议在查询的时候指定
use_index
关键字。下边的 peer 命令就是一个如何通过在选 择器语法中包含use_index
关键字来明确地指定索引的例子:// Rich Query with index name explicitly specified: peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}'
详细看一下上边的查询命令,有三个参数值得关注:
queryMarbles
Marbles 链码中的函数名称。注意使用了一个 shimshim.ChaincodeStubInterface
来访问和修改账本。getQueryResultForQueryString()
传递 queryString 给 shim APIgetQueryResult()
.
func (t *SimpleChaincode) queryMarbles(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 0
// "queryString"
if len(args) < 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
queryString := args[0]
queryResults, err := getQueryResultForQueryString(stub, queryString)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(queryResults)
}
{"selector":{"docType":"marble","owner":"tom"}
这是一个 ad hoc 选择器 字符串的示例,用来查找所有owner
属性值为tom
的marble
的文档。
"use_index":["_design/indexOwnerDoc", "indexOwner"]
指定设计文档名indexOwnerDoc
和索引名indexOwner
。在这个示例中,查询 选择器通过指定use_index
关键字明确包含了索引名。回顾一下上边的索引定义 创建一个索引 , 它包含了设计文档,"ddoc":"indexOwnerDoc"
。在 CouchDB 中,如果你想在查询 中明确包含索引名,在索引定义中必须包含ddoc
值,然后它才可以被use_index
关键字引用。
利用索引的查询成功后返回如下结果:
Query Result: [{"Key":"marble1", "Record":{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}}]
Use best practices for queries and indexes¶
Queries that use indexes will complete faster, without having to scan the full database in couchDB. Understanding indexes will allow you to write your queries for better performance and help your application handle larger amounts of data or blocks on your network.
It is also important to plan the indexes you install with your chaincode. You should install only a few indexes per chaincode that support most of your queries. Adding too many indexes, or using an excessive number of fields in an index, will degrade the performance of your network. This is because the indexes are updated after each block is committed. The more indexes need to be updated through “index warming”, the longer it will take for transactions to complete.
The examples in this section will help demonstrate how queries use indexes and what type of queries will have the best performance. Remember the following when writing your queries:
- All fields in the index must also be in the selector or sort sections of your query for the index to be used.
- More complex queries will have a lower performance and will be less likely to use an index.
- You should try to avoid operators that will result in a full table scan or a
full index scan such as
$or
,$in
and$regex
.
In the previous section of this tutorial, you issued the following query against the marbles chaincode:
// Example one: query fully supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'
The marbles chaincode was installed with the indexOwnerDoc
index:
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
Notice that both the fields in the query, docType
and owner
, are
included in the index, making it a fully supported query. As a result this
query will be able to use the data in the index, without having to search the
full database. Fully supported queries such as this one will return faster than
other queries from your chaincode.
If you add extra fields to the query above, it will still use the index. However, the query will additionally have to scan the indexed data for the extra fields, resulting in a longer response time. As an example, the query below will still use the index, but will take a longer time to return than the previous example.
// Example two: query fully supported by the index with additional data
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\",\"color\":\"red\"}, \"use_index\":[\"/indexOwnerDoc\", \"indexOwner\"]}"]}'
A query that does not include all fields in the index will have to scan the full
database instead. For example, the query below searches for the owner, without
specifying the the type of item owned. Since the ownerIndexDoc contains both
the owner
and docType
fields, this query will not be able to use the
index.
// Example three: query not supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{\"owner\":\"tom\"}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'
In general, more complex queries will have a longer response time, and have a
lower chance of being supported by an index. Operators such as $or
, $in
,
and $regex
will often cause the query to scan the full index or not use the
index at all.
As an example, the query below contains an $or
term that will search for every
marble and every item owned by tom.
// Example four: query with $or supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{"\$or\":[{\"docType\:\"marble\"},{\"owner\":\"tom\"}]}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'
This query will still use the index because it searches for fields that are
included in indexOwnerDoc
. However, the $or
condition in the query
requires a scan of all the items in the index, resulting in a longer response
time.
Below is an example of a complex query that is not supported by the index.
// Example five: Query with $or not supported by the index
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarbles", "{\"selector\":{"\$or\":[{\"docType\":\"marble\",\"owner\":\"tom\"},{"\color\":"\yellow\"}]}, \"use_index\":[\"indexOwnerDoc\", \"indexOwner\"]}"]}'
The query searches for all marbles owned by tom or any other items that are
yellow. This query will not use the index because it will need to search the
entire table to meet the $or
condition. Depending the amount of data on your
ledger, this query will take a long time to respond or may timeout.
While it is important to follow best practices with your queries, using indexes is not a solution for collecting large amounts of data. The blockchain data structure is optimized to validate and confirm transactions, and is not suited for data analytics or reporting. If you want to build a dashboard as part of your application or analyze the data from your network, the best practice is to query an off chain database that replicates the data from your peers. This will allow you to understand the data on the blockchain without degrading the performance of your network or disrupting transactions.
You can use block or chaincode events from your application to write transaction
data to an off-chain database or analytics engine. For each block received, the block
listener application would iterate through the block transactions and build a data
store using the key/value writes from each valid transaction’s rwset
. The
基于通道的节点事件服务 provide replayable events to ensure the integrity of
downstream data stores.
在 CouchDB 状态数据库查询中使用分页¶
当 CouchDB 的查询返回了一个很大的结果集时,有一些将结果分页的 API 可以提供给链码调用。分
页提供了一个将结果集合分区的机制,该机制指定了一个 pagesize
和起始点 – 一个从结果集
合的哪里开始的 书签
。客户端应用程序以迭代的方式调用链码来执行查询,直到没有更多的结
果返回。更多信息请参考 topic on pagination with CouchDB 。
我们将使用 Marbles sample
中的函数 queryMarblesWithPagination
来演示在链码和客户端应用程序中如何使用分页。
queryMarblesWithPagination –
一个 使用分页的 ad hoc 富查询 的示例。这是一个像上边的示例一样,可以将一个(选择器) 字符串传入函数的查询。在这个示例中,在查询中也包含了一个
pageSize
作为一个标签
。
为了演示分页,需要更多的数据。本例假设你已经加入了 marble1 。在节点容器中执行下边的命令创建 4 个 “tom” 的弹珠,这样就创建了 5 个 “tom” 的弹珠:
Try it yourself
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble2","yellow","35","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble3","green","20","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble4","purple","20","tom"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n marbles -c '{"Args":["initMarble","marble5","blue","40","tom"]}'
除了上边示例中的查询参数, queryMarblesWithPagination 增加了 pagesize
和 bookmark
。
PageSize
指定了每次查询返回结果的数量。 bookmark
是一个用来告诉 CouchDB 从每一页从
哪开始的 “锚(anchor)” 。(结果的每一页都返回一个唯一的书签)
queryMarblesWithPagination
Marbles 链码中函数的名称。注意 shimshim.ChaincodeStubInterface
用于访问和修改账本。getQueryResultForQueryStringWithPagination()
将 queryString 、 pagesize 和 bookmark 传递给 shim APIGetQueryResultWithPagination()
。
func (t *SimpleChaincode) queryMarblesWithPagination(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 0
// "queryString"
if len(args) < 3 {
return shim.Error("Incorrect number of arguments. Expecting 3")
}
queryString := args[0]
//return type of ParseInt is int64
pageSize, err := strconv.ParseInt(args[1], 10, 32)
if err != nil {
return shim.Error(err.Error())
}
bookmark := args[2]
queryResults, err := getQueryResultForQueryStringWithPagination(stub, queryString, int32(pageSize), bookmark)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(queryResults)
}
下边的例子是一个 peer 命令,以 pageSize 为 3
没有指定 boomark 的方式调用 queryMarblesWithPagination 。
小技巧
当没有指定 bookmark 的时候,查询从记录的“第一”页开始。
Try it yourself
// Rich Query with index name explicitly specified and a page size of 3:
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3",""]}'
下边是接收到的响应(为清楚起见,增加了换行),返回了五个弹珠中的三个,因为 pagesize
设置成了 3
。
[{"Key":"marble1", "Record":{"color":"blue","docType":"marble","name":"marble1","owner":"tom","size":35}},
{"Key":"marble2", "Record":{"color":"yellow","docType":"marble","name":"marble2","owner":"tom","size":35}},
{"Key":"marble3", "Record":{"color":"green","docType":"marble","name":"marble3","owner":"tom","size":20}}]
[{"ResponseMetadata":{"RecordsCount":"3",
"Bookmark":"g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkGoOkOWDSOSANIFk2iCyIyVySn5uVBQAGEhRz"}}]
注解
Bookmark 是 CouchDB 每次查询的时候唯一生成的,并显示在结果集中。将返回的 bookmark 传递给迭代查询的子集中来获取结果的下一个集合。
下边是一个 pageSize 为 3
的调用 queryMarblesWithPagination 的 peer 命令。
注意一下这里,这次的查询包含了上次查询返回的 bookmark 。
Try it yourself
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3","g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkGoOkOWDSOSANIFk2iCyIyVySn5uVBQAGEhRz"]}'
下边是接收到的响应(为清楚起见,增加了换行),返回了五个弹珠中的三个,返回了剩下的两个记录: .. code:: bash
- [{“Key”:”marble4”, “Record”:{“color”:”purple”,”docType”:”marble”,”name”:”marble4”,”owner”:”tom”,”size”:20}},
- {“Key”:”marble5”, “Record”:{“color”:”blue”,”docType”:”marble”,”name”:”marble5”,”owner”:”tom”,”size”:40}}]
[{“ResponseMetadata”:{“RecordsCount”:”2”, “Bookmark”:”g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1”}}]
最后一个命令是调用 queryMarblesWithPagination 的 peer 命令,其中 pageSize 为 3
,bookmark 是前一次查询返回的结果。
Try it yourself
peer chaincode query -C $CHANNEL_NAME -n marbles -c '{"Args":["queryMarblesWithPagination", "{\"selector\":{\"docType\":\"marble\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3","g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1"]}'
下边是接收到的响应(为清楚起见,增加了换行)。没有记录返回,说明所有的页 面都获取到了:
[]
[{"ResponseMetadata":{"RecordsCount":"0",
"Bookmark":"g1AAAABLeJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqz5yYWJeWkmoKkOWDSOSANIFk2iCyIyVySn5uVBQAGYhR1"}}]
对于如何使用客户端应用程序使用分页迭代结果集,请在
Marbles sample 。
中搜索 getQueryResultForQueryStringWithPagination
函数。
升级索引¶
可能需要随时升级索引。相同的索引可能会存在安装的链码的子版本中。为了索引的升级,
原来的索引定义必须包含在设计文档 ddoc
属性和索引名。为了升级索引定义,使用相
同的索引名并改变索引定义。简单编辑索引 JSON 文件并从索引中增加或者删除字段。 Fabric
只支持 JSON 类型的索引,不支持改变索引类型。升级后的索引定义在链码安装和初始化之后
会重新部署在节点的状态数据库中。
注解
如果状态数据库有大量数据,重建索引的过程会花费较长时间,在此期间链码执 行或者查询可能会失败或者超时。
迭代索引定义¶
如果你在开发环境中访问你的节点的 CouchDB 状态数据库,你可以迭代测试各种索引以支 持你的链码查询。链码的任何改变都可能需要重新部署。使用 CouchDB Fauxton interface 或者命令行 curl 工具来创建和升级索引。
注解
Fauxton 是用于创建、升级和部署 CouchDB 索引的一个网页,如果你想尝试这个接口,
有一个 Marbles 示例中索引的 Fauxton 版本格式的例子。如果你使用 CouchDB 部署了
BYFN 网络,可以通过在浏览器的导航栏中打开 http://localhost:5984/_utils
来
访问 Fauxton 。
另外,如果你不想使用 Fauxton UI,下边是通过 curl 命令在 mychannel_marbles
数据库上创
建索引的例子:
// Index for docType, owner. // Example curl command line to define index in the CouchDB channel_chaincode database
curl -i -X POST -H "Content-Type: application/json" -d
"{\"index\":{\"fields\":[\"docType\",\"owner\"]},
\"name\":\"indexOwner\",
\"ddoc\":\"indexOwnerDoc\",
\"type\":\"json\"}" http://hostname:port/mychannel_marbles/_index
注解
如果你在 BYFN 中配置了 CouchDB,请使用 localhost:5984
替换 hostname:port 。
删除索引¶
Fabric 工具不能删除索引。如果你需要删除索引,就要手动使用 curl 命令或者 Fauxton 接 口操作数据库。
删除索引的 curl 命令格式如下:
curl -X DELETE http://localhost:5984/{database_name}/_index/{design_doc}/json/{index_name} -H "accept: */*" -H "Host: localhost:5984"
要删除本教程中的索引,curl 命令应该是:
curl -X DELETE http://localhost:5984/mychannel_marbles/_index/indexOwnerDoc/json/indexOwner -H "accept: */*" -H "Host: localhost:5984"
操作指南¶
升级到最新版 Fabric¶
总的来说,将 Fabric 网络从 v1.3 升级到 v1.4 有如下步骤:
- 更新排序服务、 Fabric CA 、和 peer 的二进制文件。这些可以并行完成。
- 升级 SDK 客户端。
- (可选)升级 Kafka 集群
为了帮助你理解这个过程,我们编写了 升级你的网络组件 教程来 带你体验主要的升级步骤,包括升级 peer 节点和排序节点。为了达到升级的目的,文档中 包含了脚本和单独的步骤。
因为我们的教程依赖 构建你的第一个网络 (BYFN) 示例,它确实有一些限制(比如没有使 用 Fabric CA)。所以我们在章节的最后增加了一部分内容来演示如何升级 CA、Kafka 集群、 CouchDB、Zookeeper、链码依赖和 Node SDK 客户端。
因为在 v1.4 中没有更新 <no title> ,所以更新过程不需要通道配置 交易。
设置排序节点¶
在这个主题里,我们将描述启动排序节点的过程。如果你想了解更多不同排序服务的实现和它们的相对 优势与弱点的更多信息,请查看我们的排序关键概念文档。
大体上,本主题将涉及几个相关联的步骤:
- 建立排序节点所属于的组织(如果你还没有完成这个)
- 配置节点(使用
orderer.yaml
) - 建立排序节点系统通道所使用的创世区块
- 启动排序节点
注意:这个主题假定你已经从 docker hub 拉取了 Hyperledger Fabric 排序节点镜像。
建立组织定义¶
和对等节点相似,所有的排序节点必须从属于一个在建立其之前就建立的组织。这个组织具有由 成员服务提供者(MSP)封装的定义,该定义由专门为 此组织创建证书和 MSP 的证书颁发机构创建。
关于创建 CA 并使用它创建用户和 MSP 的信息,请查看Fabric CA 用户手册。
配置节点¶
排序节点的配置通过一个名为 orderer.yaml
的 yaml
文件来处理。环境变量 FABRIC_CFG_PATH
被用来指向你已经配置好的 orderer.yaml
文件,它将提取一系列文件系统上的文件和证书。
要查看示例 orderer.yaml
,请查看fabric-samples
github 仓库,
在继续之前应仔细阅读、研究该文档。
特别注意一些值:
LocalMSPID
— 这是排序节点组织的 MSP 的名字,由你的 CA 生成。排序节点组织管理员 会被列表显示在这里。LocalMSPDir
— 文件系统中本地 MSP 所在的位置。# TLS enabled
,Enabled: false
。指定是否要启用 TLS。如果 将值设为true
,你必须指定相关的 TLS 证书位置。注意,这对于 Raft 节点是必须的。GenesisFile
— 这是将为排序服务生成的创世区块的名字。GenesisMethod
— 创建创世区块的方法。可以是file
,指定GenesisFile
中的文件, 或者是provisional
,使用GenesisProfile
中的配置。
如果你要将这个节点部署为集群中的一个部分(例如,作为 Raft 集群中的一个部分),注意 Cluster
和 Consensus
部分。
如果你计划部署一个基于 Kafka 的排序服务,你需要完成 Kafka
部分。
生成排序节点创世区块¶
新创建通道的第一个区块称为“创世区块”。如果创世区块作为新网络创建的一部分被创建(换句话说, 如果正创建的排序节点不会加入一个已经存在的排序节点集群),那么这个创世区块将成为“排序节点系统 通道”(即“排序系统通道”)中的第一个区块,排序系统通道是一个由排序节点管理员管理的特殊通道,包 括被允许创建通道的组织的列表。排序节点系统通道的创世区块是特殊的:它必须先创建并被包含在节点 配置中,然后才能启动节点。
学习如何使用 configtxgen
工具创建创世区块,请查看通道配置(configtx)。
启动排序节点¶
一旦你构建了镜像、创建了 MSP,配置了 orderer.yaml
,并创建了创世区块,你就可以使用如下的命令
来启动排序节点:
docker-compose -f docker-compose-cli.yaml up -d --no-deps orderer.example.com
将 orderer.example.com
替换为你的排序节点地址。
更新通道配置¶
什么是通道配置?¶
通道配置包含关于通道管理的所有信息。最重要的是,通道配置指定了哪些组织是通道的成员,但它也 包含其他的全通道的配置信息,例如,通道访问策略和区块的批处理大小。
这个配置被存储在账本上的一个区块里,因此被称为配置(config)区块。每个配置区块包含一个 配置。这些区块中的第一个被称为“创世区块”,包含启动通道所需要的初始配置。对通道的每次配置修 改都是通过新配置区块完成的,最新的配置区块表示当前的通道配置。排序节点和对等节点在内存中保 持当前通道配置以便于所有通道操作,例如分割新区块、验证区块交易。
因为配置保存在区块中,更新配置是通过一个称为“配置交易”的过程进行的(尽管这个过程和普通交易 有点不同)。更新配置是一个拉取配置、转换为人可读的格式、修改并提交审核的过程,
要更深入地了解拉取配置并转换为 JSON 的过程,请查看向通道添加组织。 在本文中,我们将重点关注编辑配置的不同方法,及对它进行签名的过程。
编辑配置¶
通道是高度可配置的,但并非无限制。不同的配置元素有不同的修改策略(指定了签名配置更新所需要的 身份组).
要了解可以修改的范围,查看 JSON 格式的配置很重要。向通道添加组织 教程生成了一个配置,如果你已经阅读了该文档,你只需要参考它。如果没有,我们将在这里提供一个 (为易于阅读,可将此配置放在支持 JSON 折叠的查看器中,比如 atom 或 Visual Studio)。
**点击这里查看配置**
``` { "channel_group": { "groups": { "Application": { "groups": { "Org1MSP": { "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org1MSP", "role": "ADMIN" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org1MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org1MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" } }, "values": { "AnchorPeers": { "mod_policy": "Admins", "value": { "anchor_peers": [ { "host": "peer0.org1.example.com", "port": 7051 } ] }, "version": "0" }, "MSP": { "mod_policy": "Admins", "value": { "config": { "admins": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNHRENDQWIrZ0F3SUJBZ0lRSWlyVmg3NVcwWmh0UjEzdmltdmliakFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTVM1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NUzVsZUdGdGNHeGxMbU52YlRBZUZ3MHhOekV4TWpreE9USTBNRFphRncweU56RXhNamN4T1RJME1EWmEKTUZzeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVI4d0hRWURWUVFEREJaQlpHMXBia0J2Y21jeExtVjRZVzF3YkdVdVkyOXRNRmt3CkV3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFNkdVeDlpczZ0aG1ZRE9tMmVHSlA5eW1yaXJYWE1Cd0oKQmVWb1Vpak5haUdsWE03N2NsSE5aZjArMGFjK2djRU5lMzQweGExZVFnb2Q0YjVFcmQrNmtxTk5NRXN3RGdZRApWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEVlIwakJDUXdJb0FnWWdoR2xCMjBGWmZCCllQemdYT280czdkU1k1V3NKSkRZbGszTDJvOXZzQ013Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnYmlEWDVTMlIKRTBNWGRobDZFbmpVNm1lTEJ0eXNMR2ZpZXZWTlNmWW1UQVVDSUdVbnROangrVXZEYkZPRHZFcFRVTm5MUHp0Qwp5ZlBnOEhMdWpMaXVpaWFaCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ], "crypto_config": { "identity_identifier_hash_function": "SHA256", "signature_hash_family": "SHA2" }, "name": "Org1MSP", "root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNRekNDQWVxZ0F3SUJBZ0lSQU03ZVdTaVM4V3VVM2haMU9tR255eXd3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGN4TVRJNU1Ua3lOREEyV2hjTk1qY3hNVEkzTVRreU5EQTIKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NUzVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkJiTTVZS3B6UmlEbDdLWWFpSDVsVnBIeEl1TDEyaUcyWGhkMHRpbEg3MEljMGFpRUh1dG9rTkZsUXAzTWI0Zgpvb0M2bFVXWnRnRDJwMzZFNThMYkdqK2pYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSUdJSVJwUWR0QldYd1dEODRGenEKT0xPM1VtT1ZyQ1NRMkpaTnk5cVBiN0FqTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUdlS2VZL1BsdGlWQTRPSgpRTWdwcDRvaGRMcGxKUFpzNERYS0NuOE9BZG9YQWlCK2g5TFdsR3ZsSDdtNkVpMXVRcDFld2ZESmxsZi9MZXczClgxaDNRY0VMZ3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" ], "tls_root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTVENDQWZDZ0F3SUJBZ0lSQUtsNEFQWmV6dWt0Nk8wYjRyYjY5Y0F3Q2dZSUtvWkl6ajBFQXdJd2RqRUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIekFkQmdOVkJBTVRGblJzCmMyTmhMbTl5WnpFdVpYaGhiWEJzWlM1amIyMHdIaGNOTVRjeE1USTVNVGt5TkRBMldoY05NamN4TVRJM01Ua3kKTkRBMldqQjJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRQpCeE1OVTJGdUlFWnlZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTVM1bGVHRnRjR3hsTG1OdmJURWZNQjBHCkExVUVBeE1XZEd4elkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDkKQXdFSEEwSUFCSnNpQXVjYlcrM0lqQ2VaaXZPakRiUmFyVlRjTW9TRS9mSnQyU0thR1d5bWQ0am5xM25MWC9vVApCVmpZb21wUG1QbGZ4R0VSWHl0UTNvOVZBL2hwNHBlalh6QmRNQTRHQTFVZER3RUIvd1FFQXdJQnBqQVBCZ05WCkhTVUVDREFHQmdSVkhTVUFNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdLUVlEVlIwT0JDSUVJSnlqZnFoa0FvY3oKdkRpNnNGSGFZL1Bvd2tPWkxPMHZ0VGdFRnVDbUpFalZNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJRjVOVVdCVgpmSjgrM0lxU3J1NlFFbjlIa0lsQ0xDMnlvWTlaNHBWMnpBeFNBaUE5NWQzeDhBRXZIcUFNZnIxcXBOWHZ1TW5BCmQzUXBFa1gyWkh3ODZlQlVQZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ] }, "type": 0 }, "version": "0" } }, "version": "1" }, "Org2MSP": { "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org2MSP", "role": "ADMIN" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org2MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org2MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" } }, "values": { "AnchorPeers": { "mod_policy": "Admins", "value": { "anchor_peers": [ { "host": "peer0.org2.example.com", "port": 9051 } ] }, "version": "0" }, "MSP": { "mod_policy": "Admins", "value": { "config": { "admins": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNHVENDQWNDZ0F3SUJBZ0lSQU5Pb1lIbk9seU94dTJxZFBteStyV293Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGN4TVRJNU1Ua3lOREEyV2hjTk1qY3hNVEkzTVRreU5EQTIKV2pCYk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFZk1CMEdBMVVFQXd3V1FXUnRhVzVBYjNKbk1pNWxlR0Z0Y0d4bExtTnZiVEJaCk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkh1M0ZWMGlqdFFzckpsbnBCblgyRy9ickFjTHFJSzgKVDFiSWFyZlpvSkhtQm5IVW11RTBhc1dyKzM4VUs0N3hyczNZMGMycGhFVjIvRnhHbHhXMUZubWpUVEJMTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBTUJnTlZIUk1CQWY4RUFqQUFNQ3NHQTFVZEl3UWtNQ0tBSU1pSzdteFpnQVVmCmdrN0RPTklXd2F4YktHVGdLSnVSNjZqVmordHZEV3RUTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUQxaEtRdk8KVWxyWmVZMmZZY1N2YWExQmJPM3BVb3NxL2tZVElyaVdVM1J3QWlBR29mWmVPUFByWXVlTlk0Z2JCV2tjc3lpZgpNMkJmeXQwWG9NUThyT2VidUE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" ], "crypto_config": { "identity_identifier_hash_function": "SHA256", "signature_hash_family": "SHA2" }, "name": "Org2MSP", "root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQU1pVXk5SGRSbXB5MDdsSjhRMlZNWXN3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGN4TVRJNU1Ua3lOREEyV2hjTk1qY3hNVEkzTVRreU5EQTIKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1pNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQk50YW1PY1hyaGwrQ2hzYXNSeklNWjV3OHpPWVhGcXhQbGV0a3d5UHJrbHpKWE01Qjl4QkRRVWlWNldJS2tGSwo0Vmd5RlNVWGZqaGdtd25kMUNBVkJXaWpYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSU1pSzdteFpnQVVmZ2s3RE9OSVcKd2F4YktHVGdLSnVSNjZqVmordHZEV3RUTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFEQ3FFRmFqeU5IQmVaRworOUdWVkNFNWI1YTF5ZlhvS3lkemdLMVgyOTl4ZmdJZ05BSUUvM3JINHFsUE9HbjdSS3Yram9WaUNHS2t6L0F1Cm9FNzI4RWR6WmdRPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" ], "tls_root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTakNDQWZDZ0F3SUJBZ0lSQU9JNmRWUWMraHBZdkdMSlFQM1YwQU13Q2dZSUtvWkl6ajBFQXdJd2RqRUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIekFkQmdOVkJBTVRGblJzCmMyTmhMbTl5WnpJdVpYaGhiWEJzWlM1amIyMHdIaGNOTVRjeE1USTVNVGt5TkRBMldoY05NamN4TVRJM01Ua3kKTkRBMldqQjJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRQpCeE1OVTJGdUlFWnlZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTWk1bGVHRnRjR3hsTG1OdmJURWZNQjBHCkExVUVBeE1XZEd4elkyRXViM0puTWk1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDkKQXdFSEEwSUFCTWZ1QTMwQVVBT1ZKRG1qVlBZd1lNbTlweW92MFN6OHY4SUQ5N0twSHhXOHVOOUdSOU84aVdFMgo5bllWWVpiZFB2V1h1RCszblpweUFNcGZja3YvYUV5alh6QmRNQTRHQTFVZER3RUIvd1FFQXdJQnBqQVBCZ05WCkhTVUVDREFHQmdSVkhTVUFNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdLUVlEVlIwT0JDSUVJRnk5VHBHcStQL08KUGRXbkZXdWRPTnFqVDRxOEVKcDJmbERnVCtFV2RnRnFNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNZYlhSeApXWDZoUitPU0xBNSs4bFRwcXRMWnNhOHVuS3J3ek1UYXlQUXNVd0lnVSs5YXdaaE0xRzg3bGE0V0h4cmt5eVZ2CkU4S1ZsR09IVHVPWm9TMU5PT0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ] }, "type": 0 }, "version": "0" } }, "version": "1" }, "Org3MSP": { "groups": {}, "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org3MSP", "role": "ADMIN" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org3MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "Org3MSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" } }, "values": { "MSP": { "mod_policy": "Admins", "value": { "config": { "admins": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNHRENDQWIrZ0F3SUJBZ0lRQUlSNWN4U0hpVm1kSm9uY3FJVUxXekFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTXk1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NeTVsZUdGdGNHeGxMbU52YlRBZUZ3MHhOekV4TWpreE9UTTRNekJhRncweU56RXhNamN4T1RNNE16QmEKTUZzeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVI4d0hRWURWUVFEREJaQlpHMXBia0J2Y21jekxtVjRZVzF3YkdVdVkyOXRNRmt3CkV3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFSFlkVFY2ZC80cmR4WFd2cm1qZ0hIQlhXc2lxUWxrcnQKZ0p1NzMxcG0yZDRrWU82aEd2b2tFRFBwbkZFdFBwdkw3K1F1UjhYdkFQM0tqTkt0NHdMRG5hTk5NRXN3RGdZRApWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEVlIwakJDUXdJb0FnSWNxUFVhM1VQNmN0Ck9LZmYvKzVpMWJZVUZFeVFlMVAyU0hBRldWSWUxYzB3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnUm5LRnhsTlYKSmppVGpkZmVoczRwNy9qMkt3bFVuUWVuNFkyUnV6QjFrbm9DSUd3dEZ1TEdpRFY2THZSL2pHVXR3UkNyeGw5ZApVNENCeDhGbjBMdXNMTkJYCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ], "crypto_config": { "identity_identifier_hash_function": "SHA256", "signature_hash_family": "SHA2" }, "name": "Org3MSP", "root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNRakNDQWVtZ0F3SUJBZ0lRUkN1U2Y0RVJNaDdHQW1ydTFIQ2FZREFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTXk1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NeTVsZUdGdGNHeGxMbU52YlRBZUZ3MHhOekV4TWpreE9UTTRNekJhRncweU56RXhNamN4T1RNNE16QmEKTUhNeEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVJrd0Z3WURWUVFLRXhCdmNtY3pMbVY0WVcxd2JHVXVZMjl0TVJ3d0dnWURWUVFECkV4TmpZUzV2Y21jekxtVjRZVzF3YkdVdVkyOXRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUKZXFxOFFQMnllM08vM1J3UzI0SWdtRVdST3RnK3Zyc2pRY1BvTU42NEZiUGJKbmExMklNaVdDUTF6ZEZiTU9hSAorMUlrb21yY0RDL1ZpejkvY0M0NW9xTmZNRjB3RGdZRFZSMFBBUUgvQkFRREFnR21NQThHQTFVZEpRUUlNQVlHCkJGVWRKUUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFwQmdOVkhRNEVJZ1FnSWNxUFVhM1VQNmN0T0tmZi8rNWkKMWJZVUZFeVFlMVAyU0hBRldWSWUxYzB3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnTEgxL2xSZElWTVA4Z2FWeQpKRW01QWQ0SjhwZ256N1BVV2JIMzZvdVg4K1lDSUNPK20vUG9DbDRIbTlFbXhFN3ZnUHlOY2trVWd0SlRiTFhqCk5SWjBxNTdWCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ], "tls_root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTVENDQWZDZ0F3SUJBZ0lSQU9xc2JQQzFOVHJzclEvUUNpalh6K0F3Q2dZSUtvWkl6ajBFQXdJd2RqRUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpNdVpYaGhiWEJzWlM1amIyMHhIekFkQmdOVkJBTVRGblJzCmMyTmhMbTl5WnpNdVpYaGhiWEJzWlM1amIyMHdIaGNOTVRjeE1USTVNVGt6T0RNd1doY05NamN4TVRJM01Ua3oKT0RNd1dqQjJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRQpCeE1OVTJGdUlFWnlZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTXk1bGVHRnRjR3hsTG1OdmJURWZNQjBHCkExVUVBeE1XZEd4elkyRXViM0puTXk1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDkKQXdFSEEwSUFCSVJTTHdDejdyWENiY0VLMmhxSnhBVm9DaDhkejNqcnA5RHMyYW9TQjBVNTZkSUZhVmZoR2FsKwovdGp6YXlndXpFalFhNlJ1MmhQVnRGM2NvQnJ2Ulpxalh6QmRNQTRHQTFVZER3RUIvd1FFQXdJQnBqQVBCZ05WCkhTVUVDREFHQmdSVkhTVUFNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdLUVlEVlIwT0JDSUVJQ2FkVERGa0JPTGkKblcrN2xCbDExL3pPbXk4a1BlYXc0MVNZWEF6cVhnZEVNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJQlgyMWR3UwpGaG5NdDhHWXUweEgrUGd5aXQreFdQUjBuTE1Jc1p2dVlRaktBaUFLUlE5N2VrLzRDTzZPWUtSakR0VFM4UFRmCm9nTmJ6dTBxcThjbVhseW5jZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" ] }, "type": 0 }, "version": "0" } }, "version": "0" } }, "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "MAJORITY", "sub_policy": "Admins" } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Readers" } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Writers" } }, "version": "0" } }, "version": "1" }, "Orderer": { "groups": { "OrdererOrg": { "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "OrdererMSP", "role": "ADMIN" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "OrdererMSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 1, "value": { "identities": [ { "principal": { "msp_identifier": "OrdererMSP", "role": "MEMBER" }, "principal_classification": "ROLE" } ], "rule": { "n_out_of": { "n": 1, "rules": [ { "signed_by": 0 } ] } }, "version": 0 } }, "version": "0" } }, "values": { "MSP": { "mod_policy": "Admins", "value": { "config": { "admins": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDakNDQWJDZ0F3SUJBZ0lRSFNTTnIyMWRLTTB6THZ0dEdoQnpMVEFLQmdncWhrak9QUVFEQWpCcE1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVVNQklHQTFVRUNoTUxaWGhoYlhCc1pTNWpiMjB4RnpBVkJnTlZCQU1URG1OaExtVjRZVzF3CmJHVXVZMjl0TUI0WERURTNNVEV5T1RFNU1qUXdObG9YRFRJM01URXlOekU1TWpRd05sb3dWakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNURFZOaGJpQkdjbUZ1WTJsegpZMjh4R2pBWUJnTlZCQU1NRVVGa2JXbHVRR1Y0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJCnpqMERBUWNEUWdBRTZCTVcvY0RGUkUvakFSenV5N1BjeFQ5a3pnZitudXdwKzhzK2xia0hZd0ZpaForMWRhR3gKKzhpS1hDY0YrZ0tpcVBEQXBpZ2REOXNSeTBoTEMwQnRacU5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3RwpBMVVkRXdFQi93UUNNQUF3S3dZRFZSMGpCQ1F3SW9BZ3o3bDQ2ZXRrODU0NFJEanZENVB6YjV3TzI5N0lIMnNUCngwTjAzOHZibkpzd0NnWUlLb1pJemowRUF3SURTQUF3UlFJaEFNRTJPWXljSnVyYzhVY2hkeTA5RU50RTNFUDIKcVoxSnFTOWVCK0gxSG5FSkFpQUtXa2h5TmI0akRPS2MramJIVmgwV0YrZ3J4UlJYT1hGaEl4ei85elI3UUE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" ], "crypto_config": { "identity_identifier_hash_function": "SHA256", "signature_hash_family": "SHA2" }, "name": "OrdererMSP", "root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNMakNDQWRXZ0F3SUJBZ0lRY2cxUVZkVmU2Skd6YVU1cmxjcW4vakFLQmdncWhrak9QUVFEQWpCcE1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVVNQklHQTFVRUNoTUxaWGhoYlhCc1pTNWpiMjB4RnpBVkJnTlZCQU1URG1OaExtVjRZVzF3CmJHVXVZMjl0TUI0WERURTNNVEV5T1RFNU1qUXdObG9YRFRJM01URXlOekU1TWpRd05sb3dhVEVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNURFZOaGJpQkdjbUZ1WTJsegpZMjh4RkRBU0JnTlZCQW9UQzJWNFlXMXdiR1V1WTI5dE1SY3dGUVlEVlFRREV3NWpZUzVsZUdGdGNHeGxMbU52CmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJQTVI2MGdCcVJham9hS0U1TExRYjRIb28wN3QKYTRuM21Ncy9NRGloQVQ5YUN4UGZBcDM5SS8wMmwvZ2xiMTdCcEtxZGpGd0JKZHNuMVN6ZnQ3NlZkTitqWHpCZApNQTRHQTFVZER3RUIvd1FFQXdJQnBqQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1BOEdBMVVkRXdFQi93UUZNQU1CCkFmOHdLUVlEVlIwT0JDSUVJTSs1ZU9uclpQT2VPRVE0N3crVDgyK2NEdHZleUI5ckU4ZERkTi9MMjV5Yk1Bb0cKQ0NxR1NNNDlCQU1DQTBjQU1FUUNJQVB6SGNOUmQ2a3QxSEdpWEFDclFTM0grL3R5NmcvVFpJa1pTeXIybmdLNQpBaUJnb1BVTTEwTHNsMVFtb2dlbFBjblZGZjJoODBXR2I3NGRIS2tzVFJKUkx3PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=" ], "tls_root_certs": [ "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNORENDQWR1Z0F3SUJBZ0lRYWJ5SUl6cldtUFNzSjJacisvRVpXVEFLQmdncWhrak9QUVFEQWpCc01Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVVNQklHQTFVRUNoTUxaWGhoYlhCc1pTNWpiMjB4R2pBWUJnTlZCQU1URVhSc2MyTmhMbVY0CllXMXdiR1V1WTI5dE1CNFhEVEUzTVRFeU9URTVNalF3TmxvWERUSTNNVEV5TnpFNU1qUXdObG93YkRFTE1Ba0cKQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERWTmhiaUJHY21GdQpZMmx6WTI4eEZEQVNCZ05WQkFvVEMyVjRZVzF3YkdVdVkyOXRNUm93R0FZRFZRUURFeEYwYkhOallTNWxlR0Z0CmNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkVZVE9mdG1rTHdiSlRNeG1aVzMKZVdqRUQ2eW1UeEhYeWFQdTM2Y1NQWDlldDZyU3Y5UFpCTGxyK3hZN1dtYlhyOHM5K3E1RDMwWHl6OEh1OWthMQpSc1dqWHpCZE1BNEdBMVVkRHdFQi93UUVBd0lCcGpBUEJnTlZIU1VFQ0RBR0JnUlZIU1VBTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0tRWURWUjBPQkNJRUlJcjduNTVjTWlUdENEYmM5UGU0RFpnZ0ZYdHV2RktTdnBNYUhzbzAKSnpFd01Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lGM1gvMGtQRkFVQzV2N25JVVh6SmI5Z3JscWxET05UeVg2QQpvcmtFVTdWb0FpQkpMbS9IUFZ0aVRHY2NldUZPZTE4SnNwd0JTZ1hxNnY1K1BobEdsbU9pWHc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" ] }, "type": 0 }, "version": "0" } }, "version": "0" } }, "mod_policy": "Admins", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "MAJORITY", "sub_policy": "Admins" } }, "version": "0" }, "BlockValidation": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Writers" } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Readers" } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Writers" } }, "version": "0" } }, "values": { "BatchSize": { "mod_policy": "Admins", "value": { "absolute_max_bytes": 103809024, "max_message_count": 10, "preferred_max_bytes": 524288 }, "version": "0" }, "BatchTimeout": { "mod_policy": "Admins", "value": { "timeout": "2s" }, "version": "0" }, "ChannelRestrictions": { "mod_policy": "Admins", "version": "0" }, "ConsensusType": { "mod_policy": "Admins", "value": { "type": "solo" }, "version": "0" } }, "version": "0" } }, "mod_policy": "", "policies": { "Admins": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "MAJORITY", "sub_policy": "Admins" } }, "version": "0" }, "Readers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Readers" } }, "version": "0" }, "Writers": { "mod_policy": "Admins", "policy": { "type": 3, "value": { "rule": "ANY", "sub_policy": "Writers" } }, "version": "0" } }, "values": { "BlockDataHashingStructure": { "mod_policy": "Admins", "value": { "width": 4294967295 }, "version": "0" }, "Consortium": { "mod_policy": "Admins", "value": { "name": "SampleConsortium" }, "version": "0" }, "HashingAlgorithm": { "mod_policy": "Admins", "value": { "name": "SHA256" }, "version": "0" }, "OrdererAddresses": { "mod_policy": "/Channel/Orderer/Admins", "value": { "addresses": [ "orderer.example.com:7050" ] }, "version": "0" } }, "version": "0" }, "sequence": "3", "type": 0 } ```在这种格式下,一个配置看起来很吓人,但一旦你研究了它,你就会发现它是有逻辑结构的。
除了策略的定义 – 定义谁可以在通道级别做某些事情,及谁有权限更改谁可以修改配置 – 通道还 有其他一些特性可以使用配置更新进行修改。向通道添加组织 将带你经过一个最重要的过程 – 将一个组织加入通道。一些其他的可能通过配置更新修改的包括:
批处理大小. 这些参数决定了一个区块中交易的数量和大小。没有区块会大于
absolute_max_bytes
的大小或有比max_message_count
更多的交易在区块中。如果有可能在preferred_max_bytes
之下构建一个区块,那么区块将被提早分割,而大于此大小的交易将出现在它们自己的区块中。{ "absolute_max_bytes": 102760448, "max_message_count": 10, "preferred_max_bytes": 524288 }
批处理超时. 自一个交易到达后,在分割区块前,等待另外交易的时间量。降低这个值会 改善延迟,但降低太多将因为不允许区块填充到其最大容量而减少吞吐量。
{ "timeout": "2s" }
通道限制. 排序节点愿意分配的通道总数量被定义为 max_count。这主要用于具有弱联盟
ChannelCreation
策略的预生产环境。{ "max_count":1000 }
通道创建策略. 定义策略值,该值将被用来设置联盟中定义的新通道的 Application 组的 mod_policy。附加到通道创建请求中的签名集将根据策略在新通道中的实例化进行检查,以确保通道 创建是经过授权的。注意这个配置值仅在排序系统通道中设置。
{ "type": 3, "value": { "rule": "ANY", "sub_policy": "Admins" } }
Kafka 代理. 当
ConsensusType
设置为kafka
时,brokers
列表将遍历 Kafka 代理的某些子集(最好是全部),供排序节点在启动时进行初始连接。注意,共识类型一旦建立 (在创世区块的启动过程中),就不可能更改。{ "brokers": [ "kafka0:9092", "kafka1:9092", "kafka2:9092", "kafka3:9092" ] }
锚节点定义. 为每个组织定义锚节点位置。
{ "host": "peer0.org2.example.com", "port": 9051 }
哈希结构. 区块数据是字节数组的数组。区块数据的哈希值用默克尔树进行计算。这个值 定义了默克尔树的宽度。目前,此值固定为
4294967295
,它对应于区块字节数据的串联的简单 直接的哈希。{ "width": 4294967295 }
哈希算法. 这个算法被用来计算将要被编码进区块链中的区块的哈希值。 尤其,这会影响 数据哈希,和区块的前一区块哈希字段。注意,这个字段当前仅有一个合法的值(
SHA256
),而 且不应被改变。{ "name": "SHA256" }
区块验证. 这个策略指定了一个区块被视为有效的签名需求。默认情况下,它需要一个来自 排序组织中的一些成员的签名。
{ "type": 3, "value": { "rule": "ANY", "sub_policy": "Writers" } }
排序节点地址. 一个地址列表,客户端可以用来调用排序节点
Broadcast
和Deliver
功能。对等节点在这些地址中随机地选择,并在它们之间应用故障转移来获取区块。{ "addresses": [ "orderer.example.com:7050" ] }
正如我们通过添加它们的 MSP 等相关信息来添加一个组织一样,你也可以通过反转这个过程来删除它们。
注意 一旦共识类型被定义且网络已经被启动,就不可能再通过配置更新来改变它了。
还有另一个重要的通道配置(特别是对于 v1.1)被称为能力需求. 它有自己的文档在这里。
假如你想编辑通道的区块批处理大小(因为这是一个单独的数值字段,它是一个最容易做的修改)。首先 为了方便引用 JSON 路径,我们把它定义为一个环境变量。
要确定这点,查看你的配置,找到你要查找的内容,然后反向跟踪路径。
如果你找到了批处理大小,比如,你会发现那是一个 Orderer
的 value
。Orderer
在
channel_group
之下的 groups
之下。批处理大小值在 value
下有一个参数
max_message_count
。
形成路径如下:
export MAXBATCHSIZEPATH=".channel_group.groups.Orderer.values.BatchSize.value.max_message_count"
接着,显示属性的值如下:
jq "$MAXBATCHSIZEPATH" config.json
这应该返回一个值 10
(在我们的示例网络里如此)。
现在,让我们设置新的批处理大小并显示新的值:
jq “$MAXBATCHSIZEPATH = 20” config.json > modified_config.json
jq “$MAXBATCHSIZEPATH” modified_config.json
一旦你修改了 JSON,它就可以被转换并提交。向通道添加组织 里的脚本和步骤会引导你完成转换 JSON 的过程,现在让我们来看看提交的过程。
获取必需的签名¶
一旦你成功地生成了原型文件,就可以签名了。为完成这个,你需要知道将要修改的东西的相关的策略。
默认情况下,编辑如下配置:
- 特定组织 (例如,改变锚节点) 仅需要该组织的管理员签名。
- 应用 (例如,谁是组织成员)需要应用组织里的多数管理员的签名。
- 排序节点 需要排序节点组织里多数管理员的签名(默认为1)。
- 顶级
channel
组 同时需要应用组织、及排序节点组织里多数管理员的同意。
如果你已经修改了通道的默认策略,你需要相应地计算签名要求。
注意:你可能会编写收集签名的脚本,这取决于你的应用程序。一般来说,你可能总收集比需求的多的签名。
收集签名的真实过程取决于你如何设置你的系统,但有两种主要的实现。当前,Fabric 命令行默认 使用“传递”系统。就是说,提出配置更新的组织的管理员将更新发送给需要签名的其他人(如另 一个管理员)。这个管理员对之签名(或不签名)并把它传递给下一个管理员,以此类推,直到有足 够可以提交配置的签名。
这有一个简单的优点 – 当有了足够的签名时,最后一个管理员可以简单地提交配置交易(在 Fabric
里,peer channel update
命令默认地包含签名)。但是,这个过程只适应于较小的通道,因为
“传递”方法可能会很耗时。
另一个选项是将更新提交给通道中每个管理员并等待返回足够的签名。这些签名可以被集中在一起提交。 这使得创建配置更新的管理员的工作更加困难(强制他们处理每个签名者的一个文件),但对于正在开发 Fabric 管理应用程序的用户来说,这是推荐的工作流程。
一旦配置被加入账本,最好将之拉取并转换为 JSON 以确认所有内容添加正确。这也将作为最新配置的 有用的副本。
成员服务提供者 (MSP)¶
本文档将详细说明MSP的建立并提供MSP的最佳实践。
成员服务提供者(MSP)旨在提供抽象的成员操作架构。
具体的,MSP将分发、验证证书以及用户验证背后的所有加密机制和协议抽象出来。 MSP可以定义它们自己的身份概念,同样还可以定义管理(身份验证)和认证(签名生成和验证)这些身份的规则。
一个 Hyperledger Fabric区块链网络可以由一个或多个MSP管理。这提供了成员操作的模块化和不同成员标准和架构之间的互操作性。
此文档的剩余部分将详述MSP在Hyperledger Fabric的建立过程,然后讨论关于其使用的最佳实践。
MSP配置¶
为了建立一个MSP实体,每个peer和orderer需要指定其本地的配置文件(为了使peer和orderer可以进行签名), 也为了在通道上使peer、orderer和client进行身份验证和通道成员之间的签名验证(认证)。
首先,每个MSP必须指定一个名字以便该MSP在网络内被引用(例如 msp1
, org2
, 以及 org3.divA
)。
这是一个可以表述其所代表的在通道中联盟、组织或组织部门的名称。
这个名称也被称为 MSP Identifier 或 MSP ID。每个MSP的MSPID必须是唯一的。
例如,如果在系统通道建立时发现两个MSP的MSPID相同,orderer的建立将失败。
在默认的MSP实现中,需指定一些参数来允许身份(证书)验证和签名验证。这些参数从这里导出: RFC5280 ,并包括:
- 一个自签名(X.509)证书列表来组成信任根(root of trust)
- 一个X.509证书列表来代表证书验证时需要考虑的中间证书,这些证书应该由某一个信任根颁发;中间证书是可选的参数
- 一个X.509证书列表,并拥有从某一信任根起可验证的证书路径,来代表该MSP的管理员证书;拥有管理员证书则代表拥有申请改变该MSP配置的权力(例如,根CA、中间CA)
- 一个组织单位列表,此列表应出现在该MSP的有效成员的X.509证书中;这是一个可选的配置参数,举例来说,可用于多组织使用相同信任根和中间CA,并给其成员预留OU信息
- 一个证书撤销列表(CRLs),其中每一个对应一个列出的(根或中间)MSP CA;这是一个可选参数
- 一个自签(X.509)证书列表,用来组成TLS证书的信任根(TLS root of trust)
- 一个X.509证书列表来代表证书验证时需要考虑的TLS中间证书,这些证书应该由某一个TLS信任根颁发;TLS中间证书是可选的参数
该MSP的 有效的 身份需满足如下条件:
- 它们以X.509证书的形式存在,并拥有从某一信任根起可验证的证书路径;
- 它们不在任何证书撤销列表(CRL)中;
- 它们在其X.509证书结构的
OU
域中 列举 MSP配置中的一个或多个组织单位(OU)
更多关于当前MSP实现中身份认证的信息,我们建议读者阅读文档 MSP Identity Validity Rules
除了认证相关的参数以外,为了使MSP启用对其进行实例化的节点进行签名或身份验证,需指定:
- 用于节点签名的签名密钥(当前只支持ECDSA密钥)
- 节点的X.509证书,这是在MSP的验证参数下一个有效的标识
值得注意的是MSP身份不会过期;它们只能被撤销(添加进证书撤销列表CRLs)。此外,目前没有支持TLS证书的撤销。
如何生成MSP证书以及它们的签名密钥?¶
为了生成MSP配置所需的X.509证书,可以使用 Openssl 。需要强调的是Hyperledger Fabric不支持包含RSA密钥的证书。
另外也可以用 cryptogen
工具,它相关的操作请查看文档 入门
Hyperledger Fabric CA 也可以用来生成配置MSP的证书和密钥。
在Peer和Orderer端建立MSP¶
为了建立(Peer或Orderer的)本地MSP,管理员应当建立目录(例如,$MY_PATH/mspconfig
),其中包含一个文件和八个子目录:
- 一个
admincerts
目录,其中包含PEM文件,每个PEM文件对应一个管理员证书 - 一个
cacerts
目录,其中包含PEM文件,每个PEM文件对应一个根CA证书 - (可选的)一个
intermediatecerts
目录,其中包含PEM文件,每个PEM文件对应一个中间CA证书 - (可选的)一个文件
config.yaml
,用来配置所支持的组织单位(OU)和身份分类(参见下面对应的部分) - (可选的)一个
crls
目录,包含证书撤销列表(CRLs) - 一个
keystore
目录,包含一个PEM文件,代表该节点的签名密钥,我们强调当前不支持RSA的密钥形式 - 一个
signcerts
目录,包含一个PEM文件,代表该节点的X.509证书 - (可选的)一个
tlscacerts
目录,其中包含PEM文件,每个PEM文件对应一个TLS根CA证书 - (可选的)一个
tlsintermediatecerts
目录,其中包含PEM文件,每个PEM文件对应一个TLS中间CA证书
在节点的配置文件(对Peer来说是core.yaml,对Orderer来说是orderer.yaml)中,必须指定mspconfig目录的路径和节点MSP的MSPID。
mspconfig目录的路径应该是环境变量FABRIC_CFG_PATH的相对路径,并且是Peer端 mspConfigPath
对应的参数,或是Orderer端 LocalMSPDir
对应的参数。
节点的MSPID由Peer端 localMspId
指定,或由Orderer端 LocalMSPID
指定。这些变量可以被环境变量重写,在Peer端使用CORE前缀(例如,CORE_PEER_LOCALMSPID),在Orderer端使用ORDERER前缀(例如,ORDERER_GENERAL_LOCALMSPID)。
值得一提的是,在Orderer建立阶段,需要生成并提供给Orderer系统通道的创世块。创世块中所需的MSP配置信息将在下部分详细说明。
如果想要 重新配置 一个 “本地” MSP,目前只能手动操作,并且Peer或Orderer需要重启。在后续版本我们计划提供在线/动态的重新配置方式(例如,不需要中止节点,使用一个受节点管理的系统链码)。
组织单位(OU)¶
为了配置在该MSP有效用户的证书中的OU列表,config.yaml
文件需指定组织单位标识。例如:
OrganizationalUnitIdentifiers:
- Certificate: "cacerts/cacert1.pem"
OrganizationalUnitIdentifier: "commercial"
- Certificate: "cacerts/cacert2.pem"
OrganizationalUnitIdentifier: "administrators"
上面的例子声明了两个组织单位标识:commercial 和 administrators。
如果MSP拥有至少其中一个组织单位标识,它才是有效的。
Certificate
域代表拥有有效标识应具有的CA证书或中间CA证书路径。
该路径是相对MSP根目录的,并且不能为空。
身份分类¶
MSP的默认实现可以进一步将身份分类成clients、peers,这是基于他们X.509证书的OU字段。
如果一个身份有提出(submit)交易,或向peer查询(query)等操作,它应被归为 client。
如果一个身份有背书(endorse)或提交(commit)交易,它应被归为 peer。
为了给一个MSP定义client和peer,需要适当的设置 config.yaml
,例如:
NodeOUs:
Enable: true
ClientOUIdentifier:
Certificate: "cacerts/cacert.pem"
OrganizationalUnitIdentifier: "client"
PeerOUIdentifier:
Certificate: "cacerts/cacert.pem"
OrganizationalUnitIdentifier: "peer"
从上面可以看到,将 NodeOUs.Enable
设为 true
来开启身份分类。
通过设置 NodeOUs.ClientOUIdentifier
(或 NodeOUs.PeerOUIdentifier
)的属性来定义client(或peer)的身份:
OrganizationalUnitIdentifier
: 将其设置成与clinet(或peer)的X.509证书所包含的OU匹配。Certificate
: 将其设置为指定的client(或peer)的CA证书或中间CA证书路径来得到验证。该路径是相对MSP根目录的。如果该路径为空,说明该身份的X.509证书可以被任何MSP配置中的CA所验证。
当身份分类是开启状态时,MSP的管理员们需要成为该MSP的client,这意味着他们的X.509证书需要具有能标识client的OU。 值得注意的是,一个身份只能是client或peer其中之一。两个分类是互斥的。如果一个身份既不是client也不是peer,那么验证将失败。
最后,需注意的是对于已升级的环境,需要开启1.1版的channel capability才能使用身份分类。
通道MSP建立¶
在系统创世阶段,需要指定出现在网络中的所有MSP的验证参数,并保存到系统通道的创世块。 回顾一下,MSP验证参数包含MSP标识、根证书列表、中间CA证书和管理员证书列表、OU信息和证书撤销列表CRLs。 在Orderer建立阶段,系统创世块将被提供给Orderer,使Orderer可以认证通道建立请求。 如果系统创世块包含有两个相同标识的MSP,Orderer将拒绝该创世块,从而导致网络启动失败。
对于应用通道,通道的创世块只需包含通道管理者的MSP验证信息。 需强调的是,在将peer加入通道之前保证通道创世块(或最近的配置块)包含正确的MSP配置信息是 应用自己的责任。
当使用configtxgen工具启动通道时,可以通过将MSP验证参数包含进mspconfig目录
并在 configtx.yaml
相应部分设置其路径的方式配置通道MSP。
通道上MSP的 重新配置,MSP管理员证书的持有者在创建 config_update
事务时,
将声明与该MSP的已获得CA的证书相关的证书撤销列表。随后被管理员控制的客户端应用将在MSP出现的通道上声明这次update。
最佳实践¶
在这部分我们将详细说明对MSP配置的通用场景下的最佳实践
1) 组织/企业 和 MSP 之间的映射
我们建议组织和MSP之间是一对一映射的。 如果要使用其他类型的映射,需考虑以下情况:
- 一个组织使用多个MSP. 这对应的情况是一个组织有多个部门,每个MSP代表一个部门, 出现这种情况可以是独立管理的原因,也可能出于隐私考虑。 在这种情况下,一个peer节点只能被单一MSP拥有, 并且不能将其他MSP下peer识别成同组织的peer。 这意味着peer节点可以通过gossip组织域将数据分享给同部门内的其他peer节点, 但不能分享给组成实际组织的全体。
- 多组织使用一个MSP. 这对应的情况是多个组织组成联盟,每个组织都被类似的成员架构管理。 要知道,不论是否属于同一实际组织,peer的组织域消息将传播给同MSP下的其他peer节点。 这将限制MSP定义和(或)peer配置的粒度。
2) 一个组织有不同分部(组织单元),想要授予不同通道访问权
两种处理方法:
- 定义一个可以容纳所有组织成员的MSP.
该MSP的配置将由根CA、中间CA和管理员证书列表;
以及成员标识包括成员所属的组织单元(
OU
)组成。 随后定义策略来捕获某一特定OU
的成员, 这些策略将组成通道的读/写策略或链码的背书策略。 这种方法的局限性是gossip peer节点将把拥有和其相同成员标识的peer当成同组织成员, 并因此与它们传播组织域信息(例如状态信息)。 - 给每一个分部定义一个MSP. 这涉及到给每个分部指定一组证书,包含根CA证书、中间CA证书和管理员证书, 这样能够做到MSP之间没有重复的证书路径。 这意味着,每个分部采用不同的中间CA。 这么做的缺点是需要管理多个MSP,但是确实绕开了上面方法出现的问题。 我们也可以使用MSP配置里的OU扩展项来实现对每个分部定义一个MSP。
3) 区分同一组织下的client和peer
在很多情况下,会要求一个身份的 “type” 是可以被检索的 (例如,可能有需求要求背书必须由peer节点提供,不能是client或单独的orderer节点)。
对这种要求的支持是有限的。
实现这种区分的一种方式是为每种节点类型创建单独的中间CA, 一个给client,一个给peer或orderer,并分别配置两个不同的MSP。 组织加入到的通道需要同时包含两个MSP,但背书策略只部署在peer的MSP。 这将最终导致组织被映射到两个MSP实例,并且对peer和client的交互产生一些后果。
由于同一组织的所有peer还是属于同一个MSP,Gossip不会被严重的影响。 Peer可以基于本地MSP策略来约束特定系统链码的执行。 例如,peer可以只执行 “joinChannel”请求, 如果这个请求是被一个只能是client的本地MSP的管理员签名的(终端用户应该是请求的起点)。 我们可以绕过这个矛盾,只要我们接受该MSP的管理员是该peer/orderer的唯一client。
这种方法要考虑的另一个点是peer基于请求发起者本地MSP的资格来授权事件注册请求。 很明显,由于请求发起者是一个client,它经常被当作是属于与该peer不同的MSP, 因此peer将拒绝请求。
4) 管理员和CA证书
将MSP管理员证书设成与该MSP的 root of trust
或中间CA的证书不同非常重要。
将管理成员组件和分发新和(或)验证证书的职责分开是常规(安全的)做法。
5) 将一个中间CA列入黑名单
前面提到,可以通过重新配置机制(对本地MSP实例手动重新配置,
并对通道的MSP适当的构建 config_update
消息)对MSP进行重新配置。
很明显,有两种方式将一个中间CA列入黑名单:
- 重新配置MSP,使其中间CA证书列表不再包含该中间CA。对本地已配置的MSP来说,
这意味着这个CA的证书将从
intermediatecerts
目录移除。 - 重新配置MSP,使其包含一个由信任根颁发的证书撤销列表,该列表包含提到的中间CA的证书。
当前的MSP实现中,我们只支持方式(1),因为其更简单,并且不要求将不再考虑的中间CA列入黑名单。
6) CA和TLS CA
MSP身份的根CA和MSP TLS根CA(以及相关的中间CA)需要在不同的目录被定义。 这是为了避免不同类证书之间产生混淆。虽然没有禁止MSP身份和TLS证书使用相同的CA, 但这里建议避免在生成环境这样做。
通道配置(configtx)¶
Hyperledger Fabric 区块链网络的共享配置存储在一个集合配置交易中,每个通道一个。 配置交易通常简称为 configtx。
通道配置有以下重要特性:
- 版本化:配置中的所有元素都有一个关联的、每次修改都提高的版本。此外,每个 提交的配置都有一个序列号。
- 许可的:配置中的所有元素都有一个关联的、控制对此元素的修改是否被允许的策略。 任何拥有之前 configtx 副本(没有附加信息)的人均可以这些策略为基础来验证新配置 的有效性。
- 分层的:根配置组包含子组,而且层次结构中的每个组都有关联的值和策略。这些 策略可以利用层次结构从较低层级的策略派生出一个层级的策略。
配置解析¶
配置,作为一个类型为 HeaderType_CONFIG
的交易,被存储在一个没有其他交易的
区块中。这些区块被称为*配置区块*,其中的第一个就是*创世区块*。
配置的原型结构在 fabric/protos/common/configtx.proto
中。类型为
HeaderType_CONFIG
的信封将 ConfigEnvelope
消息编码为
Payload
data
字段。ConfigEnvelope
的原型定义如下:
message ConfigEnvelope {
Config config = 1;
Envelope last_update = 2;
}
last_update
字段在下面的 配置更新 一节定义,但只有当验证配置时才是必需的,
读取时则不是。当前提交的配置存储在 config
字段,包含 Config
消息。
message Config {
uint64 sequence = 1;
ConfigGroup channel_group = 2;
}
sequence
数字每次提交配置时增加1。channel_group
字段是包含配置的根组。
ConfigGroup
结构是递归定义的,并构建了一个组的树,每个组都包含值和策略。其
定义如下:
message ConfigGroup {
uint64 version = 1;
map<string,ConfigGroup> groups = 2;
map<string,ConfigValue> values = 3;
map<string,ConfigPolicy> policies = 4;
string mod_policy = 5;
}
因为 ConfigGroup
是递归结构,所以它有层次结构。为清楚起见,下面的例子使用
golang 展现。
// Assume the following groups are defined
var root, child1, child2, grandChild1, grandChild2, grandChild3 *ConfigGroup
// Set the following values
root.Groups["child1"] = child1
root.Groups["child2"] = child2
child1.Groups["grandChild1"] = grandChild1
child2.Groups["grandChild2"] = grandChild2
child2.Groups["grandChild3"] = grandChild3
// The resulting config structure of groups looks like:
// root:
// child1:
// grandChild1
// child2:
// grandChild2
// grandChild3
每个组定义了配置层次中的一个级别,每个组都有关联的一组值(按字符串键索引)和策略 (也按字符串键索引)。
值定义:
message ConfigValue {
uint64 version = 1;
bytes value = 2;
string mod_policy = 3;
}
策略定义:
message ConfigPolicy {
uint64 version = 1;
Policy policy = 2;
string mod_policy = 3;
}
注意,值、策略和组都有 version
和 mod_policy
。一个元素每次修改时
version
都会增长。 mod_policy
被用来控制修改元素时所需要的签名。
对于组,修改就是增加或删除值、策略或组映射中的元素(或改变 mod_policy
)。
对于值和策略,修改就是分别改变值和策略字段(或改变 mod_policy
)。每个元素
的 mod_policy
都在当前层级配置的上下文中被评估。下面是一个定义在
Channel.Groups["Application"]
中的修改策略示例(这里,我们使用 golang map
语法,所以,Channel.Groups["Application"].Policies["policy1"]
表示根组
Channel
的子组 Application
的 Policies
中的 policy1
所对应的
策略。)
policy1
对应Channel.Groups["Application"].Policies["policy1"]
Org1/policy2
对应Channel.Groups["Application"].Groups["Org1"].Policies["policy2"]
/Channel/policy3
对应Channel.Policies["policy3"]
注意,如果一个 mod_policy
引用了一个不存在的策略,那么该元素不可修改。
配置更新¶
配置更新被作为一个类型为 HeaderType_CONFIG_UPDATE
的 Envelope
消息提交。
交易中的 Payload
data
是一个封送的 ConfigUpdateEnvelope
。
ConfigUpdateEnvelope
定义如下:
message ConfigUpdateEnvelope {
bytes config_update = 1;
repeated ConfigSignature signatures = 2;
}
signatures
字段包含一组授权配置更新的签名。它的消息定义如下:
message ConfigSignature {
bytes signature_header = 1;
bytes signature = 2;
}
signature_header
是为标准交易定义的,而签名是通过 signature_header
字节
和 ConfigUpdateEnvelope
中的 config_update
字节串联而得。
ConfigUpdateEnvelope
config_update
字节是封送的 ConfigUpdate
消息,定义如下:
message ConfigUpdate {
string channel_id = 1;
ConfigGroup read_set = 2;
ConfigGroup write_set = 3;
}
channel_id
是更新所绑定的通道 ID,这对于确定支持此重配置的签名的作用域
是必需的。
read_set
定义了现有配置的子集,属稀疏指定,其中只设置 version
字段,
其他字段不需要填充。尤其 ConfigValue
value
或者 ConfigPolicy
policy
字段不应在 read_set
中设置。ConfigGroup
可以有已填充
映射字段的子集,以便引用配置树中更深层次的元素。例如,要将 Application
组
包含在 read-set
中,其父组(Channel
组)也必须包含在读集合中,但
Channel
组不需要填充所有键,例如 Orderer
group
键,或任何
values
或 policies
键。
write_set
指定了要修改的配置片段。由于配置的层次性,对层次结构中深层元素
的写入也必须在其 write_set
中包含更高级别的元素。但是,对于 read-set
中也指定的 write-set
中的任何同一版本的元素,应该像在 ``read-set``中一样
稀疏地指定该元素。
例如,给定配置:
Channel: (version 0)
Orderer (version 0)
Application (version 3)
Org1 (version 2)
为了提交一个修改 Org1
的配置更新,read_set
应如:
Channel: (version 0)
Application: (version 3)
write_set
应如
Channel: (version 0)
Application: (version 3)
Org1 (version 3)
收到 CONFIG_UPDATE
后,排序节点按以下步骤计算 CONFIG
结果。
- 验证
channel_id
和read_set
。read_set
中的所有元素都必须 以给定的版本存在。 - 收集
write_set
中的所有与read_set
版本不一致的元素以计算更新集。 - 校验更新集合中版本号刚好增长了1的每个元素。
- 校验附加到
ConfigUpdateEnvelope
的签名集是否满足更新集中每个元素的mod_policy
。 - 通过应用更新到到当前配置,计算出配置的新的完整版本。
- 将配置写入
ConfigEnvelope
,包含作为last_update
字段的CONFIG_UPDATE
,和编码为config
字段的新配置, 以及递增的sequence
值。 - 将新
ConfigEnvelope
写入类型为CONFIG
的Envelope
,并最终将其 作为唯一交易写入一个新的配置区块。
当节点(或其他任何 Deliver
的接收者)收到这个配置区块时,它应该,将
last_update
消息应用到当前配置并校验经过排序计算的 config
字段包含
当前的新配置,以此来校验这个配置是否得到了适当地验证。
组和值的许可更新¶
任何有效配置都是以下配置的子集。在这里,我们用符号 peer.<MSG>
来定义一个
ConfigValue
,其 value
字段是一个封送的名为 <MSG>
的消息。它定义在
fabric/protos/peer/configuration.proto
中。符号 common.<MSG>
、
msp.<MSG>
和 orderer.<MSG>
类似对应,它们的消息依次定义在
fabric/protos/common/configuration.proto
、
fabric/protos/msp/mspconfig.proto
和
``fabric/protos/orderer/configuration.proto``中
注意,键 {{org_name}}
和 {{consortium_name}}
表示任意名称,指示一个
可以用不同名称重复的元素。
&ConfigGroup{
Groups: map<string, *ConfigGroup> {
"Application":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
"AnchorPeers":peer.AnchorPeers,
},
},
},
},
"Orderer":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
},
},
},
Values:map<string, *ConfigValue> {
"ConsensusType":orderer.ConsensusType,
"BatchSize":orderer.BatchSize,
"BatchTimeout":orderer.BatchTimeout,
"KafkaBrokers":orderer.KafkaBrokers,
},
},
"Consortiums":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{consortium_name}}:&ConfigGroup{
Groups:map<string, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
},
},
},
Values:map<string, *ConfigValue> {
"ChannelCreationPolicy":common.Policy,
}
},
},
},
},
Values: map<string, *ConfigValue> {
"HashingAlgorithm":common.HashingAlgorithm,
"BlockHashingDataStructure":common.BlockDataHashingStructure,
"Consortium":common.Consortium,
"OrdererAddresses":common.OrdererAddresses,
},
}
排序系统通道配置¶
排序系统通道需要定义一些排序参数,以及创建通道的联盟。一个排序服务有且只能有一个 排序系统通道,它是需要创建的第一个通道(或更准确地说是启动)。建议不要在排序系统 通道的创世配置中定义应用,但在测试时是可以的。注意,任何对排序系统通道具有读权限 的成员可能看到所有的通道创建,所以,这个通道的访问应用受到限制。
排序参数被定义在如下配置子集中:
&ConfigGroup{
Groups: map<string, *ConfigGroup> {
"Orderer":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
},
},
},
Values:map<string, *ConfigValue> {
"ConsensusType":orderer.ConsensusType,
"BatchSize":orderer.BatchSize,
"BatchTimeout":orderer.BatchTimeout,
"KafkaBrokers":orderer.KafkaBrokers,
},
},
},
参与排序的每个组织在 Order
组下都有一个组元素。此组定义单个参数 MSP
,
其中包含该组织的加密身份信息。 Order
组的 Values
决定了排序节点的工作
方式。它们在每个通道中存在,因此,例如 orderer.BatchTimeout
可能在不同通
道上被不同地指定。
在启动时,排序节点将面临一个包含了很通道信息的文件系统。排序节点通过识别带有定义的 联盟组的通道来识别系统通道。联盟组的结构如下。
&ConfigGroup{
Groups: map<string, *ConfigGroup> {
"Consortiums":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{consortium_name}}:&ConfigGroup{
Groups:map<string, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
},
},
},
Values:map<string, *ConfigValue> {
"ChannelCreationPolicy":common.Policy,
}
},
},
},
},
},
注意,每个联盟定义一组成员,正如排序组织里的组织成员一样。每个联盟也定义一个
ChannelCreationPolicy
。这是一个应用于授权通道创建请求的策略。通常,该值
将被设置为一个 ImplicitMetaPolicy
,并要求通道的新成员签名以授权通道创建。
更多关于通道创建的细节,请参见下文。
应用通道配置¶
应用配置适用于为应用类型交易而设计的通道。它定义如下:
&ConfigGroup{
Groups: map<string, *ConfigGroup> {
"Application":&ConfigGroup{
Groups:map<String, *ConfigGroup> {
{{org_name}}:&ConfigGroup{
Values:map<string, *ConfigValue>{
"MSP":msp.MSPConfig,
"AnchorPeers":peer.AnchorPeers,
},
},
},
},
},
}
正如 Orderer
部分,每个组织被编码为组。但是,并非仅仅编码 MSP
身份信息,
每个组织额外编码一个 AnchorPeers
列表。这个列表允许不同组织的节点互相联系以
建立对等 gossip 网络。
应用通道对排序组织副本和共识选项进行编码,以允许对这些参数进行确定的更新,因此排序
系统通道配置中相同的 Orderer
部分也被包括在内。但从应用程序角度来看,这在很大
程度上可能被忽略。
通道创建¶
当排序节点收到一个尚不存在的通道的 CONFIG_UPDATE
时,排序节点假定这是一个通道
创建请求并执行以下内容。
- 排序节点识别将为之执行通道创建请求的联盟。它通过查看顶级组的
Consortium
值 来完成这一操作。 - 排序节点校验:包含在
Application
组中的组织是包含在对应联盟中的组织的一个子集, 并且ApplicationGroup
version
被设为1
。 - 排序节点校验:如果联盟有成员,那么新通道也要有应用成员(创建没有成员的联盟和通道仅 用于测试)。
- 排序节点从排序系统通道中获取
Orderer
组,使用新指定的成员建立Application
组,并按联盟配置中指定的ChannelCreationPolicy
指定它的mod_policy
, 以此建立模板配置。注意,策略会在新配置的上下文被评估,所以,一个要求ALL
成员 的策略,会要求新通道所有成员的签名,而不是联盟所有成员的签名。 - 然后排序节点将
CONFIG_UPDATE
作为一个更新应用到这个模板配置。 因为CONFIG_UPDATE
将修改应用到Application
组(它的version
是1
),配置代码按ChannelCreationPolicy
验证这些更新。如果通道的创 建包含任何其他修改,比如个别组织的锚节点,则这个元素相应的修改策略也会被调用。 - 带有新通道配置的
CONFIG
交易被封装并发送给的排序系统通道以进行排序,排序后, 通道被创建了。
背书策略¶
每个链码都有背书策略,指定通道上的一组 Peer 必须执行链码,并且为执行结果进行背书,证明交易是有效的。这些背书策略指定了必须为 Proposal 进行背书的组织。
注解
回想 state, 通过键值对表示, 从区块链数据中分离开来。更多信息请查看 账本 文档。
作为 Peer 进行交易验证的一部分,每个 Peer 的检查确保了交易保存了合适 数量 的背书,并且是指定背书节点的背书。这些背书结果的检查,同样确保了它们是有效的(比如,从有效的证书得到的有效签名)。
需要背书的2种方式¶
默认的, 在某个通道上实例化或升级链码时会指定背书策略(即,一个背书策略会覆盖和某个链码关联的所有状态)。
然而,对于某些特殊的状态(特殊的键值对)场景下,需要不同的背书策略。**基于状态的背书策略**允许对特殊的 Key 覆盖链码默认的背书策略。
为了解释哪些情况下使用这两种背书策略,请考虑在通道上实现汽车交易的情况。创建(也称为“发行”)一辆汽车作为可交易的资产(即把一个键值对存到世界状态)需要满足链码级别的背书策略。下文将详细介绍链码级别的背书策略。
如果汽车需要特殊的背书策略,该背书策略可以在汽车创建或之后被定义。当下有很多为什么需要特定状态的背书策略的原因,汽车可能具有历史重要性或价值,因此有必要获得持牌估价师的认可。还有,汽车的主人(如果他们是通道的成员)可能还希望确保他们的同伴在交易上签名。这两种情况,都需要为特殊资产指定,与当前链码默认背书策略不同的背书策略。
我们将在后面介绍如何定义基于状态的背书策略。但首先,让我们看看如何设置链式代码级的背书策略。
设置链码级背书策略¶
链码级背书策略可以在示例话时使用SDK(使用SDK的样例请查看 <https://github.com/hyperledger/fabric-sdk-node/blob/f8ffa90dc1b61a4a60a6fa25de760c647587b788/test/integration/e2e/e2eUtils.js#L178>`)或者在 Peer 的CLI里使用 -P
指定。 can be specified at
注解
现在不要担心背书策略语法 (比如 'Org1.member'
),我们后面部分会介绍。
比如:
peer chaincode instantiate -C <channelid> -n mycc -P "AND('Org1.member', 'Org2.member')"
该命令部署了链码 mycc
(“my chaincode”) 并指定背书策略
AND('Org1.member', 'Org2.member')
,它要求 Org1 和 Org2的任一成员对交易进行签名。
注意,如果开启了身份分类 (见 成员服务提供者 (MSP)), 可以使用 PEER
角色限定使用 Peer 进行背书。
比如:
peer chaincode instantiate -C <channelid> -n mycc -P "AND('Org1.peer', 'Org2.peer')"
链码实例化后,新加入的组织可以查询链码(如果查询具有通道策略级别的授权以及链码所规定的应用层检查),但不能执行和未链码进行背书。需要修改背书策略,让新组织能够确认交易。
注解
如果在实例化时未指定背书策略,背书策略默认为通道中任何组织的任何成员。比如,一个由 Org1 和 Org2 组成的通道,默认的背书策略是 “OR(‘Org1.member’, ‘Org2.member’)”。
背书策略语法¶
正如你上面所看到了,策略是使用主体来表达的(主题是跟角色匹配的)。主体可以描述为 'MSP.ROLE'
, MSP
代表了 MSP ID, ROLE
是以下4个其中之一:member
, admin
, client
, and
peer
。
以下是几个有效的主体示例:
'Org0.admin'
:Org0
MSP 的任何管理员'Org1.member'
:Org1
MSP 的任何成员'Org1.client'
:Org1
MSP 的任何客户端'Org1.peer'
:Org1
MSP 的任何 Peer
语法是:
EXPR(E[, E...])
EXPR
可以是 AND
, OR
, 或者 OutOf
, 并且 E
是一个以上语法的主体或者另外一个 EXPR
。
- 比如:
AND('Org1.member', 'Org2.member', 'Org3.member')
要求3个组织的都至少一个成员进行签名。OR('Org1.member', 'Org2.member')
要求组织1或者组织2的任一成员进行签名。OR('Org1.member', AND('Org2.member', 'Org3.member'))
要求组织1的任一成员签名,或者组织2和组织3的任一成员,分别进行签名。OutOf(1, 'Org1.member', 'Org2.member')
, 等价于``OR(‘Org1.member’, ‘Org2.member’)``。- 类似的,
OutOf(2, 'Org1.member', 'Org2.member')
等价于AND('Org1.member', 'Org2.member')
,OutOf(2, 'Org1.member', 'Org2.member', 'Org3.member')
等价于OR(AND('Org1.member', 'Org2.member'), AND('Org1.member', 'Org3.member'), AND('Org2.member', 'Org3.member'))
.
设置键级别的背书策略¶
设置背书策略跟对应的链码声明周期相关联。可以在通道实例化或者升级对应链码的时候进行设置。
对比来看, 键级别的背书策略可以在链码内更加细粒度的设置和修改。修改键级别的背书策略是常规交易读写集的一部分。
shim API提供了从常规Key设置和获取背书策略的功能。
注解
下文中的 ep
代表背书策略,它可以用上文介绍的语法所描述,或者下文介绍的函数。每种方法都会生成,可以被 shim API 接受的二进制版本的背书策略。
SetStateValidationParameter(key string, ep []byte) error
GetStateValidationParameter(key string) ([]byte, error)
对于在 Collection 中属于 私有数据 使用以下函数:
SetPrivateDataValidationParameter(collection, key string, ep []byte) error
GetPrivateDataValidationParameter(collection, key string) ([]byte, error)
为了帮助把背书策略序列化成有效的字节数组,shim提供了便利的函数供链码开发者,从组织 MSP 标示符的角度处理背书策略,详情见 键背书策略:
type KeyEndorsementPolicy interface {
// Policy returns the endorsement policy as bytes
Policy() ([]byte, error)
// AddOrgs adds the specified orgs to the list of orgs that are required
// to endorse
AddOrgs(roleType RoleType, organizations ...string) error
// DelOrgs delete the specified channel orgs from the existing key-level endorsement
// policy for this KVS key. If any org is not present, an error will be returned.
DelOrgs(organizations ...string) error
// ListOrgs returns an array of channel orgs that are required to endorse changes
ListOrgs() ([]string)
}
比如,当两个组织要求为键值的改变背书时,需要设置键背书策略,通过把 MSPIDs
传递给 AddOrgs()
然后调用 Policy()
来构建字节数组格式的背书策略,之后传递给 SetStateValidationParameter()
。
把 shim 作为链码的依赖请参考:ref:vendoring。
验证¶
commit交易时,设置键值的过程和设置键的背书策略的过程是一样的,都会更新键的状态并且使用相同的规则进行验证。
正如上面讨论的,如果一个键并改变了,并且没有键级别的背书策略,默认会使用链码级别的背书策略。设置键级别背书策略的时候,也是使用链码级背书策略,即新的键级别背书策略必须使用已存在的链码背书策略。
如果某个键被修改了,并且键级别的背书策略已经设置,键级别的背书策略就会覆盖链码背书策略。实际上,键级背书策略可以比链码背书策略宽松或者严格,因为设置键级背书策略必须满足链码背书策略,所以没有违反可信的假设。
如果某个键级背书策略被移除(或设为空),链码背书策略再次变为默认策略。
如果某个交易修改了多个键,并且这些键关联了多个键级背书策略,交易需要满足所有的键级策略才会有效。
可插拔交易背书与交易验证¶
动机¶
交易提交时会接受验证,此时节点会在执行交易本身带来的状态改变前进行以下检查:
- 对签署交易者的身份进行验证;
- 对交易上背书者的签名进行核实:
- 确认交易满足对应链码命名区间的相关背书政策。
在某些情况下,需要自定义交易验证规则,与Fabric默认的验证规则不同,例如:
- 未花费的交易输出(UTXO):验证过程要考虑交易是否没有对其输入使用两次,此时需要自定义交易验证规则。
- 匿名交易:当背书不包含节点身份,被共享的签名和公钥也无法与节点的身份联系起立时,需要自定义交易验证规则。
可插拔背书和验证逻辑¶
Fabric支持对节点实行、部署自定义的背书和验证逻辑,并实现了以可插拔方式将其与链码执行联系起来。这种逻辑既可作为内置型可选逻辑编进节点中,也可作为一个Golang插件与节点一起接受编译和部署。Golang 插件 。
默认情况下,链码将使用内置的背书和验证逻辑。不过,用户可以选择使用自定义的背书和验证插件来作为链码定义的一部分。管理员可通过自定义节点的本地配置来扩展对方可用的背书或验证逻辑。
配置¶
每个节点都有一个本地配置(core.yaml
),其中包括了背书或验证逻辑名与将进行的逻辑实现之间的映射关系。
默认的逻辑叫做 ESCC``(其中“E”代表背书)和 ``VSCC
(验证), handlers
部分的节点本地配置中包含了该默认逻辑。
handlers:
endorsers:
escc:
name: DefaultEndorsement
validators:
vscc:
name: DefaultValidation
当背书或验证的实现被编译到节点中,name
属性就代表了即将运行的初始化函数,以便获得生成背书或验证逻辑相关实例的工厂。
该函数是基于 core/handlers/library/library.go
构建的 HandlerLibrary
的实例方法。并且为添加自定义背书或验证逻辑,需要使用其他方法对该架构进行扩展。
由于这种方法十分繁琐,而且还给逻辑部署带来巨大挑战,因此用户可以通过在 name
属性下增加另一个名为 library
的属性来将自定义背书和验证部署为一个 Golang 插件。
比如,如果我们有被作为插件来实现的自定义背书和验证逻辑,那么 core.yaml
的配置中就会有以下记录:
handlers:
endorsers:
escc:
name: DefaultEndorsement
custom:
name: customEndorsement
library: /etc/hyperledger/fabric/plugins/customEndorsement.so
validators:
vscc:
name: DefaultValidation
custom:
name: customValidation
library: /etc/hyperledger/fabric/plugins/customValidation.so
并且我们需要把 .so
插件文件放置在节点的本地文件系统中。
注解
Hereafter, custom endorsement or validation logic implementation is going to be referred to as “plugins”, even if they are compiled into the peer.
背书插件的实现¶
若要实现一个背书插件,用户必须实现 core/handlers/endorsement/api/endorsement.go
中的 Plugin
界面。
// Plugin endorses a proposal response
type Plugin interface {
// Endorse signs the given payload(ProposalResponsePayload bytes), and optionally mutates it.
// Returns:
// The Endorsement: A signature over the payload, and an identity that is used to verify the signature
// The payload that was given as input (could be modified within this function)
// Or error on failure
Endorse(payload []byte, sp *peer.SignedProposal) (*peer.Endorsement, []byte, error)
// Init injects dependencies into the instance of the Plugin
Init(dependencies ...Dependency) error
}
通过让节点调用 PluginFactory
界面的 New 方法,为每个通道创建一个给定插件类型(或是通过方法名称被识别为 HandlerLibrary
的实例方法,亦或是通过插件 .so
文件路径被识别为HandlerLibrary 的实例方法)的背书插件实例,该 New
方法预计也将由插件开发人员实现。
// PluginFactory creates a new instance of a Plugin
type PluginFactory interface {
New() Plugin
}
Init
方法预计将接收在 core/handlers/endorsement/api/
中声明的所有依赖项作为输入,并将其标识为嵌入 Dependency
界面。
创建了 Plugin
实例后,节点在实例上调用 Init
方法,并且把 dependencies
作为参数来通过。
目前,Fabric 存在以下背书插件的依赖项:
SigningIdentityFetcher
:返回一个基于给定的签署提案的SigningIdentity
示例
// SigningIdentity signs messages and serializes its public identity to bytes
type SigningIdentity interface {
// Serialize returns a byte representation of this identity which is used to verify
// messages signed by this SigningIdentity
Serialize() ([]byte, error)
// Sign signs the given payload and returns a signature
Sign([]byte) ([]byte, error)
}
StateFetcher
:获取一个与世界状态交互的 状态 对象
// State defines interaction with the world state
type State interface {
// GetPrivateDataMultipleKeys gets the values for the multiple private data items in a single call
GetPrivateDataMultipleKeys(namespace, collection string, keys []string) ([][]byte, error)
// GetStateMultipleKeys gets the values for multiple keys in a single call
GetStateMultipleKeys(namespace string, keys []string) ([][]byte, error)
// GetTransientByTXID gets the values private data associated with the given txID
GetTransientByTXID(txID string) ([]*rwset.TxPvtReadWriteSet, error)
// Done releases resources occupied by the State
Done()
}
验证插件实现¶
要实现一个验证插件,用户必须实现 core/handlers/validation/api/validation.go
中的 Plugin
界面:
// Plugin validates transactions
type Plugin interface {
// Validate returns nil if the action at the given position inside the transaction
// at the given position in the given block is valid, or an error if not.
Validate(block *common.Block, namespace string, txPosition int, actionPosition int, contextData ...ContextDatum) error
// Init injects dependencies into the instance of the Plugin
Init(dependencies ...Dependency) error
}
每个 ContextDatum
都是运行时派生的额外元数据,由节点负责传递给验证插件。目前,代表链码背书政策的 ContextDatum
是唯一被传递的一个 。
// SerializedPolicy defines a serialized policy
type SerializedPolicy interface {
validation.ContextDatum
// Bytes returns the bytes of the SerializedPolicy
Bytes() []byte
}
与上述的背书插件一样,通过让节点调用 PluginFactory
接口的 New 方法,为每个通道创建一个给定插件类型(或是通过方法名称被识别为 HandlerLibrary
的实例方法,亦或是通过插件 .so
文件路径被识别为 HandlerLibrary
的实例方法)的验证插件实例,该 New 方法预计也将由插件开发人员实现。
// PluginFactory creates a new instance of a Plugin
type PluginFactory interface {
New() Plugin
}
Init
方法预计将接收在 core/handlers/validation/api/``中声明的所有依赖项作为输入,并将其标识为嵌入 ``Dependency
界面。
创建了 Plugin
实例后,节点会在实例上调用 Init 方法,并且把 dependencies 作为参数来通过。
目前,Fabric存在以下验证插件的依赖项:
IdentityDeserializer
:将身份的字节表示转换为Identity
对象,该对象可用于验证由这些身份所签署的签名,还能根据这些身份各自的成员服务提供者(MSP)来对自身进行验证,以确保它们满给定的 MSP 准则。core/handlers/validation/api/identities/identities.go
中包含了全部的规范。PolicyEvaluator
:评估被给定的策略是否满足要求:
// PolicyEvaluator evaluates policies
type PolicyEvaluator interface {
validation.Dependency
// Evaluate takes a set of SignedData and evaluates whether this set of signatures satisfies
// the policy with the given bytes
Evaluate(policyBytes []byte, signatureSet []*common.SignedData) error
}
StateFetcher
:获取一个与世界状态交互的State
对象:
// State defines interaction with the world state
type State interface {
// GetStateMultipleKeys gets the values for multiple keys in a single call
GetStateMultipleKeys(namespace string, keys []string) ([][]byte, error)
// GetStateRangeScanIterator returns an iterator that contains all the key-values between given key ranges.
// startKey is included in the results and endKey is excluded. An empty startKey refers to the first available key
// and an empty endKey refers to the last available key. For scanning all the keys, both the startKey and the endKey
// can be supplied as empty strings. However, a full scan should be used judiciously for performance reasons.
// The returned ResultsIterator contains results of type *KV which is defined in protos/ledger/queryresult.
GetStateRangeScanIterator(namespace string, startKey string, endKey string) (ResultsIterator, error)
// GetStateMetadata returns the metadata for given namespace and key
GetStateMetadata(namespace, key string) (map[string][]byte, error)
// GetPrivateDataMetadata gets the metadata of a private data item identified by a tuple <namespace, collection, key>
GetPrivateDataMetadata(namespace, collection, key string) (map[string][]byte, error)
// Done releases resources occupied by the State
Done()
}
重要提示¶
- 各节点上的验证插件保持一致: 在后期版本中,Fabric 通道基础设施将确保在给定区块链高度上,通道内所有节点对给定链码使用相同的验证逻辑,以消除可能导致节点间状态分歧的错误配置风险,若发生错配置,则可能会致使节点运行不同的实现。但就目前来说,系统操作员和管理员的唯一责任就是确保以上问题不会发生。
- 验证插件错误处理: 当因发生某些暂时性执行问题(比如无法访问数据库)而导致验证插件不能确定一给定交易是否有效时,插件应返回
core/handlers/validation/api/validation.go
中定义的 执行失败错误 类错误。任何其他被返回的错误将被视为背书策略错误,并且被验证逻辑标记为无效。但是,若返回的错误是ExecutionFailureError
,区块链处理不会将该交易标志为无效,而是暂停该易。目的是防止不同节点之间发生状态分歧。 - 私有元数据索取的错误处理: 当一个插件利用
StateFetcher
界面来为私有数据索取元数据,错误处理必须遵循以下方法: - 将Fabric代码导入插件: 如果把 Fabric 代码导入插件,当代码随着版本升级而改变时可能会产生一些问题;其次,当运行混合节点版本时,可能行不通。因此,不建议将 Fabric 代码导入插件。理想状况下插件代码应该仅使用为其提供的依赖项,并且应导入除 protobufs 之外的最低限度。
访问控制列表(ACL)¶
什么是访问控制列表¶
注意:这个主题在通道管理员级别处理访问控制和策略。学习链码的访问控制,请 查看 chaincode for developers tutorial。
Fabric 使用权限控制列表(ACLs)通过给资源关联的 策略 — 给身份集合一个是或否的一 个规则声明 — 来管理资源的访问权限。Fabric 包含很多默认的 ACLs。在这篇文章中,我们将 讨论他们是如何规定和如何覆盖默认值的。
但是在那之前,我们有必要理解一点资源和策略的内容。
资源¶
Fabric 的用户交互通过用户链码,系统链码, 或者 事件流源来实现。因此,这些端点被视为应该在其上执行访问 控制的“资源”。
应用开发者应该注意这些资源和与他们关联的默认策略。这些资源的完整列表可以在 configtx.yaml
中找到。你可以在这里找到 configtx.yaml
示例。
configtx.yaml
里边的资源名称详细的罗列了目前 Fabric 里边的资源。这里使用的不严格约定是
<component>/<resource>
。所以 cscc/GetConfigBlock
是 CSCC
组件中调用的 GetConfigBlock
的资源。
策略¶
策略是 Fabric 运行的基础,因为它们允许根据与完成请求所需资源相关联的策略来检查与请求 关联的身份(或身份集)。背书策略用来决定一个交易是否被合适地背书。通道配置中定义的策 略被引用为修改策略以及访问控制,并且在通道配置本身中定义。
策略可以采用以下两种方式之一进行构造:作为 Signature
策略或者 ImplicitMeta
策略。
Signature
策略¶
这些策略标示了要满足策略而必须签名的用户。例如:
Policies:
MyPolicy:
Type: Signature
Rule: “Org1.Peer OR Org2.Peer”
构造的这个策略可以被解释为: 一个名为 MyPolicy
的策略只有被 “ Org1 的节点”
或着 “ Org2 的节点”签名才可以通过。
签名策略支持 AND
, OR
和 NOutOf
的任意组合,能够构造强大的规则,比如:“组
织 A 中的一个管理员和两个其他管理员,或者20个组织管理员中的11个”。
ImplicitMeta
策略¶
ImplicitMeta
策略聚合配置层次结构中更深层次的策略结果,这些策略最终由签名策略
定义。他们支持默认规则,比如“组织中大多数管理员”。这些策略使用的语法和 Signature
策略不同但是依旧很简单: <ALL|ANY|MAJORITY> <sub_policy>
。
比如: ANY
Readers
或者 MAJORITY
Admins
。
注意,在默认策略配置中 Admins
有操作员角色。指定只有管理员—或某些管理员子集—可
以访问资源的策略往往是针对网络的敏感或操作方面(例如在通道上实例化链代码)。Writers
表示可以提交账本更新,比如一个交易,但是不能拥有管理权限。Reader
拥有被动角色。他们
可以访问信息但是没有权利提交账本更新和执行管理任务。这些默认策略可以被添加,编辑或者补
充,比如通过新的 peer
或者 client
角色(如果你拥有 NodeOU
支持)
这是一个 ImplicitMeta
策略结构的例子:
Policies:
AnotherPolicy:
Type: ImplicitMeta
Rule: "MAJORITY Admins"
这里, AnotherPolicy
策略可以通过 MAJORITY Admins
(大多数管理员同意)的方式
来满足。这里 Admins
是在通过更低级的 Signature
策略来满足的。
在哪里定义访问控制权限?¶
默认的访问控制在 configtx.yaml
中,这个文件由 configtxgen
用来编译通道配置。
访问控制可以通过两种方式中的一种来更新:编辑 configtx.yaml
自身,这会把 ACL 的
改变传递到所有新通道;或者通过特定通道的通道配置来更新访问控制。
如何在 configtx.yaml
中格式化 ACLs¶
ACLs 被格式化为资源函数名称字符串的键值对。你可以在这里看到他们的样子示例 configtx.yaml 文件.
这个示例的两个摘录:
# ACL policy for invoking chaincodes on peer
peer/Propose: /Channel/Application/Writers
# ACL policy for sending block events
event/Block: /Channel/Application/Readers
这些 ACLs 定义为对资源 peer/Propose
和 event/Block
的访问分别被限定为满足路
径 /Channel/Application/Writers
和 /Channel/Application/Readers
中定义的策略的身份。
更新 configtx.yaml
中的默认 ACL¶
如果在引导网络时需要覆盖 ACL 默认值,或者在引导通道之前更改 ACL,最佳做法是更
新 configtx.yaml
。
假如你想修改 peer/Propose
的默认 ACL — 为在节点上执行链码指定策略 — 从
/Channel/Application/Writers
到一个叫 MyPolicy
的策略。
这可以通过添加一个叫 MyPolicy
(它可以是任何名字,但是在这个例子中我们称它为
MyPolicy
)的策略来完成。这个策略定义在 configtx.yaml
中的 Application.Policies
部分,指定了一个用来检查允许或者拒绝一个用户的规则。在这个例子中,我们将创建
一个标示为 SampleOrg.admin
的 Signature
策略。
Policies: &ApplicationDefaultPolicies
Readers:
Type: ImplicitMeta
Rule: "ANY Readers"
Writers:
Type: ImplicitMeta
Rule: "ANY Writers"
Admins:
Type: ImplicitMeta
Rule: "MAJORITY Admins"
MyPolicy:
Type: Signature
Rule: "OR('SampleOrg.admin')"
然后,编辑 configtx.yaml
中的 Application: ACLs
部分来将 peer/Propose
从:
peer/Propose: /Channel/Application/Writers
改变为:
peer/Propose: /Channel/Application/MyPolicy
一旦 configtx.yaml
中的这些内容被改变了,configtxgen
工具就可以在创建交易的
时候使用这些策略和定义的 ACLs。当交易以合适的方式被联盟中的管理员签名和确认之后,
被定义了 ACLs 和策略的新通道就被创建了。
一旦 MyPolicy
被引导进通道配置,它就还可以被引用来覆盖其他默认的 ACL。例如:
SampleSingleMSPChannel:
Consortium: SampleConsortium
Application:
<<: *ApplicationDefaults
ACLs:
<<: *ACLsDefault
event/Block: /Channel/Application/MyPolicy
这将限制订阅区块事件到 SampleOrg.admin
的能力。
如果已经被创建的通道想使用这个 ACL,他们必须使用如下流程每次更新一个通道配置:
在通道配置中更新默认 ACL¶
如果已经创建的通道想使用 MyPolicy
来显示访问 peer/Propose
— 或者他们想创
建一个不想让其他通道知道的 ACLs — 他们将不得不通过配置更新交易来每次更新一个
通道。
注意:通道配置交易的过程我们在这里就不深究了。如果你想了解更多,请参考这篇文 章 channel configuration updates 和 “Adding an Org to a Channel” tutorial.
下边添加 MyPolicy
,在这里 Admins
,Writers
, 和 Readers
都已经存在了。
"MyPolicy": {
"mod_policy": "Admins",
"policy": {
"type": 1,
"value": {
"identities": [
{
"principal": {
"msp_identifier": "SampleOrg",
"role": "ADMIN"
},
"principal_classification": "ROLE"
}
],
"rule": {
"n_out_of": {
"n": 1,
"rules": [
{
"signed_by": 0
}
]
}
},
"version": 0
}
},
"version": "0"
},
特别注意这里的 msp_identifer
和 role
。
然后,在配置中的 ACL 部分,将 peer/Propose
的 ACL 从:
"peer/Propose": {
"policy_ref": "/Channel/Application/Writers"
改为:
"peer/Propose": {
"policy_ref": "/Channel/Application/MyPolicy"
注意:如果你不想在你的通道配置中定义 ACLs,你就要添加完整的 ACL 结构。
一旦配置被更新了,它就需要通过常规的通道更新过程来提交。
满足需要访问多个资源的 ACL¶
如果一个成员生成了一个访问多个系统链码的请求,必须满足所有系统链码的
例如,peer/Propose
引用通道上的任何提案请求。如果特定提案请求访问需要满足 Writers
身份的两个系统链码和一个需要满足 MyPolicy
身份的系统链码,那提交这个提案的成员就必
须拥有 Writers
和 MyPolicy
都评估为 “true” 的身份。
在默认配置中,Writers
是一个 rule
为 SampleOrg.membe
的签名策略。换句话说就是,
“组织中的任何成员”。上边列出的 MyPolicy
,拥有 SampleOrg.admin
或者 “组织中的任何
管理员”。为了满足这些 ACL,成员必须同时是一个管理员和 SampleOrg
中的成员。默认地,
所有管理员都是成员(尽管并非所有管理员都是成员),但可以将这些策略覆盖为你希望的任何
成员。因此,跟踪这些策略非常重要,以确保节点提案的 ACLs 不是不可能满足的(除非是这样)。
使用实验性ACL功能的客户的迁移注意事项¶
以前,管理访问控制列表是在通道创建交易中的 isolated_data
部分完成的,通过
PEER_RESOURCE_UPDATE
交易进行更新。最初,人们认为 resourcse
树将处理一些函数的
更新,这些函数最终以其他方式处理,因此维护单独的并行节点配置树被认为是不必要的。
可以使用 v1.1 中的实验资源树进行客户迁移。由于官方 v1.2 版本不支持旧的 ACL 方法, 因此网络运营商应关闭所有节点。然后,他们应该将它们升级到 v1.2,提交一个通道重新配 置交易,来启用 v1.2 功能并设置所需的 ACL,然后最终重新启动升级的节点。重新启动的节 点将立即使用新的通道配置并根据需要强制执行 ACLs。
使用身份混合器(Identity Mixer)的 MSP 实现¶
什么是 Idemix?¶
Idemix 是一个加密协议套件,提供了强大的身份验证和隐私保护功能,比如,匿名(anonymity),这是一个不用明示交易者的身份即可执行交易的功能;还有,不可链接性(unlinkability),该特性可以使一个身份发送多个交易时,不能显示出这些交易是由同一个身份发出的。
在 Idemix 流程中包括三中角色: 用户(user)、发布者(issuer) 和 验证者(verifier)。

- 发布者以数字证书的形式发布一组用户属性,以下称此证书为“凭证(credential)”。
- 用户随后会生成一个 “零知识证明” 来证明自己拥有这个凭证,并且只选择性的公开自己想公开的属性。这个证明,因为是零知识的,所以不会向验证者、发布者或任何人透露任何额外信息。
例如,假设 “Alice” 需要向 Bob(商店职员)证明她有机动车管理局(DMV)发给她的驾照。
在这个场景中,Alice 是用户,机动车管理局是发布者,Bob 是验证者。为了向 Bob 证明 Alice 有驾驶执照,她可以给他看。但是,这样 Bob 就可以看到 Alice 的名字、地址、确切年龄等等,这比 Bob 有必要知道的信息多得多。
换句话说,Alice 可以使用 Idemix 为 Bob 生成一个“零知识证明”,该证明只显示她拥有有效的驾照,除此之外什么都没有。
所以,从这个证明中:
- Bob 只知道 Alice 有一个有效的执照,除此之外他没有了解到关于 Alice 的任何其他信息(匿名性)。
- 如果 Alice 多次访问商店并每次都为 Bob 生成一个证明,Bob 将无法从这些证明中看出这是同一个人(不可链接性)。
Idemix 身份验证技术提供了与标准 X.509 证书类似的信任模型和安全保证,但是使用了底层加密算法,有效地提供了高级隐私特性,包括上面描述的特性。在下面的技术部分中,我们将详细比较 Idemix 和 X.509 技术。
如何使用 Idemix¶
要了解如何在 Hyperledger Fabric 中使用 Idemix,我们需要查看哪些 Fabric 组件对应于 Idemix 中的用户、发布者和验证者。
Fabric Java SDK 是 用户 的 API 。在将来,其他 Fabric SDK 也会支持 Idemix 。
Fabric 提供了两种可能的 Idemix 发布者 :
- Fabric CA 支持生产环境和开发环境
- idemixgen 工具支持开发环境。
验证者 在 Fabric 中是 Idemix MSP 。
为了在超级账本 Fabric 中使用 Idemix ,需要以下三个基本步骤:

对比这个图和上面那个图中的角色。
考虑发布者。
Fabric CA(1.3 或更高版本)改进后可自动充当 Idemix 发布者。当启动
fabric-ca-server
时(或通过fabric-ca-server init
命令初始化时),将在fabric-ca-server
的主目录中自动创建以下两个文件:IssuerPublicKey
和IssuerRevocationPublicKey
。步骤 2 需要这些文件。对于开发环境,如果你还没使用 Fabric CA,你可以使用
idemixgen
创建这些文件。考虑验证者。
您需要使用步骤1中的
IssuerPublicKey
和IssuerRevocationPublicKey
创建 Idemix MSP。例如,考虑下面的这些摘自 Hyperledger Java SDK 示例中 configtx.yaml 的片段:
- &Org1Idemix # defaultorg defines the organization which is used in the sampleconfig # of the fabric.git development environment name: idemixMSP1 # id to load the msp definition as id: idemixMSPID1 msptype: idemix mspdir: crypto-config/peerOrganizations/org3.example.com
msptype
设为idemix
,并且目录mspdir``(本例中是 ``crypto-config/peerOrganizations/org3.example.com/msp
)的内容包含IssuerPublicKey
和IssuerRevocationPublicKey
文件。注意,在本例中,
Org1Idemix
代表Org1``(未显示)的 Idemix MSP,``Org1
还有一个 X509 MSP 。考虑用户。回想一下,Java SDK 是用户的 API。
要使用 Java SDK 的 Idemix,只需要额外调用
org.hyperledger.fabric_ca.sdk.HFCAClient
类中的idemixEnroll
方法。例如,假设hfcaClient
是你的 HFCAClient 对象,x509Enrollment
是与你的 X509 证书相关联的org.hyperledger.fabric.sdk.Enrollment
。下面的调用将会返回一个和你的 Idemix 凭证相关联的
org.hyperledger.fabric.sdk.Enrollment
对象。IdemixEnrollment idemixEnrollment = hfcaClient.idemixEnroll(x509enrollment, "idemixMSPID1");
还需要注意,
IdemixEnrollment
实现了org.hyperledger.fabric.sdk.Enrollment
接口,因此可以像使用 X509 注册对象一样使用它,当然 Idemix 自动提供了改进的隐私保护功能。
Idemix 和链码¶
从验证者的角度来看,还有一个角色需要考虑:链码。当使用 Idemix 凭证时,链码可以获取有关交易参与者的哪些信息?
当使用 Idemix 凭证时,`cid (Client Identity) 库<https://github.com/hyperledger/fabric/tree/master/core/chaincode/shim/ext/cid>`_ (只支持 golang )已扩展支持 GetAttributeValue
方法。但是,像下面“当前限制”模块提到的那样,在 Idemix 的情况下,只有两个展示出来的属性:ou
和 role
。
如果 Fabric CA 是凭证发布者:
- ou 属性的值是身份的 **从属(affiliation)**(例如,“org1.department1”);
role
属性的值将是 ‘member’ 或 ‘admin’。‘admin’ 表示该身份是 MSP 管理员。默认情况下,Fabric CA 创建的身份将返回 ‘member’ 角色。要创建一个 ‘admin’ 身份,使用值为2
的role
属性注册身份。
用 Java SDK 设置从属的例子,请查看 示例 。
在 go 链码中使用 CID 库来检索属性的例子,请查看 `go 链码<https://github.com/hyperledger/fabric-sdk-java/blob/master/src/test/fixture/sdkintegration/gocc/sampleIdemix/src/github.com/example_cc/example_cc.go#L88>`_ 。
当前限制¶
Idemix 的当前版本有一些限制。
固定的属性集合
还不支持发布 Idemix 凭证的自定义属性。自定义属性在将来会支持。
下面的四个属性是支持的:
- 组织单元(Organizational Unit)属性(”ou”):
- 用法:和 X.509 一样
- 类型:String
- 显示(Revealed):总是
- 角色(Role) 属性(”role”):
- 用法:和 X.509 一样
- 类型:integer
- 显示(Revealed):总是
- 注册 ID(Enrollment ID)属性:
- 用法:用户的唯一身份,即属于同一用户的所有注册凭证都是相同的(在将来的版本中用于审计)
- 类型:BIG
- 显示(Revealed):不在签名中使用,只在为 Fabric CA 生成身份验证 token 时使用
- 撤销句柄(Revocation Handle)属性:
- 用法:唯一性身份凭证(在将来的版本中用于撤销)
- 类型:integer
- 显示:从不
还不支持撤销
尽管存在上面提到的撤销句柄属性,可以看出撤销框架的大部分已经就绪,但是还不支持撤销 Idemix 凭证。
节点背书时不使用 Idemix
目前 Idemix MSP 只被节点用来验证签名。只完成了在Client SDK 中使用 Idemix 签名。未来会支持更多角色(包括 ‘peer’ 角色)使用 Idemix MSP 。
技术总结¶
对比 Idemix 凭证和 X.509 证书¶
Idemix 和 X.509 中的证书/凭证的概念、颁发过程,非常相似:一组属性使用不能伪造的数字签名进行签名,并且有一个利用密码学绑定的密钥。
标准 X.509 证书和 Identity Mixer 证书之间的主要区别是用于验证属性的签名方案。Identity Mixer 系统下的签名能够使其有效地证明所有者拥有该签名和相应的属性,而无需揭示签名和(选择的)属性值本身。我们使用零知识证明来确保这些“知识”或“信息”不会被泄露,同时确保属性上的签名有效,并且确保用户拥有相应的凭证密钥。
这样的证明,比如 X.509 证书,可以使用最初签署证书的机构的公钥进行验证,并且无法成功伪造。只有知道凭证密钥的用户才能生成凭证及其属性的证明。
关于不可链接性,当提供 X.509 证书时,必须显示所有属性来验证证书签名。这意味着所有用于签署交易的证书的用法都是可链接的。
为了避免这种可链接性,每次都需要使用新的 X.509 证书,这会导致复杂的密钥管理、通信和存储开销。此外,在某些情况下,即使颁发证书的 CA 也不应该将所有交易链接到用户,这一点很重要。
Idemix 有助于避免 CA 和验证者之间的可链接性,因为即使是 CA 也不能将证明链接到原始凭证。发布者或验证者都不能分辨两种证明是否是来自同一凭证。
这篇文章详细介绍了 Identity Mixer 技术的概念和特点 `Concepts and Languages for Privacy-Preserving Attribute-Based Authentication<https://link.springer.com/chapter/10.1007%2F978-3-642-37282-7_4>`_ 。
拓扑信息¶
鉴于上述限制,建议每个通道仅使用一个基于 Idemix 的 MSP,或者在极端情况下,每个网络使用一个基于 Idemix 的 MSP。实际上,如果每个通道有多个基于 Idemix 的 MSP,那么任意参与方读取该通道的账本,即可区分出来各个交易分别是由哪个 Idemix MSP 签署的。这是因为,每个交易都会泄漏签名者的 MSP-ID 。换句话说,Idemix 目前只提供同一组织(MSP)中客户端的匿名性。
将来,Idemix 可以扩展为支持基于 Idemix 的多层匿名结构的认证机构体系,这些机构认证的凭证可以通过使用唯一的公钥进行验证,从而实现跨组织的匿名性(MSP)。这将允许多个基于 Idemix 的 MSP 在同一个通道中共存。
在主体中,可以将通道配置为具有单个基于 Idemix 的 MSP 和多个基于 X.509 的 MSP。当然,这些 MSP 之间的交互可能会泄露信息。对泄露的信息需要逐案进行评估。
底层加密协议¶
Idemix 技术是建立在一个盲签名方案的基础上的,该方案支持签名拥有多个消息和有效的的零知识证明。Idemix 的所有密码构建模块都在顶级会议和期刊上发表了,并得到了科学界的验证。
Fabric 的这个特定 Idemix 实现使用了一个 pairing-based 的签名方案,该方案由 `Camenisch 和 Lysyanskaya https://link.springer.com/chapter/10.1007/978-3-540-28628-8_4>`_ 简要提出,并由 Au et al. 详细描述。使用了在零知识证明 Camenisch et al. 中证明签名的知识的能力。
身份混合器(Identity Mixer) MSP 配置生成器(idemixgen)¶
本文讲述了 idemixgen
工具的用法,它用来根据 MSP 为身份混合器创建配置文件。有两个可用的命令,一个用来创建新的 CA 密钥对,另一个用来根据之前生成的 CA 密钥创建 MSP 配置。
目录结构¶
idemixgen
工具将根据下边的结构创建目录:
- /ca/
IssuerSecretKey
IssuerPublicKey
RevocationKey
- /msp/
IssuerPublicKey
RevocationPublicKey
- /user/
SignerConfig
ca
目录包含发布者的私钥(包括已撤销的密钥)并且应该只代表一个 CA。msp
目录包含用于验证 idemix 签名的 MSP 信息。user
目录指定一个默认的签名者。
CA 密钥生成¶
身份混合器的 CA(发布者) 密钥套件可以使用 idemixgen ca-keygen
命令创建。这将会在工作目录创建 ca
和 msp
目录。
添加默认签名者¶
在使用 idemixgen ca-keygen
命令创建 ca
和 msp
目录后,可以使用 idemixgen signerconfig
向配制中添加 user
目录中的一个用户为默认签名者。
$ idemixgen signerconfig -h
usage: idemixgen signerconfig [<flags>]
Generate a default signer for this Idemix MSP
Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
-u, --org-unit=ORG-UNIT The Organizational Unit of the default signer
-a, --admin Make the default signer admin
-e, --enrollment-id=ENROLLMENT-ID
The enrollment id of the default signer
-r, --revocation-handle=REVOCATION-HANDLE
The handle used to revoke this signer
例如,我们可以创建组织单元 “OrgUnit1” 中的一个成员为默认签名者,他的注册身份是 “johndoe”, 撤销句柄为 “1234”,并且是一个管理员,创建的命令如下:
idemixgen signerconfig -u OrgUnit1 --admin -e "johndoe" -r 1234
操作服务¶
Peer 和 orderer 管理一个 HTTP 服务器,该服务器提供 RESTful “操作” API(应用程序编程接口)。此API与Fabric网络服务无关,旨在供操作人员使用,而非网络管理员或“用户”。
该 API 提供了以下功能:
- 日志级别管理
- 健康检查
- (当API被配置时)操作指标数据的Prometheus目标
配置操作服务¶
操作服务需要两层基本的配置
- 要监听的 地址 和 端口 。
- 用于身份验证和加密的 TLS(传输层安全协议)证书 和 密钥 。
注意,这些证书必须由另外一个专门的CA(证书授权中心)来生成,不能由已为某通道上的组织生成过证书的CA来生成。
节点¶
对每个 peer 来说,可在 core.yaml
部分的 operations
中配置操作服务器:
operations:
# host and port for the operations server
listenAddress: 127.0.0.1:9443
# TLS configuration for the operations endpoint
tls:
# TLS enabled
enabled: true
# path to PEM encoded server certificate for the operations server
cert:
file: tls/server.crt
# path to PEM encoded server key for the operations server
key:
file: tls/server.key
# most operations service endpoints require client authentication when TLS
# is enabled. clientAuthRequired requires client certificate authentication
# at the TLS layer to access all resources.
clientAuthRequired: false
# paths to PEM encoded ca certificates to trust for client authentication
clientRootCAs:
files: []
listenAddress
键定义了操作服务器将执行监听的主机和端口。如果服务器要监听所有地址,则可以忽略主机部分。
tls
部分用于指明是否为操作服务启用了TLS,还指明了该服务的证书和私钥的位置以及客户端身份验证应该信任的证书颁发机构根证书的位置。当 enabled
是真实的,多数操作服务端点会要要求客户验证,因此必须设定 clientRootCAs.files
。当 clientAuthRequired
是 true
,TLS层将需要客户在每次请求时都提供一份证书以供验证。参考下面操作安全部分的内容来获取更多信息。
排序节点¶
对每个orderer来说,运行服务器都可配置在的 orderer.yaml
的 Operations 部分。
Operations:
# host and port for the operations server
ListenAddress: 127.0.0.1:8443
# TLS configuration for the operations endpoint
TLS:
# TLS enabled
Enabled: true
# PrivateKey: PEM-encoded tls key for the operations endpoint
PrivateKey: tls/server.key
# Certificate governs the file location of the server TLS certificate.
Certificate: tls/server.crt
# Paths to PEM encoded ca certificates to trust for client authentication
ClientRootCAs: []
# Most operations service endpoints require client authentication when TLS
# is enabled. ClientAuthRequired requires client certificate authentication
# at the TLS layer to access all resources.
ClientAuthRequired: false
ListenAddress
键定义了操作服务器将监听的主机和端口。如果服务器要监听所有地址,则可忽略主机部分。
TLS
部分用于指明是否为操作服务启用了TLS,还指明了该服务的证书和私钥的位置以及客户端身份验证应该信任的证书授权中心根证书的位置。当 Enabled
是真实的,多数操作服务端点会要要求客户验证,因此必须设定 RootCAs
。当 clientAuthRequired
是 true
,TLS层将需要客户在每次请求时都提供一份证书以供验证。参考下面 操作安全部分的内容来获取更多信息。
操作安全¶
由于操作服务专注于操作,与 Fabric 网络无关,因此它不是用MSP(成员服务提供者)来进行访问控制,而是完全依赖于具有客户端证书身份验证功能的双向 TLS。
禁用TLS后,授权将被绕过,这样一来,任何能连接到运行端点的客户端都可以使用API(应用程序编程接口)。
启用TLS后,除非下面另有说明,否则必须提供有效的客户端证书才能访问所有资源。
若同时启用了 clientAuthRequired 时,无论访问的是什么资源,TLS 层都将需要有效的客户端证书。
日志级别管理¶
操作服务提供了 /logspec
资源,操作人员可用该资源来管理peer或orderer的活跃日志记录规范。该资源是常规的REST资源,支持 GET
和 PUT
请求。
当操作服务接收到 GET /logspec
请求时,它将使用包含当前日志记录规范的 JSON 有效负载进行响应:
{"spec":"info"}
当操作服务接收到 PUT /logspec
请求时,它将把 body 读取为 JASON 有效负载。有效负载必须包含名为 spec
的单个属性。
{"spec":"chaincode=debug:info"}
如果规范成功激活,服务将回复 204 "No Content"
。如果出现错误,服务将回复 400 "Bad Request"
以及一个错误有效负载:
{"error":"error message"}
健康检查¶
操作服务提供了 /healthz
资源,操作人员可用该资源来确定 peer 和 orderer 的活跃度及健康状况。该资源是支持GET请求的常规REST资源。它的实现旨在与 Kubernetes 使用的活跃度探针模型兼容,不过还可以在其他场景中进行。
当操作服务收到 GET/healthz
请求,它将调用所有已注册的运行状况检查程序来执行该流程。当所有运行状况检查程序都成功返回时,操作服务将以 200 "OK"
和 JSON body 进行回应:
{
"status": "OK",
"time": "2009-11-10T23:00:00Z"
}
如果运行状况检查程序中的一个或多个返回错误时,运行服务将响应 503 "Service Unavailable"
和一个包含未成功的运行状况检查程序的JASON body:
{
"status": "Service Unavailable",
"time": "2009-11-10T23:00:00Z",
"failed_checks": [
{
"component": "docker",
"reason": "failed to connect to Docker daemon: invalid endpoint"
}
]
}
在当前版本中,唯一注册的运行状况检查程序是针对Docker的。后期版本将增加额外的运行状况检查程序。
当启用TLS时,不需要提供有效的客户端证书就可以使用该服务,除非 clientAuthRequired
被设置为 true
。
指标数据(Metrics)¶
Fabric的peer和orderer的某些组件获取metrics,这些metrics可帮助深入了解系统行为。通过这些信息,操作人员和管理人员可以更好地理解系统随着时间的推移是如何运行的。
配置metrics¶
Fabric提供了两种获取metrics的方法:一种是基于Prometheus的 拉式 模型,另一种是基于StatsD的 推式 模型。
### Prometheus
典型的Prometheus部署通过从已检测目标公开的HTTP端点请求指标来获取指标数据。由于Prometheus负责请求metrics,因此它被看成是一种拉式系统。
当配置完成,Fabric的peer或orderer将在操作服务中展示 /metrics 资源。
节点¶
通过在 core.yaml
部分的 metrics
中将metrics获取方式设置为prometheus ,可对peer进行配置,从而获取 /metrics
端点,以供Prometheus使用。
.. code:: yaml
- metrics:
- provider: prometheus
排序节点¶
通过在 orderer.yaml
部分的 Metrics
中将metrics获取方式设置为prometheus ,可对orderer进行配置,从而获取 /metrics
端点,以供Prometheus使用。
Metrics:
Provider: prometheus
StatsD¶
StatsD是一个简单的统计聚合守护程序。Metrics被发送到 statsd
守护程序进行收集、汇总并推送至后端以进行可视化和警报。由于该模型需要辅助型流程来将metrics数据发送至StatsD,因此它被视为一种推式系统。
节点¶
通过在 core.yaml
部分的 metrics
中将metrics获取方式设置为 statsd
,可对节点进行配置,从而使metrics被发送至StatsD. statsd
子节必须配置有StatsD守护程序的地址、要使用的网络类型( tcp
or udp
)以及发送metrics的频率。通过指定一个可选 prefix
,可帮助区分metrics的来源(例如,区分来自不同peer的metrics),这些metrics将被添加到所有已生成的metrics中。
metrics:
provider: statsd
statsd:
network: udp
address: 127.0.0.1:8125
writeInterval: 10s
prefix: peer-0
排序节点¶
通过在 orderer.yaml
部分的 Metrics
中将metrics获取方式设置为 statsd
,可对排序节点进行配置,使得metrics被发送至StatsD. Statsd
子节必须配置有StatsD守护程序的地址、要使用的网络类型( tcp
or udp
)以及发送metrics的频率。通过指定一个可选 prefix
,可帮助区分metrics的来源。
Metrics:
Provider: statsd
Statsd:
Network: udp
Address: 127.0.0.1:8125
WriteInterval: 30s
Prefix: org-orderer
想了解已生成的不同metrics,请参考 Metrics Reference
Metrics Reference¶
Prometheus Metrics¶
The following metrics are currently exported for consumption by Prometheus.
Name | Type | Description | Labels |
---|---|---|---|
blockcutter_block_fill_duration | histogram | The time from first transaction enqueing to the block being cut in seconds. | channel |
broadcast_enqueue_duration | histogram | The time to enqueue a transaction in seconds. | channel type status |
broadcast_processed_count | counter | The number of transactions processed. | channel type status |
broadcast_validate_duration | histogram | The time to validate a transaction in seconds. | channel type status |
chaincode_execute_timeouts | counter | The number of chaincode executions (Init or Invoke) that have timed out. | chaincode |
chaincode_launch_duration | histogram | The time to launch a chaincode. | chaincode success |
chaincode_launch_failures | counter | The number of chaincode launches that have failed. | chaincode |
chaincode_launch_timeouts | counter | The number of chaincode launches that have timed out. | chaincode |
chaincode_shim_request_duration | histogram | The time to complete chaincode shim requests. | type channel chaincode success |
chaincode_shim_requests_completed | counter | The number of chaincode shim requests completed. | type channel chaincode success |
chaincode_shim_requests_received | counter | The number of chaincode shim requests received. | type channel chaincode |
cluster_comm_egress_queue_capacity | gauge | Capacity of the egress queue. | host msg_type channel |
cluster_comm_egress_queue_length | gauge | Length of the egress queue. | host msg_type channel |
cluster_comm_egress_queue_workers | gauge | Count of egress queue workers. | channel |
cluster_comm_egress_stream_count | gauge | Count of streams to other nodes. | channel |
cluster_comm_egress_tls_connection_count | gauge | Count of TLS connections to other nodes. | |
cluster_comm_ingress_stream_count | gauge | Count of streams from other nodes. | |
cluster_comm_msg_dropped_count | counter | Count of messages dropped. | host channel |
cluster_comm_msg_send_time | histogram | The time it takes to send a message in seconds. | host channel |
consensus_etcdraft_cluster_size | gauge | Number of nodes in this channel. | channel |
consensus_etcdraft_committed_block_number | gauge | The block number of the latest block committed. | channel |
consensus_etcdraft_config_proposals_received | counter | The total number of proposals received for config type transactions. | channel |
consensus_etcdraft_data_persist_duration | histogram | The time taken for etcd/raft data to be persisted in storage (in seconds). | channel |
consensus_etcdraft_is_leader | gauge | The leadership status of the current node: 1 if it is the leader else 0. | channel |
consensus_etcdraft_leader_changes | counter | The number of leader changes since process start. | channel |
consensus_etcdraft_normal_proposals_received | counter | The total number of proposals received for normal type transactions. | channel |
consensus_etcdraft_proposal_failures | counter | The number of proposal failures. | channel |
consensus_etcdraft_snapshot_block_number | gauge | The block number of the latest snapshot. | channel |
consensus_kafka_batch_size | gauge | The mean batch size in bytes sent to topics. | topic |
consensus_kafka_compression_ratio | gauge | The mean compression ratio (as percentage) for topics. | topic |
consensus_kafka_incoming_byte_rate | gauge | Bytes/second read off brokers. | broker_id |
consensus_kafka_last_offset_persisted | gauge | The offset specified in the block metadata of the most recently committed block. | channel |
consensus_kafka_outgoing_byte_rate | gauge | Bytes/second written to brokers. | broker_id |
consensus_kafka_record_send_rate | gauge | The number of records per second sent to topics. | topic |
consensus_kafka_records_per_request | gauge | The mean number of records sent per request to topics. | topic |
consensus_kafka_request_latency | gauge | The mean request latency in ms to brokers. | broker_id |
consensus_kafka_request_rate | gauge | Requests/second sent to brokers. | broker_id |
consensus_kafka_request_size | gauge | The mean request size in bytes to brokers. | broker_id |
consensus_kafka_response_rate | gauge | Requests/second sent to brokers. | broker_id |
consensus_kafka_response_size | gauge | The mean response size in bytes from brokers. | broker_id |
couchdb_processing_time | histogram | Time taken in seconds for the function to complete request to CouchDB | database function_name result |
deliver_blocks_sent | counter | The number of blocks sent by the deliver service. | channel filtered |
deliver_requests_completed | counter | The number of deliver requests that have been completed. | channel filtered success |
deliver_requests_received | counter | The number of deliver requests that have been received. | channel filtered |
deliver_streams_closed | counter | The number of GRPC streams that have been closed for the deliver service. | |
deliver_streams_opened | counter | The number of GRPC streams that have been opened for the deliver service. | |
dockercontroller_chaincode_container_build_duration | histogram | The time to build a chaincode image in seconds. | chaincode success |
endorser_chaincode_instantiation_failures | counter | The number of chaincode instantiations or upgrade that have failed. | channel chaincode |
endorser_duplicate_transaction_failures | counter | The number of failed proposals due to duplicate transaction ID. | channel chaincode |
endorser_endorsement_failures | counter | The number of failed endorsements. | channel chaincode chaincodeerror |
endorser_proposal_acl_failures | counter | The number of proposals that failed ACL checks. | channel chaincode |
endorser_proposal_validation_failures | counter | The number of proposals that have failed initial validation. | |
endorser_proposals_received | counter | The number of proposals received. | |
endorser_propsal_duration | histogram | The time to complete a proposal. | channel chaincode success |
endorser_successful_proposals | counter | The number of successful proposals. | |
fabric_version | gauge | The active version of Fabric. | version |
gossip_comm_messages_received | counter | Number of messages received | |
gossip_comm_messages_sent | counter | Number of messages sent | |
gossip_comm_overflow_count | counter | Number of outgoing queue buffer overflows | |
gossip_leader_election_leader | gauge | Peer is leader (1) or follower (0) | channel |
gossip_membership_total_peers_known | gauge | Total known peers | channel |
gossip_payload_buffer_size | gauge | Size of the payload buffer | channel |
gossip_privdata_commit_block_duration | histogram | Time it takes to commit private data and the corresponding block (in seconds) | channel |
gossip_privdata_fetch_duration | histogram | Time it takes to fetch missing private data from peers (in seconds) | channel |
gossip_privdata_list_missing_duration | histogram | Time it takes to list the missing private data (in seconds) | channel |
gossip_privdata_pull_duration | histogram | Time it takes to pull a missing private data element (in seconds) | channel |
gossip_privdata_purge_duration | histogram | Time it takes to purge private data (in seconds) | channel |
gossip_privdata_reconciliation_duration | histogram | Time it takes for reconciliation to complete (in seconds) | channel |
gossip_privdata_retrieve_duration | histogram | Time it takes to retrieve missing private data elements from the ledger (in seconds) | channel |
gossip_privdata_send_duration | histogram | Time it takes to send a missing private data element (in seconds) | channel |
gossip_privdata_validation_duration | histogram | Time it takes to validate a block (in seconds) | channel |
gossip_state_commit_duration | histogram | Time it takes to commit a block in seconds | channel |
gossip_state_height | gauge | Current ledger height | channel |
grpc_comm_conn_closed | counter | gRPC connections closed. Open minus closed is the active number of connections. | |
grpc_comm_conn_opened | counter | gRPC connections opened. Open minus closed is the active number of connections. | |
grpc_server_stream_messages_received | counter | The number of stream messages received. | service method |
grpc_server_stream_messages_sent | counter | The number of stream messages sent. | service method |
grpc_server_stream_request_duration | histogram | The time to complete a stream request. | service method code |
grpc_server_stream_requests_completed | counter | The number of stream requests completed. | service method code |
grpc_server_stream_requests_received | counter | The number of stream requests received. | service method |
grpc_server_unary_request_duration | histogram | The time to complete a unary request. | service method code |
grpc_server_unary_requests_completed | counter | The number of unary requests completed. | service method code |
grpc_server_unary_requests_received | counter | The number of unary requests received. | service method |
ledger_block_processing_time | histogram | Time taken in seconds for ledger block processing. | channel |
ledger_blockchain_height | gauge | Height of the chain in blocks. | channel |
ledger_blockstorage_and_pvtdata_commit_time | histogram | Time taken in seconds for committing the block and private data to storage. | channel |
ledger_blockstorage_commit_time | histogram | Time taken in seconds for committing the block to storage. | channel |
ledger_statedb_commit_time | histogram | Time taken in seconds for committing block changes to state db. | channel |
ledger_transaction_count | counter | Number of transactions processed. | channel transaction_type chaincode validation_code |
logging_entries_checked | counter | Number of log entries checked against the active logging level | level |
logging_entries_written | counter | Number of log entries that are written | level |
StatsD Metrics¶
The following metrics are currently emitted for consumption by StatsD. The
%{variable_name}
nomenclature represents segments that vary based on
context.
For example, %{channel}
will be replaced with the name of the channel
associated with the metric.
Bucket | Type | Description |
---|---|---|
blockcutter.block_fill_duration.%{channel} | histogram | The time from first transaction enqueing to the block being cut in seconds. |
broadcast.enqueue_duration.%{channel}.%{type}.%{status} | histogram | The time to enqueue a transaction in seconds. |
broadcast.processed_count.%{channel}.%{type}.%{status} | counter | The number of transactions processed. |
broadcast.validate_duration.%{channel}.%{type}.%{status} | histogram | The time to validate a transaction in seconds. |
chaincode.execute_timeouts.%{chaincode} | counter | The number of chaincode executions (Init or Invoke) that have timed out. |
chaincode.launch_duration.%{chaincode}.%{success} | histogram | The time to launch a chaincode. |
chaincode.launch_failures.%{chaincode} | counter | The number of chaincode launches that have failed. |
chaincode.launch_timeouts.%{chaincode} | counter | The number of chaincode launches that have timed out. |
chaincode.shim_request_duration.%{type}.%{channel}.%{chaincode}.%{success} | histogram | The time to complete chaincode shim requests. |
chaincode.shim_requests_completed.%{type}.%{channel}.%{chaincode}.%{success} | counter | The number of chaincode shim requests completed. |
chaincode.shim_requests_received.%{type}.%{channel}.%{chaincode} | counter | The number of chaincode shim requests received. |
cluster.comm.egress_queue_capacity.%{host}.%{msg_type}.%{channel} | gauge | Capacity of the egress queue. |
cluster.comm.egress_queue_length.%{host}.%{msg_type}.%{channel} | gauge | Length of the egress queue. |
cluster.comm.egress_queue_workers.%{channel} | gauge | Count of egress queue workers. |
cluster.comm.egress_stream_count.%{channel} | gauge | Count of streams to other nodes. |
cluster.comm.egress_tls_connection_count | gauge | Count of TLS connections to other nodes. |
cluster.comm.ingress_stream_count | gauge | Count of streams from other nodes. |
cluster.comm.msg_dropped_count.%{host}.%{channel} | counter | Count of messages dropped. |
cluster.comm.msg_send_time.%{host}.%{channel} | histogram | The time it takes to send a message in seconds. |
consensus.etcdraft.cluster_size.%{channel} | gauge | Number of nodes in this channel. |
consensus.etcdraft.committed_block_number.%{channel} | gauge | The block number of the latest block committed. |
consensus.etcdraft.config_proposals_received.%{channel} | counter | The total number of proposals received for config type transactions. |
consensus.etcdraft.data_persist_duration.%{channel} | histogram | The time taken for etcd/raft data to be persisted in storage (in seconds). |
consensus.etcdraft.is_leader.%{channel} | gauge | The leadership status of the current node: 1 if it is the leader else 0. |
consensus.etcdraft.leader_changes.%{channel} | counter | The number of leader changes since process start. |
consensus.etcdraft.normal_proposals_received.%{channel} | counter | The total number of proposals received for normal type transactions. |
consensus.etcdraft.proposal_failures.%{channel} | counter | The number of proposal failures. |
consensus.etcdraft.snapshot_block_number.%{channel} | gauge | The block number of the latest snapshot. |
consensus.kafka.batch_size.%{topic} | gauge | The mean batch size in bytes sent to topics. |
consensus.kafka.compression_ratio.%{topic} | gauge | The mean compression ratio (as percentage) for topics. |
consensus.kafka.incoming_byte_rate.%{broker_id} | gauge | Bytes/second read off brokers. |
consensus.kafka.last_offset_persisted.%{channel} | gauge | The offset specified in the block metadata of the most recently committed block. |
consensus.kafka.outgoing_byte_rate.%{broker_id} | gauge | Bytes/second written to brokers. |
consensus.kafka.record_send_rate.%{topic} | gauge | The number of records per second sent to topics. |
consensus.kafka.records_per_request.%{topic} | gauge | The mean number of records sent per request to topics. |
consensus.kafka.request_latency.%{broker_id} | gauge | The mean request latency in ms to brokers. |
consensus.kafka.request_rate.%{broker_id} | gauge | Requests/second sent to brokers. |
consensus.kafka.request_size.%{broker_id} | gauge | The mean request size in bytes to brokers. |
consensus.kafka.response_rate.%{broker_id} | gauge | Requests/second sent to brokers. |
consensus.kafka.response_size.%{broker_id} | gauge | The mean response size in bytes from brokers. |
couchdb.processing_time.%{database}.%{function_name}.%{result} | histogram | Time taken in seconds for the function to complete request to CouchDB |
deliver.blocks_sent.%{channel}.%{filtered} | counter | The number of blocks sent by the deliver service. |
deliver.requests_completed.%{channel}.%{filtered}.%{success} | counter | The number of deliver requests that have been completed. |
deliver.requests_received.%{channel}.%{filtered} | counter | The number of deliver requests that have been received. |
deliver.streams_closed | counter | The number of GRPC streams that have been closed for the deliver service. |
deliver.streams_opened | counter | The number of GRPC streams that have been opened for the deliver service. |
dockercontroller.chaincode_container_build_duration.%{chaincode}.%{success} | histogram | The time to build a chaincode image in seconds. |
endorser.chaincode_instantiation_failures.%{channel}.%{chaincode} | counter | The number of chaincode instantiations or upgrade that have failed. |
endorser.duplicate_transaction_failures.%{channel}.%{chaincode} | counter | The number of failed proposals due to duplicate transaction ID. |
endorser.endorsement_failures.%{channel}.%{chaincode}.%{chaincodeerror} | counter | The number of failed endorsements. |
endorser.proposal_acl_failures.%{channel}.%{chaincode} | counter | The number of proposals that failed ACL checks. |
endorser.proposal_validation_failures | counter | The number of proposals that have failed initial validation. |
endorser.proposals_received | counter | The number of proposals received. |
endorser.propsal_duration.%{channel}.%{chaincode}.%{success} | histogram | The time to complete a proposal. |
endorser.successful_proposals | counter | The number of successful proposals. |
fabric_version.%{version} | gauge | The active version of Fabric. |
gossip.comm.messages_received | counter | Number of messages received |
gossip.comm.messages_sent | counter | Number of messages sent |
gossip.comm.overflow_count | counter | Number of outgoing queue buffer overflows |
gossip.leader_election.leader.%{channel} | gauge | Peer is leader (1) or follower (0) |
gossip.membership.total_peers_known.%{channel} | gauge | Total known peers |
gossip.payload_buffer.size.%{channel} | gauge | Size of the payload buffer |
gossip.privdata.commit_block_duration.%{channel} | histogram | Time it takes to commit private data and the corresponding block (in seconds) |
gossip.privdata.fetch_duration.%{channel} | histogram | Time it takes to fetch missing private data from peers (in seconds) |
gossip.privdata.list_missing_duration.%{channel} | histogram | Time it takes to list the missing private data (in seconds) |
gossip.privdata.pull_duration.%{channel} | histogram | Time it takes to pull a missing private data element (in seconds) |
gossip.privdata.purge_duration.%{channel} | histogram | Time it takes to purge private data (in seconds) |
gossip.privdata.reconciliation_duration.%{channel} | histogram | Time it takes for reconciliation to complete (in seconds) |
gossip.privdata.retrieve_duration.%{channel} | histogram | Time it takes to retrieve missing private data elements from the ledger (in seconds) |
gossip.privdata.send_duration.%{channel} | histogram | Time it takes to send a missing private data element (in seconds) |
gossip.privdata.validation_duration.%{channel} | histogram | Time it takes to validate a block (in seconds) |
gossip.state.commit_duration.%{channel} | histogram | Time it takes to commit a block in seconds |
gossip.state.height.%{channel} | gauge | Current ledger height |
grpc.comm.conn_closed | counter | gRPC connections closed. Open minus closed is the active number of connections. |
grpc.comm.conn_opened | counter | gRPC connections opened. Open minus closed is the active number of connections. |
grpc.server.stream_messages_received.%{service}.%{method} | counter | The number of stream messages received. |
grpc.server.stream_messages_sent.%{service}.%{method} | counter | The number of stream messages sent. |
grpc.server.stream_request_duration.%{service}.%{method}.%{code} | histogram | The time to complete a stream request. |
grpc.server.stream_requests_completed.%{service}.%{method}.%{code} | counter | The number of stream requests completed. |
grpc.server.stream_requests_received.%{service}.%{method} | counter | The number of stream requests received. |
grpc.server.unary_request_duration.%{service}.%{method}.%{code} | histogram | The time to complete a unary request. |
grpc.server.unary_requests_completed.%{service}.%{method}.%{code} | counter | The number of unary requests completed. |
grpc.server.unary_requests_received.%{service}.%{method} | counter | The number of unary requests received. |
ledger.block_processing_time.%{channel} | histogram | Time taken in seconds for ledger block processing. |
ledger.blockchain_height.%{channel} | gauge | Height of the chain in blocks. |
ledger.blockstorage_and_pvtdata_commit_time.%{channel} | histogram | Time taken in seconds for committing the block and private data to storage. |
ledger.blockstorage_commit_time.%{channel} | histogram | Time taken in seconds for committing the block to storage. |
ledger.statedb_commit_time.%{channel} | histogram | Time taken in seconds for committing block changes to state db. |
ledger.transaction_count.%{channel}.%{transaction_type}.%{chaincode}.%{validation_code} | counter | Number of transactions processed. |
logging.entries_checked.%{level} | counter | Number of log entries checked against the active logging level |
logging.entries_written.%{level} | counter | Number of log entries that are written |
错误处理¶
概述¶
Hyperledger Fabric代码应该使用供应商提供的软件包 github.com/pkg/errors 来代替Go提供的标准错误类型。该软件包允许生成和显示带有错误信息的堆栈跟踪。
## 运用指南
应该使用**github.com/pkg/errors**来代替对 fmt.Errorf() 或`errors.New()`的所有调用。使用此程序包将生成一个调用堆栈,该调用堆栈会被附加到错误信息上。
使用该程序包很简单,只需对你的代码做轻微调整。
首先,你需要引入 github.com/pkg/errors
然后,更新您的代码生成的所有错误,以使用以下错误创建函数中的一个 (errors.New(), errors.Errorf(), errors.WithMessage(), errors.Wrap(), errors.Wrapf().
注解
See https://godoc.org/github.com/pkg/errors for complete documentation of the available error creation function. Also, refer to the General guidelines section below for more specific guidelines for using the package for Fabric code.
最后,将所有记录器或fmt.Printf()调用的格式指令从 %s
更改为 %+v
,以打印调用堆栈和错误信息。
Hyperledger Fabric中错误处理的通用准则¶
- 若要处理用户请求,应记录并返回错误。
- 若错误来自外部来源(如Go文库或vendored软件包),则用errors.Wrap()打包该错误,为其生成一个调用堆栈。
- 若错误来自另一个Fabric函数,当有需要时,在不影响调用堆栈的情况下使用errors.WithMessage()在错误信息中添加更多context。
- Panic不应被传播给其他软件包。
示例程序¶
以下案列程序清楚地展示了如何使用软件包:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func wrapWithStack() error {
err := createError()
// do this when error comes from external source (go lib or vendor)
return errors.Wrap(err, "wrapping an error with stack")
}
func wrapWithoutStack() error {
err := createError()
// do this when error comes from internal Fabric since it already has stack trace
return errors.WithMessage(err, "wrapping an error without stack")
}
func createError() error {
return errors.New("original error")
}
func main() {
err := createError()
fmt.Printf("print error without stack: %s\n\n", err)
fmt.Printf("print error with stack: %+v\n\n", err)
err = wrapWithoutStack()
fmt.Printf("%+v\n\n", err)
err = wrapWithStack()
fmt.Printf("%+v\n\n", err)
}
日志控制¶
概览¶
在 peer
和 orderer
中的日志功能是 common/flogging
包提供的。
如果使用 Go 语言编写的链码使用了 shim
提供的日志记录方法,那么它也使
用这个包。这个包支持
- 基于消息的严重程度的日志控制
- 基于软件 记录器 生成的消息的日志控制
- 基于不同严重程度的各种漂亮的输出选项
目前所有的日志都输出到 stderr
。为用户和开发者都提供了全局的和根据严重
程度的记录器级别的日志控制。目前对不同严重程度的信息没有固定的格式。当提交
bug 报告的时候,开发者可能会想看到 DEBUG 级别下的日志全文。
在一个精美的日志输出中,日志级别要同时有颜色和四个字符的错误码标识,比如:
“ERRO” 代表 ERROR, “DEBU” 代表 DEBUG 等。在一个日志上下文中, 记录器 是
开发者给出的一组相关信息的任意的名称。在下边精美的输出示例中,记录器 ledgermgmt
、
kvledger
、 和 peer
正在生成日志。
2018-11-01 15:32:38.268 UTC [ledgermgmt] initialize -> INFO 002 Initializing ledger mgmt
2018-11-01 15:32:38.268 UTC [kvledger] NewProvider -> INFO 003 Initializing ledger provider
2018-11-01 15:32:38.342 UTC [kvledger] NewProvider -> INFO 004 ledger provider Initialized
2018-11-01 15:32:38.357 UTC [ledgermgmt] initialize -> INFO 005 ledger mgmt initialized
2018-11-01 15:32:38.357 UTC [peer] func1 -> INFO 006 Auto-detected peer address: 172.24.0.3:7051
2018-11-01 15:32:38.357 UTC [peer] func1 -> INFO 007 Returning peer0.org1.example.com:7051
在运行的时候,可以创建任意数量的记录器,但是没有记录器的“主列表”,所以日志控制 结构不能检查日志记录器在工作或者即将退出。
日志规范¶
peer
和 orderer
命令的日志级别由一个日志规范控制,该规范通过 FABRIC_LOGGING_SPEC
环境变量来控制。
完整的日志界别规范是这样一个表单
[<logger>[,<logger>...]=]<level>[:[<logger>[,<logger>...]=]<level>...]
日志的严重程度由下边这些大小写敏感的字符串指明
FATAL | PANIC | ERROR | WARNING | INFO | DEBUG
日志级别有一个默认值。但是,可以通过下边的语法来覆盖一个或者一组记录器的 日志级别
<logger>[,<logger>...]=<level>
示例规范:
info - Set default to INFO
warning:msp,gossip=warning:chaincode=info - Default WARNING; Override for msp, gossip, and chaincode
chaincode=info:msp,gossip=warning:warning - Same as above
日志格式¶
peer
和 orderer
命令的日志格式通过 FABRIC_LOGGING_FORMAT
环境变
量来控制。它可以设置为一个格式化字符串,默认为
"%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}"
把日志打印为我们可读的终端格式。它还可以设置为 json
来输出 JSON 格式的日
志。
Go 链码¶
链码应用中的标准机制是和日志传输整合在一起,通过 peer 节点暴露给每一个链码实
例。链码的 shim
包提供了允许链码创建和管理日志对象的 API,它们的日志将进
行格式化并被插入到 shim
的日志中。
就像独立执行的程序,用户链码在技术上同样可以输出到标准输出或者标准错误。这通 常对“开发模式”很有用,这些通道通常在生产网络上被禁用,以减轻损坏或恶意代码的 滥用。但是在被管理的节点管理容器(例如 “netmode” )中,在每个节点上可以设置 CORE_VM_DOCKER_ATTACHSTDOUT=true 选项来打开输出。
当打开之后,每一个链码将接收到以它的容器 id 为键的日志通道。任何写入标准输出 或者标准错误的输出都将以行的方式整合到节点的日志中。在生产环境中不建议开启这 个设置。
API¶
NewLogger(name string) *ChaincodeLogger
- 创建一个链码使用的日志对象
(c *ChaincodeLogger) SetLevel(level LoggingLevel)
- 设置记录器的日志级别
(c *ChaincodeLogger) IsEnabledFor(level LoggingLevel) bool
- 当给定级别的日志生成时,返回 true
LogLevel(levelString string) (LoggingLevel, error)
- 将字符串转换为 LoggingLevel
LoggingLevel
是枚举中的一个成员
LogDebug, LogInfo, LogNotice, LogWarning, LogError, LogCritical
可以被直接使用,或者通过传递的一个大小写敏感的字符串给 LogLevel
API 来生成。
DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL
to the LogLevel
API.
不同严重程度级别的日志的格式通过如下函数提供:
(c *ChaincodeLogger) Debug(args ...interface{})
(c *ChaincodeLogger) Info(args ...interface{})
(c *ChaincodeLogger) Notice(args ...interface{})
(c *ChaincodeLogger) Warning(args ...interface{})
(c *ChaincodeLogger) Error(args ...interface{})
(c *ChaincodeLogger) Critical(args ...interface{})
(c *ChaincodeLogger) Debugf(format string, args ...interface{})
(c *ChaincodeLogger) Infof(format string, args ...interface{})
(c *ChaincodeLogger) Noticef(format string, args ...interface{})
(c *ChaincodeLogger) Warningf(format string, args ...interface{})
(c *ChaincodeLogger) Errorf(format string, args ...interface{})
(c *ChaincodeLogger) Criticalf(format string, args ...interface{})
日志 API 中的 f
表示提供了对日志格式的精细控制。没有使用 f
标示的话,
目前的 API 会在输出的参数标示之间插入一个空格,并且任意选择一种样式使用。
在目前的实现中,日志通过 shim
和打了时间戳的 ChaincodeLogger
产生,
由记录器 名字 和严重程度的级别标记,写入到 stderr
。注意,当 ChaincodeLogger
创建的时候,日志级别控制基于 名字 提供。为了避免歧义,所有 ChaincodeLogger
都应该赋予一个唯一的名字而不是 “shim” 。记录器的 名字 在所有的日志消息中都会显示。
shim
日志是 “shim” 。
在链码容器中记录器的默认日志级别可以在 core.yaml
文件中设置。 chaincode.logging.level
键设置了链码容器中所有记录器的默认级别。
chaincode.logging.shim
键覆盖了 shim
记录器的默认级别。
# Logging section for the chaincode container
logging:
# Default level for all loggers within the chaincode container
level: info
# Override default level for the 'shim' logger
shim: warning
默认的日志界别可以通过环境变量覆盖。 CORE_CHAINCODE_LOGGING_LEVEL
设置了所有记
录器的默认日志级别。 CORE_CHAINCODE_LOGGING_SHIM
覆盖了 shim
记录器的级别。
Go 语言链码同样可以通过链码 shim
接口的 SetLoggingLevel
API 来控制日志级别。
SetLoggingLevel(LoggingLevel level)
- 控制 shim 的日志级别
下边是一个链码如何在 LogInfo
级别下创建私有日志对象记录日志的简单例子。
var logger = shim.NewLogger("myChaincode")
func main() {
logger.SetLevel(shim.LogInfo)
...
}
利用传输层安全(TLS)进行安全通信¶
Fabric 支持在节点间使用 TLS 进行安全通信。 TLS 通信可以单向(仅服务端)也可以双向 (服务端和客户端)认证。
为 peer 节点配置 TLS¶
peer 节点既可以是 TLS 服务端也可以是 TLS 客户端。当它和另外一个 peer 节点、应用或者 CLI 连接时它是前者,当它和其他 peer 节点或者排序节点连接时它是后者。
为了在 peer 节点上启用 TLS ,需要设置下边这些节点配置的属性:
peer.tls.enabled
=true
peer.tls.cert.file
= 包含 TLS 服务证书文件的完整路径peer.tls.key.file
= 包含 TLS 服务私钥文件的完整路径peer.tls.rootcert.file
= 包含用于授权 TLS 服务证书的证书链的完整路径
默认下,当在一个 peer 节点上启用 TLS 的时候 TLS 客户端授权是关闭的。这就意味着 peer 节点在
TLS 握手期间不能验证客户端(另外一个 peer 节点、应用或者 CLI)的授权证书。为了在一个 peer 节
点上启用 TLS 客户端授权,将节点配置属性 peer.tls.clientAuthRequired
设置为 true
并且
将 peer.tls.clientRootCAs.files
属性设置为包含 CA 证书链的 CA 链文件,用于验证你组织中客
户端的 TLS 证书。
默认下,当一个 peer 节点同时作为 TLS 服务端和客户端的时候,都使用同一个证书和密钥对。如果要在
客户端侧使用不同的证书和密钥对,就分别设置 peer.tls.clientCert.file
和
peer.tls.clientKey.file
的配置属性为客户端证书和密钥文件的完整路径。
客户端授权 TLS 也可以通过设置如下环境变量来启用:
CORE_PEER_TLS_ENABLED
=true
CORE_PEER_TLS_CERT_FILE
= 服务端证书的完整路径CORE_PEER_TLS_KEY_FILE
= 服务端私钥的完整路径CORE_PEER_TLS_ROOTCERT_FILE
= CA 链文件的完整路径CORE_PEER_TLS_CLIENTAUTHREQUIRED
=true
CORE_PEER_TLS_CLIENTROOTCAS_FILES
= CA 链文件的完整路径CORE_PEER_TLS_CLIENTCERT_FILE
= 客户端证书的完整路径CORE_PEER_TLS_CLIENTKEY_FILE
= 客户端私钥的完整路径
当 peer 节点的客户端认证启动以后,客户端需要在 TLS 握手期间发送它的证书。如果客户端没有发送它 的证书,握手就会失败,并且节点会关闭连接。
当一个节点加入通道的时候,会从通道的配置区块中读取通道成员的根 CA 证书链并加入到 TLS 客户端和 服务端的根 CA 数据结构中。所以,peer 节点和 peer 节点之间的通信,peer 节点和排序节点的通信是无 缝连接的。
配置排序节点的 TLS¶
启动排序节点的 TLS ,需要设置如下排序节点的配置属性:
General.TLS.Enabled
=true
General.TLS.PrivateKey
= 包含服务端私钥文件的完整路径General.TLS.Certificate
= 包含服务端证书的文件的完整路径eGeneral.TLS.RootCAs
= 包含用于 TLS 服务端证书的 CA 证书链的完整路径
默认下,和 peer 节点一样,在排序节点上 TLS 客户端授权是关闭的。启用 TLS 客户端授权,需要设 置如下配置属性:
General.TLS.ClientAuthRequired
=true
General.TLS.ClientRootCAs
= 包含 TLS 服务端证书所使用的 CA 证书链文件的完整路径
也可以通过设置下边的环境变量来启用客户端 TLS 认证:
ORDERER_GENERAL_TLS_ENABLED
=true
ORDERER_GENERAL_TLS_PRIVATEKEY
= 包含服务端私钥文件的完整路径ORDERER_GENERAL_TLS_CERTIFICATE
= 包含服务端证书文件的完整路径ORDERER_GENERAL_TLS_ROOTCAS
= 包含 TLS 服务端证书所使用的 CA 证书链文件的完整路径ORDERER_GENERAL_TLS_CLIENTAUTHREQUIRED
=true
ORDERER_GENERAL_TLS_CLIENTROOTCAS
= 包含 TLS 客户端证书所使用的 CA 证书链文件的完整路径
为节点 CLI 配置 TLS¶
当在一个启用了 TLS 的 peer 节点上运行 CLI 命令的时候必须设置下边的环境变量:
CORE_PEER_TLS_ENABLED
=true
CORE_PEER_TLS_ROOTCERT_FILE
= 包含 TLS 服务端证书所使用的 CA 证书链文件的完整路径
如果服务器也启用了 TLS 客户端认证,下边的环境变量必须在上边的基础上进行配置:
CORE_PEER_TLS_CLIENTAUTHREQUIRED
=true
CORE_PEER_TLS_CLIENTCERT_FILE
= 客户端证书完整路径CORE_PEER_TLS_CLIENTKEY_FILE
= 客户端私钥完整路径
当运行一个命令连接排序服务时,就像 peer channel <create|update|fetch> 或者 peer chaincode <invoke|instantiate> , 如果排序节点启用了 TLS ,下边的命令行参数也必须提供:
- –tls
- –cafile <fully qualified path of the file that contains cert chain of the orderer CA>
如果排序节点启用了 TLS 客户端认证,下边的参数也必须提供:
- –clientauth
- –keyfile <fully qualified path of the file that contains the client private key>
- –certfile <fully qualified path of the file that contains the client certificate>
排查 TLS 问题¶
在排查 TLS 问题之前,建议在 TLS 客户端和服务端都开启 GRPC debug
来获得额外的信息。
可以通过设置环境变量 FABRIC_LOGGING_SPEC
包含 grpc=debug
来开启 GRPC debug
。
例如,要设施默认的日志级别为 INFO
和 GRPC 日志界别为 DEBUG
,可以设置日志配置
为 grpc=debug:info
。
如果你在客户端侧看到错误信息 remote error: tls: bad certificate
,那一般来说就意味着,
TLS 服务端启用了客户端认证而且服务端没有收到正确的客户端证书或者它不信任收到的客户证书。确
认一下客户端发送了它的证书,并且它的证书被 peer 或者排序节点信任的 CA 证书签了名。
如果你在链码的日志中看到了错误信息 remote error: tls: bad certificate
,请确认你的链码
使用的是 v1.1 或者更新的 Fabric 版本的链码 shim 。如果你的链码不包含 shim 的副本,删除链码
容器并重启节点,这样就会使用当前版本的 shim 重新编译链码容器。
配置并使用 Raft 排序服务¶
受众: Raft 排序节点管理员
概述¶
有关排序概念的整体概述以及排序服务(包括 Raft)是如何工作的,请查看我们关于排序服务的概念文档。
要了解设置排序节点的过程(包括创建本地 MSP 和创建初始区块),请查看我们关于设置排序节点的文档。
配置¶
虽然必须将每个 Raft 节点添加到系统通道,但不需要将节点添加到每个应用程序通道。此外,您可以动态地从通道中删除和添加节点,而不会影响其他节点,下面的重新配置部分中描述了该过程。
Raft 节点使用 TLS pinning 技术互相识别,因此要假冒 Raft 节点,攻击者需要获取其 TLS 证书的私钥。所以如果没有有效的 TLS 配置,就无法运行 Raft 节点。
Raft 集群要在两个部分进行配置:
- 本地配置:主要用于配置节点控制方面,如 TLS 通信、复制行为和文件存储。
- 通道配置:定义相应通道的 Raft 集群成员,以及协议指定的参数,如心跳频率、领导节点超时等。
回想一下,每个通道都有自己的 Raft 协议实例运行。因此,必须在其所属的每个通道的配置中引用 Raft 节点,方法是将其服务器和客户端 TLS 证书(以 PEM
格式)添加到通道配置中。这确保了当其他节点接收到该节点的消息时,它们可以安全地确认发送消息的节点的身份。
下边是 configtx.yaml
中的一部分内容,展示了通道中三个 Raft 节点(也就是所谓的“共识者”)的配置:
Consenters:
- Host: raft0.example.com
Port: 7050
ClientTLSCert: path/to/ClientTLSCert0
ServerTLSCert: path/to/ServerTLSCert0
- Host: raft1.example.com
Port: 7050
ClientTLSCert: path/to/ClientTLSCert1
ServerTLSCert: path/to/ServerTLSCert1
- Host: raft2.example.com
Port: 7050
ClientTLSCert: path/to/ClientTLSCert2
ServerTLSCert: path/to/ServerTLSCert2
注意:排序节点将被列为系统通道以及他们加入的任何应用程序通道的共识者。
当创建通道配置区块时,configtxgen 工具读取到 TLS 证书的路径,并将路径替换为相应的证书。
本地配置¶
orderer.yaml
中有两个部分和 Raft 排序节点的配置有关:
Cluster,决定了 TLS 通信的配置;和 consensus,决定了预写式日志和快照的存储位置。
Cluster 参数:
默认情况下,Raft 服务与面向客户端的服务器(用于发送交易或拉取区块)运行在同一个 gRPC 服务器上,但它可以配置为单独的具有独立端口的 gRPC 服务器。
这对于希望组织 CA 颁发的 TLS 证书仅用于群集节点的相互通信,而公共 TLS CA 颁发的 TLS 证书用于面向客户端的 API 的情况是非常有用的。
ClientCertificate
,ClientPrivateKey
:客户端 TLS 证书和相关私钥的文件路径。ListenPort
:集群监听的端口。如果为空,该端口就和排序节点通用端口(general.listenPort)一致 。ListenAddress
:集群服务监听的地址。ServerCertificate
,ServerPrivateKey
:TLS 服务器证书密钥对,当集群服务运行在单独的 gRPC 服务器(不同端口)上时使用。SendBufferSize
:调节出口缓冲区中的消息数。
注意: ListenPort
、ListenAddress
、ServerCertificate
和 ServerPrivateKey
必须同时设置或都不设置,如果没有设置它们,它们就会从 general TLS 部分继承,例如 general.tls.{privateKey, certificate}
。
general.cluster
还有隐藏的配置参数,可用于进一步微调集群通信或复制机制:
DialTimeout
,RPCTimeout
:指定创建连接和建立流的超时时间。ReplicationBufferSize
:可以为从其他群集节点进行块复制的每个内存缓冲区分配的最大字节数。每个通道都有自己的内存缓冲区。默认为20971520
,即20 MB
。PullTimeout
:排序节点等待接收区块的最长时间。默认为五秒。ReplicationBackgroundRefreshInterval
:节点连续两次尝试复制其加入的通道,或无法复制通道的时间间隔。默认为五分钟。TLSHandshakeTimeShift
:如果排序节点的 TLS 证书已过期且没有被及时替换(参见下文的 TLS 证书替换),那么节点之间的通信就无法建立,也不可能向排序服务传输新的交易。要从这种场景中恢复,可以让排序节点间的 TLS 握手认为时间向后移动了给定的时长,这个时长配置为TLSHandshakeTimeShift
。这只对为集群内部通信使用了独立 gRPC 服务器的排序节点有效(通过general.cluster.ListenPort
和general.cluster.ListenAddress
)。
Consensus 参数:
WALDir
:指定etcd/raft
预写式日志的存储位置。每个通道都会有以通道 ID 为名的子目录。SnapDir
:指定etcd/raft
快照的存储位置。每个通道都会有以通道 ID 为名的子目录。
还有一个隐藏配置参数可以加入到 orderer.yaml
的共识部分:
EvictionSuspicion
:通道驱逐嫌疑节点的时间,这会触发节点从其他节点拉取区块并查看它是否被通道驱逐以此来确认它的嫌疑。如果确认了它的嫌疑(被检查的区块没有包含节点 TLS 证书),该节点将终止对通道的操作。当一个节点不知道任何被选举的领导节点或者不能被选举为该通道中的领导节点时,它就有可能是被通道逐出了。默认十分钟。
通道配置¶
除(已经讨论过的)共识者之外,Raft 通道配置中还有一个 Options
部分和协议有关。目前,在节点运行的时候不能动态地修改这些值。只能重新修改配置并重启节点。
只有 SnapshotIntervalSize
是例外,它可以在节点运行的时候修改。
注意:建议不要修改下面的这些配置,因为错误的配置可能会导致领导节点选举失败(比如,TickInterval
和 ElectionTick
设置得过小)。不能选举领导节点的情况是无法解决的,因为领导节点是做变更所必需的。由于这些危险,我们建议一般不要调整这些参数。
TickInterval
:两次Node.Tick
之间的时间间隔。ElectionTick
:两次选举之间必须度过的Node.Tick
次数。就是说,如果一个跟随节点在经过ElectionTick
次的时间后仍没有从当前一轮的领导节点接收到任何消息,它就会变为候选节点,并且开始新一轮选举。ElectionTick
必须大于HeartbeatTick
。HeartbeatTick
:两次心跳之间必须度过的Node.Tick
次数。就是说,领导节点每经过HeartbeatTick
次的时间就会发送心跳消息来维持其领导地位。MaxInflightBlocks
:限制乐观复制阶段在途新增区块的最大数量。SnapshotIntervalSize
:定义每次创建的快照字节数。
重新配置¶
Raft 排序节点支持动态地(意思是,当通道正在使用时)添加和移除节点,只是一次只能添加或移除一个节点。在你尝试重新配置之前,请注意你的集群必须可以维护,并且能够获得共识。举个例子,如果你有 3 个节点,然后 2 个节点宕机了,你就不能重新配置你的集群来移除那些节点。同样地,如果你在一个有着三个节点的通道内有一个宕机的节点,那么不应该尝试替换证书,因为这会造成二次错误。作为一个准则,除非所有共识者都在线且健康,你永远都不应该尝试对 Raft 共识者做配置变更,如添加或删除共识者,或替换共识者的证书等。
如果你决定修改这些参数,我们建议只在维护周期内进行尝试。修改配置的问题绝大多数都发生在只有少量节点的集群中且有一个节点宕机之时。比如,如果你有三个节点的共识者,其中有一个宕机,这意味着你只有两个节点存活。如果你在这个状态下将集群扩展到 4 个节点,你仍然只有 2 个节点存活,这无法达到法定人数。第四个节点不能上线,因为节点只能加入到运作中的集群(除非集群总大小是 1 或 2)。
因此,在扩展一个(只有两个节点存活的)三节点集群为四节点时,你完全会被卡住,直到原先离线的节点恢复。
添加一个新节点到 Raft 集群需要通过以下步骤完成:
- 通过一个通道配置更新交易将新节点的 TLS 证书添加到通道中。注意:新节点在加入一个或更多应用通道前,必须先加入到系统通道。
- 从一个排序节点中获取最新的系统通道配置区块,这是系统通道的一部分。
- 通过验证配置区块是否包含(即将)加入的节点证书来确保此节点是系统通道的一部分。
- 使用在
General.GenesisFile
配置参数中指定的配置区块路径启动新的 Raft 节点。 - 等待 Raft 节点从已有节点复制其证书所加入的通道中的区块。在这一步完成后,节点开始服务于通道。
- 将新增的 Raft 节点端点加入所有通道的配置。
可以将已经运行(且已经加入某些通道)的节点在运行时加入到通道中。要做到这点,只需添加该节点的证书到通道的通道配置中。节点会自动检测其加入到新的通道(默认值是 5 分钟,但如果你想让节点更快检测新通道,可以重启节点),然后从通道中的 orderer 拉取通道区块,最后为该链启动 Raft 实例。
在成功完成以上步骤后,就能更新通道配置以纳入新 Raft orderer 的端点。
从一个 Raft 集群中移除一个节点需要通过以下步骤完成:
- 从所有通道的通道配置中移除其端点,包括由 orderer 管理员控制的系统通道。
- 从所有通道的通道配置中移除该节点(由证书识别)的记录。再次强调,这也包括系统通道。
- 关闭节点。
从一个指定的通道中移除节点,但仍维持其服务于其他通道需要通过以下步骤完成:
- 从该通道的通道配置中移除其端点。
- 从该通道的配置中移除该节点(由证书识别)的记录。
- 第二个阶段会导致:
- 通道中剩余的 orderer 节点在被移除的通道上下文中停止与被移除的 orderer 节点通信。它们仍会在其他通道上通信。
- 从通道中移出的节点会立即或在度过
EvictionSuspicion
的时间(默认 10 分钟)之后自动检测它的移除,然后停止其 Raft 实例。
Orderer 节点的 TLS 证书替换¶
所有 TLS 证书都有一个由颁发者决定的过期日期。这些过期日期从颁发日期算起,长至 10 年,短则数月,所以请与颁发者确认。在过期日期到来前,你需要更换节点的证书以及节点所加入的每一个通道,包括系统通道。 对于每一个节点加入的通道:
- 使用新证书更新通道配置。
- 替换节点文件系统上的证书。
- 重启节点。
因为一个节点只能有一对 TLS 证书私钥,因此在更新过程中,节点不能在新证书加入前服务通道,从而降低了容错的能力。由于这个原因,证书替换的过程一旦开始,就应该尽快完成。
如果因为某些原因,TLS 证书的替换已经开始但不能在所有通道上完成,建议回退到原先的 TLS 证书,并在以后再尝试替换。
指标¶
关于维护服务的描述和如何配置启动,参见我们关于维护服务的文档。
关于维护服务收集的指标列表,参考我们的指标参考文档。
当你为特定的用例为指标排列优先级而做大量配置工作的时候,有两个特别的指标可能是你想要监控的:
consensus_etcdraft_is_leader
:识别集群中的哪个节点是当前的领导者。如果没有设置任何节点,你就是丢失了法定人数。consensus_etcdraft_data_persist_duration
:指示写入 Raft 集群持久化预写式日志所持续的时间。从协议安全性考虑,消息必须在被共享给共识者集合之前适当地调用fsync
来持续持久化。如果这个值开始攀升,这个节点可能无法参与到共识中(这会导致此节点或者网络的服务中断)。
排错¶
- 你越是给节点更大的压力,就越可能需要修改相关参数。对于任何系统来说,电脑或机器,压力会导致性能的拖累。正如我们在概念文档中所述,Raft 中的领导者选举会发生在跟随者节点在一段时间内未接收到来自领导者的“心跳”消息或带有数据的“附加”消息时。因为 Raft 节点在通道间共享相同的通信层(这不代表他们共享数据——他们不共享!),如果一个 Raft 节点在许多通道中都属于共识者集合的部分,你会想增加它触发选举所需的时间长度来避免无意的领导者选举。
从 Kafka 迁移到 Raft¶
注意:这篇文章面向的是已熟练掌握通道配置更新交易的读者。由于迁移会涉及到几次通道配置升级交易,因此我们建议您先熟悉 向通道添加组织 教程,该教程详细讲述了通道更新的流程,熟练掌握后再尝试进行迁移。
对于想把通道从使用基于Kafka的排序服务转换成使用基于Raft的排序服务的用户,v1.4.2通过在网络各通道上进行一系列配置更新交易,使得这一点得到实现。
该教程将从宏观层面来讲述迁移流程,指出一些必要的细节,不会详细讲述每个命令。
假设与思考¶
尝试迁移之前,要考虑以下几点:
- 该流程只针对从Kafka迁移到Raft,暂不支持其他类型orderer共识之间的迁移。
- 迁移是单方向的。一旦排序服务被迁移成Raft并开始提交交易时,就没有办法再恢复成Kafka。
- 因为排序节点必须关闭然后重新启动,所以在迁移期间必须允许停机。
- 若迁移失败,只有当在本文后面规定的迁移点进行了备份,才可能对其恢复,如果未进行备份,则无法恢复至初始状态。
- 必须在同一维护窗口对所有通道完成迁移。若只迁移部分通道则不可能恢复正常操作。
- 在迁移过程结束时,每个通道都将拥有相同的Raft节点共识者集,与将出现在排序系统通道上的共识者集一样。这样就可以判断出成功的迁移。
- 利用已部署的排序节点的现有账本就能完成迁移,orderer的增加或移除应在迁移完成后进行。
高级别迁移流¶
迁移共分为五个步骤。
- 将系统置于维护模式,该模式拒绝应用程序交易,只有排序服务管理员能改动通道配置。
- 关闭系统,当迁移过程中发生错误,进行备份。
- 启动系统,每个通道都有自己的共识类型,各自的元数据都已修改。
- 重启系统后系统开始用Raft共识运行;检查各通道以确保达到规定人数。
- 将系统移出维护模式,重启正常功能。
准备迁移¶
准备迁移前还需进行以下步骤。
- 设计Raft部署,决定在Raft共识者者保留哪些排序服务节点。至少需要部署三个排序节点,但要注意的是,部署包含五个以上排序节点的共识者集能保证即使其中一个节点关闭,依然能维持高可用度,而对于只包含三个排序节点的共识者集,一旦由于某些原因(例如,正处于维护期间)导致其中一节点关闭,则极大降低了节点的可用度。
- 准备搭建Raft
Metadata
配置的材料。注意:所有通道都需收到相同的RaftMetadata
配置。. 访问 Raft配置指南 获取更多相关信息。注意:你或许会发现最简单的方法就是用Raft共识协议来引导新的排序网络,然后从其配置中复制并修改共识元数据部分。无论使用哪种方法,都需要(对每个排序节点):hostname
port
server certificate
client certificate
- 编译系统中所有通道(系统和应用程序)的列表。确保你有对配置更新签名的正确证书。例如,相关的排序服务管理员身份。
- 确保所有的排序服务节点运行的Fabric版本相同,都是v1.4.2或该版之后的版本。
- 确保所有节点运行的Fabric版本不低于v1.4.2。确保所有通道都配置了支持迁移的通道能力。
- orderer能力
V1_4_2
(或之后的版本) - 通道能力
V1_4_2
(或之后的版本)
- orderer能力
维护模式的入口¶
建议在维护模式中设置排序服务之前关闭网络的节点和客户端。虽然让节点或客户端继续运行是安全的。但由于排序服务将拒绝所有请求,因此它们的日志将充满良性但会让人误解的故障。
参照 向通道添加组织
教程中的步骤来pull,翻译并检查每个通道的配置,从系统通道开始。该步中唯一需要你改动的地方是/Channel/Orderer/ConsensusType
中的通道配置。在通道配置的一个JSON代表中需要改动的可能是.channel_group.groups.Orderer.values.ConsensusType
。
ConsensusType
由三个值代表:Type
,Metadata
和State
,其中:
Type
要么是kafka
要么是etcdraft
(Raft)。该值只能在维护模式中才能修改。- 如果
Type
是Kafka,那么Metadata
就是空的,但是如果·ConsensusType
是etcdraft
,那就必须包含有效Raft元数据。下文中还会详细谈到。 State
要么是NORMAL
,要么是MAINTENANCE
。当通道正在处理交易时State
是NORMAL
,当通道正处于迁移过程中时则为MAINTENANCE
。
在通道配置更新的第一步,只用将State
从 Normal
改成 MAINTENANCE
。暂且先不要改Type
或 Metadata
区域。要注意的是,当前Type
应该是 Kafka
。
当处于维护模式时,正常交易,与迁移无关的配置更新,peer用来索取新区块而发出的请求,这些都会被拒绝。这样做是为了避免在迁移过程中需要对节点进行备份和(如果需要的话)恢复,因为节点只有在成功完成迁移时才会收到更新。换句话来说,我们想把排序服务备份点(下一步的内容)放置在节点的账本之前,以期能够在需要的时候撤销操作。不过,排序节点管理员能发出Deliver
请求(管理员需要该功能以继续迁移流程)。
验证每个排序服务节点在各自通道是否已进入维护模式。要想完成验证,可通过获取最后一个配置区块,确保每个通道上的 Type
, Metadata
, State
分别是 kafka
,空(上文中我们刚谈到Kafka没有元数据), MAINTENANCE
。
当通道成功更新后,就可以备份排序服务了。
备份文件和关闭服务器¶
关闭所有的排序节点,Kafka服务器和Zookeeper服务器。先关闭排序服务节点至关重要。随后,在允许Kafka服务将其日志刷新到磁盘(一般耗时30秒,但可能会更久,具体时间取决于你的系统情况)后,应关闭Kafka服务器。在关闭orderer的同时关闭Kafka服务器会导致orderer的文件系统状态比Kafka服务器的新,这可能会阻止你的系统启动。
为这些服务器的文件系统创建一个备份。随后重启Kafka服务,紧接着再重启排序服务节点。
在维护模式中切换成Raft¶
迁移流程的下一步是为各通道进行通道配置更新。在配置更新中,将Type
切换成 etcdraft
(Raft) 的同时保持 State
为MAINTENANCE
,填写 Metadata
配置。我们强烈建议让所有通道上的 Metadata
配置保持一致。如果你想用不同的节点组建不同的共识者集,你就能在系统被重启为 etcdraft
模式后重新配置 Metadata
的配置。因此,提供一个完全相同的元数据对象并因此提供相同的共识者集就意味着,当重启节点时,如果系统通道组成仲裁并且能够退出维护模式时,那么其他通道也可能会执行相同的操作。为各通道提供不同的共识者集会导致有一个通道成功形成集合而另一个通道失败了。
随后,验证每个排序服务是否都通过拉取和检测每个通道的配置来提交了 ConsensusType
改变配置更新。
注意:改变 ConsensusType
的交易必须是重启节点(下一步会谈到)之前的最后一次配置交易。如果这一步之后发生其他配置交易,那么节点最有可能会在重启时崩溃,或者导致未定义的行为。
重启和验证主节点¶
注意:重启后必须退出维护模式。
在每个通道都完成 ConsensusType
更新后,关闭所有排序服务节点,关闭所有Kafka服务器和Zookeeper,然后仅重启排序服务节点。重启和Raft节点一样,在每个通道上组成一个集合,选出每个通道上的主节点。确保通过检测节点日志来验证各通道上都已选出各自的主节点(下文中指出了你需检测的内容)。这将证实迁移流程顺利完成。
当一个主节点被选举出来,对于每个通道,日志将显示:
"Raft leader changed: 0 -> node-number channel=channel-name
node=node-number "
例如:
2019-05-26 10:07:44.075 UTC [orderer.consensus.etcdraft] serveRequest ->
INFO 047 Raft leader changed: 0 -> 1 channel=testchannel1 node=2
在这个例子中 node 2
指明了主节点由通道 testchannel1
的集合选举出来(主节点是node 1
)。
切出维护模式¶
在# 各通道上执行另一项通道配置更新(向截止目前你已发送过配置更新的排序节点发送配置更新),从而将State
从 MAINTENANCE
切换成 NORMAL
。如正常一样,从系统通道先开始。如果在排序系统通道上成功了,有可能所有通道上的迁移都成功了。要进行验证,请从排序节点中获取系统通道的最后一个配置区块,验证当前 State
为 NORMAL
。为了完整性,请在各排序节点上进行验证。
当流程完成时,所有通道上的排序服务就能接受所有交易了。如果你像我们建议的那样关闭了节点和应用程序,现在你可以重启它们了。
中止和恢复¶
如果在迁移过程中还未退出维护模式时出现一个问题,只需执行以下恢复步骤:
- 关闭排序节点和Kafka服务(服务器和Zookeeper的整体)。
- 在改变
ConsensusType
之前将这些服务器的文件系统恢复成维护模式下进行的备份。 - 重启这些服务器,排序节点将在维护模式下引导至Kafka。
- 发送退出维护模式的配置更新,以继续使用Kafka来作为你的共识机制,或者在备份后恢复这些指令,修补阻碍形成Raft仲裁的错误,用修正过的Raft配置
Metadata
重新进行迁移。
若出现以下状态,则表明迁移可能未成功:
- 某些节点崩溃或关闭。
- 日志中没有关于各通道上成功选举出一个主节点的记录。
- 尝试在系统通道上切换成
NORMAL
模式,但是失败了。
启用基于 Kafka 的排序服务¶
郑重声明¶
本文档假设读者知道怎么配置 Kafka 和 ZooKeeper 集群,并阻止了非授权访问以保证它们在使用过程中的安全性。本指南的唯一目的是说明如何配置你的 Hyperledger Fabric 排序服务节点(OSN,ordering service node)使用 Kafka 集群为你的区块链网络提供排序服务的步骤。
关于排序节点的角色以及它在网络和交易流程中的作用的信息,请参考 排序服务 。
关于如何设置排序节点的信息,请参考 设置排序节点 。
关于配置 Raft 排序服务的信息,请参考 配置并使用 Raft 排序服务 。
概览¶
每个通道映射到 Kafka 中一个单独的单分区主题(topic)。当一个 OSN 通过 Broadcast
RPC 接收到交易时,它会进行检查以确认广播的客户端有写入通道的权限,然后将交易转发(或者说是生产)到 Kafka 中合适的分区中。这个分区被 OSN 消费,将接受到的交易打包到本地区块,持久化保存在他们的本地账本,然后通过 Deliver
RPC 将他们发送给接收客户端。底层的细节请参考 the document that describes how we came to this design ,图表 8 阐述了上述过程。
步骤¶
使用 K
和 Z
表示 Kafka 和 ZooKeeperer 集群中的节点数量:
K
的最小值为4。(我们将在第4步中解释,这是崩溃错误容忍的最小节点数量,比如,有4个 broker,当有1个 broker 宕机的时候,仍然可以继续读写和创建新通道。)Z
可以是3、5或者7。它应该是奇数个以防止脑裂的发生,并且多余1个以防止单点故障。多余7个 ZooKeeper 服务器就没有必要了。
具体过程如下:
- Orderers: Kafka 相关的信息编码在网络的创世区块中。 如果你使用了
configtxgen
, 编辑configtx.yaml
,或者使用一个系统通道创世区块的预配置文件,然后:
Orderer.OrdererType
设置为kafka
。Orderer.Kafka.Brokers
包含 至少两个 你集群中的 Kafka broker 的IP:port
。列表中不需要是所有的节点。(这些是你的引导 broker 。)
Orderers: 设置最小区块大小。 每个区块最大为 Orderer.AbsoluteMaxBytes 个字节(不包含头部),这个值你可以在
configtx.yaml
中设置。我们使用A
来代表它,它将影响到我们在第6步对 Kafka broker 的配置。Orderers: 创建创世区块。 使用
configtxgen
。在第3步和第4步的设置是系统层级的设计,将影响到网络中所有的 OSN。记下创世区块的位置。Kafka 集群: 合理的配置你的 Kafka brokers。 确保每一个 Kafka broker 配置了这些关键项:
unclean.leader.election.enable = false
— 数据持久化是区块链环境中的重要环节。我们不能在同步复制集合之外选择一个领导通道,或者我们冒着覆盖上一个领队产生的偏移量的风险,并因此重写排序节点产生的区块。min.insync.replicas = M
— 这里的值M
设为1 < M < N
(查看下边的default.replication.factor
)。 数据写入至少M
个副本之后才认为被提交。其他情况下,写操作返回一个错误。然后:- 如果写入的
N
个副本中有N-M
个不可用,操作仍可正常运行。 - 如果有更多的副本不可用,Kafka 就不能维护 ISR 集合中的
M
个,所以它就会停止接受写入。读取是没有问题的。当重新同步到M
个副本的时候,通道可以恢复写的功能。
- 如果写入的
default.replication.factor = N
— 这里的值N
设为N < K
。N
个副本意味着每个通道都会将它的数据备份到N
个 broker。 这些是一个通道 ISR 集合的备份。就像我们在上边提到的min.insync.replicas section
不是所有的节点一直都是可用的。N
的值要设置的小于K
,因为当少于N
个 broker 运行的时候就不能创建通道了。所以,如果你设置为N = K
,那么只要有一个 broker 宕机了,就意味着区块链网络就不能创建通道了,也就是说排序服务的崩溃容错就不存在了。基于我们上边所说的,
M
和N
的最小值分别为2和3。这样的配置可以保证新通道的创建,并且所有的通道都持续可写。message.max.bytes
和replica.fetch.max.bytes
的值应该设A
的值大,在上边你在Orderer.AbsoluteMaxBytes
中将A
的值设为了4。再为头部数据增加一些空间(多余 1 MiB 就够了)。以下条件适用:Orderer.AbsoluteMaxBytes < replica.fetch.max.bytes <= message.max.bytes (为了完备性的考虑,我们要求 ``message.max.bytes`` 的值小于 ``socket.request.max.bytes`` , ``socket.request.max.bytes`` 的默认值是 100 MiB。如果你希望区块容量大于100 MiB,你需要修改源码 ``fabric/orderer/kafka/config.go`` 中 ``brokerConfig.Producer.MaxMessageBytes`` 的值,然后重新编译。不建议这样的操作。)
Orderers: 将每一个 OSN 指向创世区块。 编辑
orderer.yaml
中的General.GenesisFile
来指定 Orderer 指向步骤5中创建的创世区块。(同时,要确保 YAML 文件中的其他键合理的配置。)Orderers: 调整轮询间隔和超时。 (可选步骤。)
orderer.yaml
文件中的Kafka.Retry
部分可以让你调整 metadata/producer/consumer 请求的频率和 socket 超时时间。(这里有你希望看到的 Kafka 生产者和消费者的全部信息。)另外,当创建一个新通道时,或者重新加载一个存在的通道时(比如重启一个排序节点),排序节点和 Kafka 集群的交互过程如下:
- 排序节点为该通道相关的 Kafka 分区创建一个 Kafka 生产者(写入者)。
- 排序节点使用生产者向分区发送一个无操作的
CONNECT
消息。 - 排序节点为分区创建一个 Kafka 消费者(读取者)。
- 即使任意一个步骤失败了,你也可以通过调整重试的频率重复上边的步骤。他们将会每隔
Kafka.Retry.ShortInterval
所设置的时间进行Kafka.Retry.ShortTotal
次尝试,和每隔Kafka.Retry.LongInterval
所设置的时间进行Kafka.Retry.LongTotal
次尝试,直到成功为止。注意,排序节点只有在上述步骤成功完成后才可以进行读写。
设置 OSN 和 Kafka 之间的 SSL 通信。 (可选步骤,但是强烈建议。)参考 the Confluent guide 配置 Kafka 集群的设置,然后在每一个相关的 OSN 中设置
orderer.yaml
中Kafka.TLS
的键值。以如下顺序启动节点:ZooKeeper 集群,Kafka 集群,排序服务节点。
其他注意事项¶
- 首选消息容量。 在上边第4步中(查看 `Steps`_ 部分)你可以通过设置
Orderer.Batchsize.PreferredMaxBytes
来设定默认区块大小。Kafka 对于相对较小的消息有较高的吞吐量;所以该值不要大于1 MiB。 - 使用环境变量覆盖设置。 当使用 Fabric 提供的示例 Kafka 和 ZooKeeper Docker 镜像时(请查看
images/kafka
和images/zookeeper
相关信息),你可以通过环境变量来覆盖 Kafka broker 或者 ZooKeeper 服务器的设置。将配置文件中的点替换为下划线,例如KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE=false
将覆盖unclean.leader.election.enable
的值。这将和 OSN 本地 配置文件的效果是一样的,例如在orderer.yaml
中的设置。例如ORDERER_KAFKA_RETRY_SHORTINTERVAL=1s
将覆盖Orderer.Kafka.Retry.ShortInterval
所设置的值。
Kafka 协议版本兼容性¶
Fabric 使用 sarama client library 支持 Kafka 0.10 到 1.0 的版本,同样还支持较老的版本。
使用 orderer.yaml
中的 Kafka.Version
键,你可以配置你使用哪个 Kafka 协议版本和 Kafka 集群的 brokers 通信。使用老协议版本的 Kafka 代理向后兼容。因为 Kafka 代理对老协议版本的向后兼容性,升级你的 Kafka 代理版本时不需要升级 Kafka.Version
的键值,但是 Kafka 集群使用老协议版本可能会出现 性能损失 。
调试¶
将环境变量 FABRIC_LOGGING_SPEC
设置为 DEBUG
和 orderer.yaml
中的 Kafka.Verbose` 设置为 true
。
命令参考¶
peer¶
描述¶
peer
命令有5个不同的子命令,每个命令都可以让指定的 peer 节点执行特定的一组任务。比如,你可以使用子命令 peer channel
让一个 peer 节点加入通道,或者使用 peer chaincode
命令把智能合约链码部署到 peer 节点上。
语法¶
peer
命令的5个子命令如下:
peer chaincode [option] [flags]
peer channel [option] [flags]
peer logging [option] [flags]
peer node [option] [flags]
peer version [option] [flags]
每一个子命令拥有不同的选项 (option),并且会在它们专属的章节进行介绍。为了简便起见,我们说一个命令的时候,通常包含了 peer
命令,channel
子命令,以及 fetch
子命令选项。
如果使用子命令没有指定选项,会打印更高一级的帮助信息,下文的 --help
标记会进行描述。
标记¶
peer
的每个子命令都有一组标记,由于一些标记可以被所有子命令使用,所有它们设置为全局性的。这些标记会在 peer
的子命令中进行介绍。
顶层的 peer
命令有如下标记:
--help
使用
--help
可以获得peer
命令的简要帮助信息。--help
标记非常有用,它统一可以获取子命令和选项的帮助信息。比如
peer --help peer channel --help peer channel list --help
各子命令的帮助信息细节见
peer
的个子命令。
用法¶
这是展示 peer
命令标记用法的样例:
在
peer channel join
命令上使用--help
标记。peer channel join --help Joins the peer to a channel. Usage: peer channel join [flags] Flags: -b, --blockpath string Path to file containing genesis block -h, --help help for join Global Flags: --cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint --certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint --clientauth Use mutual TLS when communicating with the orderer endpoint --connTimeout duration Timeout for client to connect (default 3s) --keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint -o, --orderer string Ordering service endpoint --ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer. --tls Use TLS when communicating with the orderer endpoint
这展示了
peer channel join
命令的简要帮助信息。
节点链码¶
peer chaincode
命令允许管理员执行与一个节点上运行有关的链码,例如安装,实例化,调用,包装,查询和升级链码。
语法¶
peer chaincode
命令有以下子命令:
- 安装
- 实例化
- 调用
- 列表
- 包装
- 查询
- 签名包装
- 升级
不同的子命令选项(安装,实例化)牵涉到与一个peer相关的不同链码操作。例如,用peer chaincode install
子命令选项在节点上安装一个链码,或者用peer chaincode query
子命令选项为一节点上账本的当前值查询链码。
本主题将描述每个节点链码子命令以及它们的选项。
Flags¶
每个子命令都有一套专门对应各子命令的flag,以及一套涉及到所有peer chaincode
子命令的全局flag。但并不是所有的子命令都会使用这些flag。比如,query
子命令就不需要--orderer
flag。
每个flag都会和与其相关的子命令一起来讲解。全局flag包括
--cafile <string>
是通往一文件的路径,该文件包含了用于排序端点的PEM编码受信任证书
--certfile <string>
是通往一文件的路径,该文件包含了用于和orderer端点进行相互TLS通信的PEM编码X509公钥。
--keyfile <string>
是通往一文件的路径,该文件包含了用于和orderer端点进行相互TLS通信的PEM编码私钥
-o
和--orderer <string>
排序服务端点被指明为
<hostname or IP address>:<port>
--ordererTLSHostnameOverride <string>
验证与orderer的TLS连接时要用到的主机名覆盖
--tls
当与orderer端点通信时用TLS
--transient <string>
JSON编码中的参数的瞬时映射
节点链码安装¶
Install a chaincode on a peer. This installs a chaincode deployment spec package (if provided) or packages the specified chaincode before subsequently installing it.
Usage:
peer chaincode install [flags]
Flags:
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-h, --help help for install
-l, --lang string Language the chaincode is written in (default "golang")
-n, --name string Name of the chaincode
-p, --path string Path to chaincode
--peerAddresses stringArray The addresses of the peers to connect to
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
-v, --version string Version of the chaincode specified in install/instantiate/upgrade commands
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Transient map of arguments in JS
节点链码实例化¶
Deploy the specified chaincode to the network.
Usage:
peer chaincode instantiate [flags]
Flags:
-C, --channelID string The channel on which this command should be executed
--collections-config string The fully qualified path to the collection JSON file including the file name
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-E, --escc string The name of the endorsement system chaincode to be used for this chaincode
-h, --help help for instantiate
-l, --lang string Language the chaincode is written in (default "golang")
-n, --name string Name of the chaincode
--peerAddresses stringArray The addresses of the peers to connect to
-P, --policy string The endorsement policy associated to this chaincode
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
-v, --version string Version of the chaincode specified in install/instantiate/upgrade commands
-V, --vscc string The name of the verification system chaincode to be used for this chaincode
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Tr
节点链码调用¶
Invoke the specified chaincode. It will try to commit the endorsed transaction to the network.
Usage:
peer chaincode invoke [flags]
Flags:
-C, --channelID string The channel on which this command should be executed
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-h, --help help for invoke
-I, --isInit Is this invocation for init (useful for supporting legacy chaincodes in the new lifecycle)
-n, --name string Name of the chaincode
--peerAddresses stringArray The addresses of the peers to connect to
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
--waitForEvent Whether to wait for the event from each peer's deliver filtered service signifying that the 'invoke' transaction has been committed successfully
--waitForEventTimeout duration Time to wait for the event from each peer's deliver filtered service signifying that the 'invoke' transaction has been committed successfully (default 30s)
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string
节点链码列表¶
Get the instantiated chaincodes in the channel if specify channel, or get installed chaincodes on the peer
Usage:
peer chaincode list [flags]
Flags:
-C, --channelID string The channel on which this command should be executed
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-h, --help help for list
--installed Get the installed chaincodes on a peer
--instantiated Get the instantiated chaincodes on a channel
--peerAddresses stringArray The addresses of the peers to connect to
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string
节点链码包装¶
Package a chaincode and write the package to a file.
Usage:
peer chaincode package [outputfile] [flags]
Flags:
-s, --cc-package create CC deployment spec for owner endorsements instead of raw CC deployment spec
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-h, --help help for package
-i, --instantiate-policy string instantiation policy for the chaincode
-l, --lang string Language the chaincode is written in (default "golang")
-n, --name string Name of the chaincode
-p, --path string Path to chaincode
-S, --sign if creating CC deployment spec package for owner endorsements, also sign it with local MSP
-v, --version string Version of the chaincode specified in install/instantiate/upgrade commands
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Transient map of arguments in JSON encoding
节点链码查询¶
Get endorsed result of chaincode function call and print it. It won't generate transaction.
Usage:
peer chaincode query [flags]
Flags:
-C, --channelID string The channel on which this command should be executed
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-h, --help help for query
-x, --hex If true, output the query value byte array in hexadecimal. Incompatible with --raw
-n, --name string Name of the chaincode
--peerAddresses stringArray The addresses of the peers to connect to
-r, --raw If true, output the query value as raw bytes, otherwise format as a printable string
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Transient map of arguments in JSON encoding
节点链码签名包¶
Sign the specified chaincode package
Usage:
peer chaincode signpackage [flags]
Flags:
-h, --help help for signpackage
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Transient map of arguments in JSON encoding
节点链码升级¶
Upgrade an existing chaincode with the specified one. The new chaincode will immediately replace the existing chaincode upon the transaction committed.
Usage:
peer chaincode upgrade [flags]
Flags:
-C, --channelID string The channel on which this command should be executed
--collections-config string The fully qualified path to the collection JSON file including the file name
--connectionProfile string Connection profile that provides the necessary connection information for the network. Note: currently only supported for providing peer connection information
-c, --ctor string Constructor message for the chaincode in JSON format (default "{}")
-E, --escc string The name of the endorsement system chaincode to be used for this chaincode
-h, --help help for upgrade
-l, --lang string Language the chaincode is written in (default "golang")
-n, --name string Name of the chaincode
-p, --path string Path to chaincode
--peerAddresses stringArray The addresses of the peers to connect to
-P, --policy string The endorsement policy associated to this chaincode
--tlsRootCertFiles stringArray If TLS is enabled, the paths to the TLS root cert files of the peers to connect to. The order and number of certs specified should match the --peerAddresses flag
-v, --version string Version of the chaincode specified in install/instantiate/upgrade commands
-V, --vscc string The name of the verification system chaincode to be used for this chaincode
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
--transient string Transient map of arguments in JSON encoding
范例运用¶
节点链码实例化范例¶
以下是peer chaincode instantiate
命令的一些例子,它们在1.0
版本中mychannel
通道上将名为mycc
的链码实例化:
用
--tls
和--cafile
全局变量来对网络中的链码实例化,其中TLS被启用:export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer chaincode instantiate -o orderer.example.com:7050 --tls --cafile $ORDERER_CA -C mychannel -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')" 2018-02-22 16:33:53.324 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc 2018-02-22 16:33:53.324 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc 2018-02-22 16:34:08.698 UTC [main] main -> INFO 003 Exiting.....
仅用命令专门选项来将网络中的链码实例化,其中TLS未被启用:
peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n mycc -v 1.0 -c '{"Args":["init","a","100","b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')" 2018-02-22 16:34:09.324 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc 2018-02-22 16:34:09.324 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc 2018-02-22 16:34:24.698 UTC [main] main -> INFO 003 Exiting.....
节点链码调用范例¶
以下是peer chaincode invoke
命令的一个范例:
调用版本
1.0
名为mycc
的链码,该链码位于peer0.org1.example.com:7051
和peer0.org2.example.com:9051
(节点由 –peerAddresses 上的通道mychannel
中,请求从变量a
中转移10个单位到变量b
中:peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n mycc --peerAddresses peer0.org1.example.com:7051 --peerAddresses peer0.org2.example.com:9051 -c '{"Args":["invoke","a","b","10"]}' 2018-02-22 16:34:27.069 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc 2018-02-22 16:34:27.069 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc . . . 2018-02-22 16:34:27.106 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> DEBU 00a ESCC invoke result: version:1 response:<status:200 message:"OK" > payload:"\n \237mM\376? [\214\002 \332\204\035\275q\227\2132A\n\204&\2106\037W|\346#\3413\274\022Y\nE\022\024\n\004lscc\022\014\n\n\n\004mycc\022\002\010\003\022-\n\004mycc\022%\n\007\n\001a\022\002\010\003\n\007\n\001b\022\002\010\003\032\007\n\001a\032\00290\032\010\n\001b\032\003210\032\003\010\310\001\"\013\022\004mycc\032\0031.0" endorsement:<endorser:"\n\007Org1MSP\022\262\006-----BEGIN CERTIFICATE-----\nMIICLjCCAdWgAwIBAgIRAJYomxY2cqHA/fbRnH5a/bwwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTgwMjIyMTYyODE0WhcNMjgwMjIwMTYyODE0\nWjBwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzETMBEGA1UECxMKRmFicmljUGVlcjEfMB0GA1UEAxMWcGVl\ncjAub3JnMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDEa\nWNNniN3qOCQL89BGWfY39f5V3o1pi//7JFDHATJXtLgJhkK5KosDdHuKLYbCqvge\n46u3AC16MZyJRvKBiw6jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA\nMCsGA1UdIwQkMCKAIN7dJR9dimkFtkus0R5pAOlRz5SA3FB5t8Eaxl9A7lkgMAoG\nCCqGSM49BAMCA0cAMEQCIC2DAsO9QZzQmKi8OOKwcCh9Gd01YmWIN3oVmaCRr8C7\nAiAlQffq2JFlbh6OWURGOko6RckizG8oVOldZG/Xj3C8lA==\n-----END CERTIFICATE-----\n" signature:"0D\002 \022_\342\350\344\231G&\237\n\244\375\302J\220l\302\345\210\335D\250y\253P\0214:\221e\332@\002 \000\254\361\224\247\210\214L\277\370\222\213\217\301\r\341v\227\265\277\336\256^\217\336\005y*\321\023\025\367" > 2018-02-22 16:34:27.107 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 00b Chaincode invoke successful. result: status:200 2018-02-22 16:34:27.107 UTC [main] main -> INFO 00c Exiting.....
现在你就能看到该调用在日志信息的基础上被成功上传了:
2018-02-22 16:34:27.107 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 00b Chaincode invoke successful. result: status:200
一个成功的回复反映了该交易被成功提交至排序服务。随后,该交易会被添加至区块中,最后,接受通道上每个节点的验证,若不通过则被视为无效。
节点链码列表范例¶
以下是peer chaincode list
命令的一些例子:
用
--installed
flag来对安装在节点上的链码进行列表。peer chaincode list --installed Get installed chaincodes on peer: Name: mycc, Version: 1.0, Path: github.com/hyperledger/fabric-samples/chaincode/abstore/go, Id: 8cc2730fdafd0b28ef734eac12b29df5fc98ad98bdb1b7e0ef96265c3d893d61 2018-02-22 17:07:13.476 UTC [main] main -> INFO 001 Exiting.....
可以看到该节点安装了一个名为
mycc
的链码,它是版本1.0
的。用
--instantiated
和-C
(通道ID)flag一起来对通道上被实例化的链码进行列表。peer chaincode list --instantiated -C mychannel Get instantiated chaincodes on channel mychannel: Name: mycc, Version: 1.0, Path: github.com/hyperledger/fabric-samples/chaincode/abstore/go, Escc: escc, Vscc: vscc 2018-02-22 17:07:42.969 UTC [main] main -> INFO 001 Exiting.....
现在你能看到版本
1.0
的链码mycc
被在mychannel
通道上实例化了。
节点链码包实例¶
以下是peer chaincode package
命令的一个例子,它打包了版本为1.0
名为mycc
的链码,生成了链码部署规定,用本地MSP签署了该包装,同时还将其输出为ccpack.out
:
peer chaincode package ccpack.out -n mycc -p github.com/hyperledger/fabric-samples/chaincode/abstore/go -v 1.1 -s -S
.
.
.
2018-02-22 17:27:01.404 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc
2018-02-22 17:27:01.405 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc
.
.
.
2018-02-22 17:27:01.879 UTC [chaincodeCmd] chaincodePackage -> DEBU 011 Packaged chaincode into deployment spec of size <3426>, with args = [ccpack.out]
2018-02-22 17:27:01.879 UTC [main] main -> INFO 012 Exiting.....
节点链码查询范例¶
以下是peer chaincode query
命令的示例,该命令在节点账本上查询版本1.0
名为mycc
的链码,查询变量a
的值:
从输出中可看出变量
a
在查询时有一个值是90。peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}' 2018-02-22 16:34:30.816 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc 2018-02-22 16:34:30.816 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc Query Result: 90
节点链码签名包示例¶
以下是peer chaincode signpackage
命令的示例,它接受了现存的签名包,还创建了一个新的签名包,上面有本地MSP追加的一个签名。
peer chaincode signpackage ccwith1sig.pak ccwith2sig.pak
Wrote signed package to ccwith2sig.pak successfully
2018-02-24 19:32:47.189 EST [main] main -> INFO 002 Exiting.....
节点链码升级示例¶
以下是peer chaincode upgrade
命令的示例,它在通道mychannel
上将版本1.1
名为mycc
的链码升级成版本1.2
,其中包含了一个新的变量c
:
启用TLS,用
--tls
和--cafile
global flags来升级网络中的链码:export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem peer chaincode upgrade -o orderer.example.com:7050 --tls --cafile $ORDERER_CA -C mychannel -n mycc -v 1.2 -c '{"Args":["init","a","100","b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')" . . . 2018-02-22 18:26:31.433 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc 2018-02-22 18:26:31.434 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc 2018-02-22 18:26:31.435 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode enabled 2018-02-22 18:26:31.435 UTC [chaincodeCmd] upgrade -> DEBU 006 Get upgrade proposal for chaincode <name:"mycc" version:"1.1" > . . . 2018-02-22 18:26:46.687 UTC [chaincodeCmd] upgrade -> DEBU 009 endorse upgrade proposal, get response <status:200 message:"OK" payload:"\n\004mycc\022\0031.1\032\004escc\"\004vscc*,\022\014\022\n\010\001\022\002\010\000\022\002\010\001\032\r\022\013\n\007Org1MSP\020\003\032\r\022\013\n\007Org2MSP\020\0032f\n \261g(^v\021\220\240\332\251\014\204V\210P\310o\231\271\036\301\022\032\205fC[|=\215\372\223\022 \311b\025?\323N\343\325\032\005\365\236\001XKj\004E\351\007\247\265fu\305j\367\331\275\253\307R\032 \014H#\014\272!#\345\306s\323\371\350\364\006.\000\356\230\353\270\263\215\217\303\256\220i^\277\305\214: \375\200zY\275\203}\375\244\205\035\340\226]l!uE\334\273\214\214\020\303\3474\360\014\234-\006\315B\031\022\010\022\006\010\001\022\002\010\000\032\r\022\013\n\007Org1MSP\020\001" > . . . 2018-02-22 18:26:46.693 UTC [chaincodeCmd] upgrade -> DEBU 00c Get Signed envelope 2018-02-22 18:26:46.693 UTC [chaincodeCmd] chaincodeUpgrade -> DEBU 00d Send signed envelope to orderer 2018-02-22 18:26:46.908 UTC [main] main -> INFO 00e Exiting.....
不启用TLS,仅用命令专门选项来升级网络中的链码:
peer chaincode upgrade -o orderer.example.com:7050 -C mychannel -n mycc -v 1.2 -c '{"Args":["init","a","100","b","200"]}' -P "AND ('Org1MSP.peer','Org2MSP.peer')" . . . 2018-02-22 18:28:31.433 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003 Using default escc 2018-02-22 18:28:31.434 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004 Using default vscc 2018-02-22 18:28:31.435 UTC [chaincodeCmd] getChaincodeSpec -> DEBU 005 java chaincode enabled 2018-02-22 18:28:31.435 UTC [chaincodeCmd] upgrade -> DEBU 006 Get upgrade proposal for chaincode <name:"mycc" version:"1.1" > . . . 2018-02-22 18:28:46.687 UTC [chaincodeCmd] upgrade -> DEBU 009 endorse upgrade proposal, get response <status:200 message:"OK" payload:"\n\004mycc\022\0031.1\032\004escc\"\004vscc*,\022\014\022\n\010\001\022\002\010\000\022\002\010\001\032\r\022\013\n\007Org1MSP\020\003\032\r\022\013\n\007Org2MSP\020\0032f\n \261g(^v\021\220\240\332\251\014\204V\210P\310o\231\271\036\301\022\032\205fC[|=\215\372\223\022 \311b\025?\323N\343\325\032\005\365\236\001XKj\004E\351\007\247\265fu\305j\367\331\275\253\307R\032 \014H#\014\272!#\345\306s\323\371\350\364\006.\000\356\230\353\270\263\215\217\303\256\220i^\277\305\214: \375\200zY\275\203}\375\244\205\035\340\226]l!uE\334\273\214\214\020\303\3474\360\014\234-\006\315B\031\022\010\022\006\010\001\022\002\010\000\032\r\022\013\n\007Org1MSP\020\001" > . . . 2018-02-22 18:28:46.693 UTC [chaincodeCmd] upgrade -> DEBU 00c Get Signed envelope 2018-02-22 18:28:46.693 UTC [chaincodeCmd] chaincodeUpgrade -> DEBU 00d Send signed envelope to orderer 2018-02-22 18:28:46.908 UTC [main] main -> INFO 00e Exiting.....
peer channel¶
peer channel
命令允许管理员在 Peer 上执行通道相关的操作,比如加入通道,或者列出当前 Peer 加入的通道。
peer channel¶
Operate a channel: create|fetch|join|list|update|signconfigtx|getinfo.
Usage:
peer channel [command]
Available Commands:
create Create a channel
fetch Fetch a block
getinfo get blockchain information of a specified channel.
join Joins the peer to a channel.
list List of channels peer has joined.
signconfigtx Signs a configtx update.
update Send a configtx update.
Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
-h, --help help for channel
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
Use "peer channel [command] --help" for more information about a command.
peer channel create¶
Create a channel and write the genesis block to a file.
Usage:
peer channel create [flags]
Flags:
-c, --channelID string In case of a newChain command, the channel ID to create. It must be all lower case, less than 250 characters long and match the regular expression: [a-z][a-z0-9.-]*
-f, --file string Configuration transaction file generated by a tool such as configtxgen for submitting to orderer
-h, --help help for create
--outputBlock string The path to write the genesis block for the channel. (default ./<channelID>.block)
-t, --timeout duration Channel creation timeout (default 10s)
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel fetch¶
Fetch a specified block, writing it to a file.
Usage:
peer channel fetch <newest|oldest|config|(number)> [outputfile] [flags]
Flags:
--bestEffort Whether fetch requests should ignore errors and return blocks on a best effort basis
-c, --channelID string In case of a newChain command, the channel ID to create. It must be all lower case, less than 250 characters long and match the regular expression: [a-z][a-z0-9.-]*
-h, --help help for fetch
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel getinfo¶
get blockchain information of a specified channel. Requires '-c'.
Usage:
peer channel getinfo [flags]
Flags:
-c, --channelID string In case of a newChain command, the channel ID to create. It must be all lower case, less than 250 characters long and match the regular expression: [a-z][a-z0-9.-]*
-h, --help help for getinfo
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel join¶
Joins the peer to a channel.
Usage:
peer channel join [flags]
Flags:
-b, --blockpath string Path to file containing genesis block
-h, --help help for join
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel list¶
List of channels peer has joined.
Usage:
peer channel list [flags]
Flags:
-h, --help help for list
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel signconfigtx¶
Signs the supplied configtx update file in place on the filesystem. Requires '-f'.
Usage:
peer channel signconfigtx [flags]
Flags:
-f, --file string Configuration transaction file generated by a tool such as configtxgen for submitting to orderer
-h, --help help for signconfigtx
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
peer channel update¶
Signs and sends the supplied configtx update file to the channel. Requires '-f', '-o', '-c'.
Usage:
peer channel update [flags]
Flags:
-c, --channelID string In case of a newChain command, the channel ID to create. It must be all lower case, less than 250 characters long and match the regular expression: [a-z][a-z0-9.-]*
-f, --file string Configuration transaction file generated by a tool such as configtxgen for submitting to orderer
-h, --help help for update
Global Flags:
--cafile string Path to file containing PEM-encoded trusted certificate(s) for the ordering endpoint
--certfile string Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the orderer endpoint
--clientauth Use mutual TLS when communicating with the orderer endpoint
--connTimeout duration Timeout for client to connect (default 3s)
--keyfile string Path to file containing PEM-encoded private key to use for mutual TLS communication with the orderer endpoint
-o, --orderer string Ordering service endpoint
--ordererTLSHostnameOverride string The hostname override to use when validating the TLS connection to the orderer.
--tls Use TLS when communicating with the orderer endpoint
使用样例¶
peer channel create 样例¶
本样例展示了 peer channel create
使用全局标识 --orderer
的用法。
使用
./createchannel.tx
中的配置交易创建样例通道mychannel
。使用排序节点orderer.example.com:7050
。peer channel create -c mychannel -f ./createchannel.txn --orderer orderer.example.com:7050 2018-02-25 08:23:57.548 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-25 08:23:57.626 UTC [channelCmd] InitCmdFactory -> INFO 019 Endorser and orderer connections initialized 2018-02-25 08:23:57.834 UTC [channelCmd] readBlock -> INFO 020 Received block: 0 2018-02-25 08:23:57.835 UTC [main] main -> INFO 021 Exiting.....
返回区块 0 代表已经成功创建通道。
下一个例子展示使用 peer channel create
的命令选项。
使用
orderer.example.com:7050
创建新的通道mychannel
,配置交易同样定义在./createchannel.tx
文件中。但多了通道创建等待30s的选项。peer channel create -c mychannel --orderer orderer.example.com:7050 -f ./createchannel.txn -t 30s 2018-02-23 06:31:58.568 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-23 06:31:58.669 UTC [channelCmd] InitCmdFactory -> INFO 019 Endorser and orderer connections initialized 2018-02-23 06:31:58.877 UTC [channelCmd] readBlock -> INFO 020 Received block: 0 2018-02-23 06:31:58.878 UTC [main] main -> INFO 021 Exiting..... ls -l -rw-r--r-- 1 root root 11982 Feb 25 12:24 mychannel.block
你可以看到输出了区块0,证明了
mychannel
创建成功了,区块0存在了本地目录,名字为mychanenl.block
。区块0通常被长尾 创世块,因为它包含了通道的初始配置。所有对通道的更新,都会创建配置区块存在通道的区块链上,并且新配置区块中的配置会取代老的配置。
peer channel fetch 样例¶
以下是 peer channel fetch
命令的样例.
使用
newest
选项获取指定通道的最新区块,并把区块保存到mychanenl.block
文件中。peer channel fetch newest mychannel.block -c mychannel --orderer orderer.example.com:7050 2018-02-25 13:10:16.137 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-25 13:10:16.144 UTC [channelCmd] readBlock -> INFO 00a Received block: 32 2018-02-25 13:10:16.145 UTC [main] main -> INFO 00b Exiting..... ls -l -rw-r--r-- 1 root root 11982 Feb 25 13:10 mychannel.block
你可以看到获取的区块高度是32,并且区块已经被写入到
mychanenl.block
文件中。使用
(block number)
获取指定的区块,并且保存到默认的区块文件,本例中区块号是16。peer channel fetch 16 -c mychannel --orderer orderer.example.com:7050 2018-02-25 13:46:50.296 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-25 13:46:50.302 UTC [channelCmd] readBlock -> INFO 00a Received block: 16 2018-02-25 13:46:50.302 UTC [main] main -> INFO 00b Exiting..... ls -l -rw-r--r-- 1 root root 11982 Feb 25 13:10 mychannel.block -rw-r--r-- 1 root root 4783 Feb 25 13:46 mychannel_16.block
你可以看到获取的区块高度是16,并且区块已经被写入到
mychanenl.block
文件中。对于配置区块,可以使用
configtxlator
命令解析区块文件。请查看该命令的帮助信息获取解析样例。用户交易区块同样可以被解析,但需要写一个专门的程序做这件事。
peer channel getinfo example¶
如下是 peer channel getinfo
命令的使用样例。
获取当前 Peer 节点上
mychannel
通道的信息。peer channel getinfo -c mychannel 2018-02-25 15:15:44.135 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized Blockchain info: {"height":5,"currentBlockHash":"JgK9lcaPUNmFb5Mp1qe1SVMsx3o/22Ct4+n5tejcXCw=","previousBlockHash":"f8lZXoAn3gF86zrFq7L1DzW2aKuabH9Ow6SIE5Y04a4="} 2018-02-25 15:15:44.139 UTC [main] main -> INFO 006 Exiting.....
你可以看到
mychannel
最新的区块是5,以及当前通道中,最近区块的加密哈希值。
peer channel join example¶
如下是 peer channel join
命令的例子.
把一个 Peer 加入到
./mychannel.genesis.block
定义的通道。本例中,通道配置块是之前通过peer channel fetch
命令获取的区块。peer channel join -b ./mychannel.genesis.block 2018-02-25 12:25:26.511 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-25 12:25:26.571 UTC [channelCmd] executeJoin -> INFO 006 Successfully submitted proposal to join channel 2018-02-25 12:25:26.571 UTC [main] main -> INFO 007 Exiting.....
你可以看到 Peer 已成功创建了加入通道的交易。
peer channel list example¶
如下是 peer channel list
命令的样例。
列出 Peer 加入的通道。
peer channel list 2018-02-25 14:21:20.361 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized Channels peers has joined: mychannel 2018-02-25 14:21:20.372 UTC [main] main -> INFO 006 Exiting.....
你可以看到 Peer 加入了
mychannel
通道.
peer channel signconfigtx example¶
如下是 peer channel signconfigtx
命令的样例。
为定义在
./updatechannel.txn
中的channel update
交易进行签名。样例在执行命令前后列出了配置交易文件。ls -l -rw-r--r-- 1 anthonyodowd staff 284 25 Feb 18:16 updatechannel.tx peer channel signconfigtx -f updatechannel.tx 2018-02-25 18:16:44.456 GMT [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized 2018-02-25 18:16:44.459 GMT [main] main -> INFO 002 Exiting..... ls -l -rw-r--r-- 1 anthonyodowd staff 2180 25 Feb 18:16 updatechannel.tx
你可以看到配置交易文件
updatechannel.tx
的大小从 284 字节增加到 2180 字节,说明 Peer 成功对配置交易文件进行了签名。
peer channel update example¶
如下是 peer channel update
命令的样例.
使用
./updatechannel.tx
中定义的配置交易更新mychannel
的配置。使用orderer.example.com:7050
作为排序节点,把配置交易发送给在通道中的所有 Peer,让它们更新本地通道的配置。peer channel update -c mychannel -f ./updatechannel.txn -o orderer.example.com:7050 2018-02-23 06:32:11.569 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and orderer connections initialized 2018-02-23 06:32:11.626 UTC [main] main -> INFO 010 Exiting.....
可以看到通道
mychannel
成功被更新。
This work is licensed under a Creative Commons Attribution 4.0 International License.
peer version¶
peer version
命令可以查看 peer 的版本信息。它会显示版本号、Commit SHA、Go 的版本、操作系统及架构和链码信息。例如:
peer:
Version: 1.4.0
Commit SHA: 0efc897
Go version: go1.11.1
OS/Arch: linux/amd64
Chaincode:
Base Image Version: 0.4.14
Base Docker Namespace: hyperledger
Base Docker Label: org.hyperledger.fabric
Docker Namespace: hyperledger
语法¶
Print current version of the fabric peer server.
Usage:
peer version [flags]
Flags:
-h, --help help for version
This work is licensed under a Creative Commons Attribution 4.0 International License.
节点日志记录¶
管理者可以使用peer logging
子命令来动态查看和配置节点旳日志等级。
语法¶
peer logging
命令包含以下子命令:
- getlogspec
- setlogspec
还包括以下已弃用的子命令,这些将不会出现在后期版本中:
- getlevel
- setlevel
- revertlevels
不同的子命令选项(getlogspec
, setlogspec
, getlevel
, setlevel
, 和revertlevels
) 涉及了与同节点相关的不同记录操作。
本章节将在每个节点日志子命令的部分分别描述各节点日志子命令及其选项。
节点日志记录¶
Logging configuration: getlevel|setlevel|getlogspec|setlogspec|revertlevels.
Usage:
peer logging [command]
Available Commands:
getlevel Returns the logging level of the requested logger.
getlogspec Returns the active log spec.
revertlevels Reverts the logging spec to the peer's spec at startup.
setlevel Adds the logger and log level to the current logging spec.
setlogspec Sets the logging spec.
Flags:
-h, --help help for logging
Use "peer logging [command] --help" for more information about a command.
节点日志记录getlevel¶
Returns the logging level of the requested logger. Note: the logger name should exactly match the name that is displayed in the logs.
Usage:
peer logging getlevel <logger> [flags]
Flags:
-h, --help help for getlevel
节点日志记录revertlevel¶
Reverts the logging spec to the peer's spec at startup.
Usage:
peer logging revertlevels [flags]
Flags:
-h, --help help for revertlevels
节点日志记录setlevel¶
Adds the logger and log level to the current logging specification.
Usage:
peer logging setlevel <logger> <log level> [flags]
Flags:
-h, --help help for setlevel
实例运用¶
获取级别运用¶
以下是 pee logging getlevel
命令的一个实例:
获取记录器 peer的日志级别
peer logging getlevel peer 2018-11-01 14:18:11.276 UTC [cli.logging] getLevel -> INFO 001 Current log level for logger 'peer': INFO
获取日志特定运用¶
以下是peer logging getlogspec
命令的一个实例:
获取节点的活跃日志记录规定:
peer logging getlogspec 2018-11-01 14:21:03.591 UTC [cli.logging] getLogSpec -> INFO 001 Current logging spec: info
设定级别运用¶
以下是 peer logging setlevel
命令的一些实例:
对记录器的日志级别进行设定,使记录器的名称前缀
gossip
对应日志级别WARNING
:peer logging setlevel gossip warning 2018-11-01 14:21:55.509 UTC [cli.logging] setLevel -> INFO 001 Log level set for logger name/prefix 'gossip': WARNING
仅将与所提供名完全匹配的记录器的日志级别设定为
ERROR
,并在该记录器的名称后加一个标点:peer logging setlevel gossip. error 2018-11-01 14:27:33.080 UTC [cli.logging] setLevel -> INFO 001 Log level set for logger name/prefix 'gossip.': ERROR
设定日志的特定运用¶
以下为 peer logging setlogspec
命令的一个实例:
以
gossip
和msp
开头的记录器被设置为日志级别 WARNING,为该记录器所在的peer设定活跃日志记录规范;除以gossip
和msp
开头的记录器以外的所有记录器的默认设置是日志级别INFO
:peer logging setlogspec gossip=warning:msp=warning:info 2018-11-01 14:32:12.871 UTC [cli.logging] setLogSpec -> INFO 001 Current logging spec set to: gossip=warning:msp=warning:info
注意:只存在一个活跃日志规范。包括经“setlevel”更新的模块在内的所有之前的规范都将不适用。
恢复级别运用¶
以下是 peer logging revertlevels
命令的一个实例:
将日志规范恢复到初始值:
peer logging revertlevels 2018-11-01 14:37:12.402 UTC [cli.logging] revertLevels -> INFO 001 Logging spec reverted to the peer's spec at startup.
peer node¶
管理员可以通过 peer node
命令来启动 Peer 节点或者检查节点状态。
peer node start¶
启动一个和网络交互的节点。
Usage:
peer node start [flags]
Flags:
-h, --help start 的帮助
--peer-chaincodedev 是否启动链码开发者模式
peer node status¶
返回正在运行的节点的状态。
Usage:
peer node status [flags]
Flags:
-h, --help status 的帮助
peer node reset¶
将通道重置到创世区块。执行该命令时,节点必须是离线的。当节点在重置之后启动时,它将会从排序节点或者其他 Peer 节点从1号区块开始获取区块,并重建区块存储和状态数据库。
Usage:
peer node reset [flags]
Flags:
-h, --help reset 的帮助
peer node rollback¶
从指定的区块号回滚通道。执行该命令时,节点必须是离线的。当节点在回滚之后启动时,它将会从排序节点或者其他 Peer 节点获取回滚过程中删除的区块,并重建区块存储和状态数据库。
Usage:
peer node rollback [flags]
Flags:
-b, --blockNumber uint 通道要回滚的区块序号
-c, --channelID string 要回滚的通道
-h, --help rollback 的帮助
示例用法¶
peer node start example¶
下边的命令:
peer node start --peer-chaincodedev
以开发者模式启动 Peer 节点。一般来说链码容器由 Peer 节点启动和维护。但是在链码的开发者模式下,链码通过用户来编译和启动。这个模式在链码开发阶段很有帮助。更多信息请查看开发模式下的链码教程。
This work is licensed under a Creative Commons Attribution 4.0 International License.
configtxgen¶
configtxgen
命令允许用户创建和查看通道配置相关构件。所生成的构件取决于 configtx.yaml
的内容。
语法¶
configtxgen
工具没有子命令,但是支持标识(flag),通过设置标识可以完成不同的任务。
configtxgen¶
Usage of configtxgen:
-asOrg string
以特定组织(按名称)执行配置生成,仅包括组织(可能)有权设置的写集中的值。
-channelCreateTxBaseProfile string
指定要视为排序系统通道当前状态的轮廓(profile),以允许在通道创建交易生成期间修改非应用程序参数。仅在与 “outputCreateChannelTX” 结合时有效。
-channelID string
配置交易中使用的通道 ID。
-configPath string
包含所用的配置的路径。(如果设置的话)
-inspectBlock string
打印指定路径的区块中包含的配置。
-inspectChannelCreateTx string
打印指定路径的交易中包含的配置。
-outputAnchorPeersUpdate string
创建一个更新锚节点的配置更新(仅在默认通道创建时有效,并仅用于第一次更新)。
-outputBlock string
写入创世区块的路径。(如果设置的话)
-outputCreateChannelTx string
写入通道创建交易的路径。(如果设置的话)
-printOrg string
以 JSON 方式打印组织的定义。(手动向通道中添加组织时很有用)
-profile string
configtx.yaml 中用于生成的轮廓。默认(“SampleInsecureSolo”)
-version
显示版本信息。
用法¶
输出创世区块¶
将通道 orderer-system-channel
和轮廓(Profile) SampleSingleMSPSoloV1_1
的创世区块写入 genesis_block.pb
。
configtxgen -outputBlock genesis_block.pb -profile SampleSingleMSPSoloV1_1 -channelID orderer-system-channel
输出创建通道的交易¶
将轮廓 SampleSingleMSPChannelV1_1
的通道创建交易写入 create_chan_tx.pb
。
configtxgen -outputCreateChannelTx create_chan_tx.pb -profile SampleSingleMSPChannelV1_1 -channelID application-channel-1
查看创建通道交易¶
将通道创建交易 create_chan_tx.pb
以 JSON 的格式打印到屏幕上。
configtxgen -inspectChannelCreateTx create_chan_tx.pb
打印组织定义¶
基于 configtx.yaml
的配置项(比如 MSPdir)来构建组织并以 JSON 格式打印到屏幕。(常用于创建通道时的重新配置,例如添加成员)
configtxgen -printOrg Org1
输出锚节点交易¶
将配置更新交易输出到 anchor_peer_tx.pb
,就是将组织 Org1 的锚节点设置成 configtx.yaml
中轮廓 SampleSingleMSPChannelV1_1 所定义的。
configtxgen -outputAnchorPeersUpdate anchor_peer_tx.pb -profile SampleSingleMSPChannelV1_1 -asOrg Org1
配置¶
configtxgen
工具的输出依赖于 configtx.yaml
。configtx.yaml
可在 FABRIC_CFG_PATH
下找到,且在 configtxgen
执行时必须存在。
这个配置文件可以被编辑,或者通过重写环境变量的方式修改一些单独的属性,例如 CONFIGTX_ORDERER_ORDERERTYPE=kafka
。
对许多 configtxgen
的操作来说,必须提供轮廓名(profile name)。使用轮廓可以在一个文件里描述多条相似的配置。例如,一个轮廓中可以定义含有3个组织的通道,另一个轮廓可能定义了含4个组织的通道。configtx.yaml
依赖 YAML 的锚点和引用特性从而避免文件变得繁重。配置中的基础部分使用锚点标记,例如 &OrdererDefaults
,然后合并到一个轮廓的引用,例如 <<: *OrdererDefaults
。要注意的是,当使用轮廓来执行 configtxgen
时,重写环境变量不必包含轮廓前缀,可以直接从引用轮廓的根元素开始引用。例如,不用指定 CONFIGTX_PROFILE_SAMPLEINSECURESOLO_ORDERER_ORDERERTYPE
, 而是省略轮廓的细节,使用 CONFIGTX
前缀,后面直接使用相对配置名后的元素,例如 CONFIGTX_ORDERER_ORDERERTYPE
。
参考 Fabric 中的示例 configtx.yaml
可以查看所有可能的配置选项。你可以在发布版本的 config
文件夹,或者源码的 sampleconfig
文件夹找到这个配置文件。
This work is licensed under a Creative Commons Attribution 4.0 International License.
configtxlator¶
configtxlator
命令允许用户在 protobuf 和 JSON 版本的 fabric 数据结构之间进行转换,并创建配置更新。该命令可以启动 REST 服务器并通过 HTTP 公开,也可以直接用作命令行工具。
configtxlator start¶
usage: configtxlator start [<flags>]
启动 configtxlator REST 服务。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--hostname="0.0.0.0" REST 服务器监听的主机名或者 IP。
--port=7059 REST 服务器监听的端口。
--CORS=CORS ... 允许跨域的域名, 例如 ‘*’ 或者 ‘www.example.com’ (可能是重复的)。
configtxlator proto_encode¶
usage: configtxlator proto_encode --type=TYPE [<flags>]
将 JSON 文档转换为 protobuf。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--type=TYPE 要将 protobuf 编码成的类型。例如‘common.Config’。
--input=/dev/stdin 包含 JSON 文档的文件。
--output=/dev/stdout 要将输出写入的文件。
configtxlator proto_decode¶
usage: configtxlator proto_decode --type=TYPE [<flags>]
将 proto 消息转换为 JSON。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--type=TYPE 要将 protobuf 解码成的类型。例如‘common.Config’。
--input=/dev/stdin 包含 proto 消息的文件。
--output=/dev/stdout 要将 JSON 文档写入的文件。
configtxlator compute_update¶
usage: configtxlator compute_update --channel_id=CHANNEL_ID [<flags>]
比较两个封送(marshaled)的 common.Config 信息,并计算它们的更新。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--original=ORIGINAL 原始配置信息。
--updated=UPDATED 更新的配置信息。
--channel_id=CHANNEL_ID 本次更新的通道名。
--output=/dev/stdout 要将 JSON 文档写入的文件。
configtxlator version¶
usage: configtxlator version
显示版本信息。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
示例¶
解码¶
将一个名为 fabric_block.pb
的块解码为 JSON 并打印到标准输出。
configtxlator proto_decode --input fabric_block.pb --type common.Block
或者,在启动 REST 服务器之后,使用下面的 curl 命令通过 REST API 执行相同的操作。
curl -X POST --data-binary @fabric_block.pb "${CONFIGTXLATOR_URL}/protolator/decode/common.Block"
编码¶
将策略的 JSON 文档从标准输入转换为名为 policy.pb
的文件。
configtxlator proto_encode --type common.Policy --output policy.pb
或者,在启动 REST 服务器之后,使用下面的 curl 命令通过 REST API 执行相同的操作。
curl -X POST --data-binary /dev/stdin "${CONFIGTXLATOR_URL}/protolator/encode/common.Policy" > policy.pb
Pipelines¶
从 original_config.pb
和 modified_config.pb
计算配置更新,然后将其解码为 JSON,再将其打印到标准输出。
configtxlator compute_update --channel_id testchan --original original_config.pb --updated modified_config.pb | configtxlator proto_decode --type common.ConfigUpdate
或者,在启动 REST 服务器之后,使用下面的 curl 命令通过 REST API 执行相同的操作。
curl -X POST -F channel=testchan -F "original=@original_config.pb" -F "updated=@modified_config.pb" "${CONFIGTXLATOR_URL}/configtxlator/compute/update-from-configs" | curl -X POST --data-binary /dev/stdin "${CONFIGTXLATOR_URL}/protolator/encode/common.ConfigUpdate"
附加笔记¶
该工具名称是 configtx 和 translator 的组合,旨在表示该工具只是在不同的等效数据表示之间进行转换。它不生成配置。它不提交或检索配置。它不修改配置本身,只是在 configtx 格式的不同视图之间提供一些双射操作。
对于 REST 服务器,既没有配置文件 configtxlator
,也没有包含任何身份验证或授权工具。因为 configtxlator
不能访问任何可能被认为敏感的数据、关键材料或其他信息,所以服务器的所有者将其暴露给其他客户端没有风险。但是,由于用户发送到 REST 服务器的数据可能是机密的,所以用户应该信任服务器的管理员、运行本地实例或者通过 CLI 进行操作。
This work is licensed under a Creative Commons Attribution 4.0 International License.
cryptogen¶
cryptogen
是用来生成 Hyperledger Fabric 密钥材料的工具。它为测试提供了一种预配置网络的工具。通常它不应使用在生产环境中。
cryptogen help¶
usage: cryptogen [<flags>] <command> [<args> ...]
生成 Hyperledger Fabric 密钥材料的工具。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
Commands:
help [<command>...]
显示帮助。
generate [<flags>]
生成密钥材料。
showtemplate
显示默认配置模板。
version
显示版本信息。
extend [<flags>]
扩展现有网络。
cryptogen generate¶
usage: cryptogen generate [<flags>]
生成密钥材料
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--output="crypto-config" 用来存放构件的输出目录。
--config=CONFIG 使用的配置模板。
cryptogen showtemplate¶
usage: cryptogen showtemplate
显示默认配置模板。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
cryptogen extend¶
usage: cryptogen extend [<flags>]
扩展现有网络。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
--input="crypto-config" 存放现有网络的输入目录。、existing network place
--config=CONFIG 使用的配置模板。
cryptogen version¶
usage: cryptogen version
显示版本信息。
Flags:
--help 查看完整的帮助(可以尝试 --help-long 和 --help-man)。
用法¶
这里有一个例子,在 cryptogen extend
命令上使用不同的标识(flag)。
cryptogen extend --input="crypto-config" --config=config.yaml
org3.example.com
这里 config.yaml 添加了一个新组织 org3.example.com
。
This work is licensed under a Creative Commons Attribution 4.0 International License.
服务发现命令行界面¶
发现服务有自己的命令行界面(CLI),它用YAML配置文件来对包括证书和私钥路径以及成员服务提供者身份证(MSP ID)在内的属性进行维持。
discover
命令有以下子命令:
- saveConfig
- peers
- config
- endorsers
下面展示的是该命令的用法:
usage: discover [<flags>] <command> [<args> ...]
Command line client for fabric discovery service
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
--configFile=CONFIGFILE Specifies the config file to load the configuration from
--peerTLSCA=PEERTLSCA Sets the TLS CA certificate file path that verifies the TLS peer's certificate
--tlsCert=TLSCERT (Optional) Sets the client TLS certificate file path that is used when the peer enforces client authentication
--tlsKey=TLSKEY (Optional) Sets the client TLS key file path that is used when the peer enforces client authentication
--userKey=USERKEY Sets the user's key file path that is used to sign messages sent to the peer
--userCert=USERCERT Sets the user's certificate file path that is used to authenticate the messages sent to the peer
--MSP=MSP Sets the MSP ID of the user, which represents the CA(s) that issued its user certificate
Commands:
help [<command>...]
Show help.
peers [<flags>]
Discover peers
config [<flags>]
Discover channel config
endorsers [<flags>]
Discover chaincode endorsers
saveConfig
Save the config passed by flags into the file specified by --configFile
配置外部端点¶
当前,若想在服务发现中看到节点,需要在其上配置 EXTERNAL_ENDPOINT
。否则,Fabric假定该节点不应被揭露。
要定义这些端点,就得在peer的 core.yaml
字段指明端点,把下面的样本端点换成你peer上的端点。
CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.example.com:8051
维持配置¶
要想维持配置,需要通过—configFile
flag和 saveConfig
命令来提供一个配置文件名:
discover --configFile conf.yaml --peerTLSCA tls/ca.crt --userKey msp/keystore/ea4f6a38ac7057b6fa9502c2f5f39f182e320f71f667749100fe7dd94c23ce43_sk --userCert msp/signcerts/User1\@org1.example.com-cert.pem --MSP Org1MSP saveConfig
通过执行以上命令可创建配置文件:
$ cat conf.yaml
version: 0
tlsconfig:
certpath: ""
keypath: ""
peercacertpath: /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/tls/ca.crt
timeout: 0s
signerconfig:
mspid: Org1MSP
identitypath: /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem
keypath: /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/ea4f6a38ac7057b6fa9502c2f5f39f182e320f71f667749100fe7dd94c23ce43_sk
当TLS(安全传输层协议)启动时运行peer,peer上的发现服务需要客户端凭借相互的TLS证书来与之相连,这就意味着,发现服务需要提供一个TLS证书。peer的默认配置为请求(但不验证)客户端的TLS证书,因此并不需要提供TLS证书(除非peer的tls.clientAuthRequired
被设置为true
)。
当发现CLI的配置文件有 peercacertpath
的证书路径,但是 certpath
和keypath
没有按以上方式进行配置——发现CLI生成一个自签名的TLS证书并用该证书与节点连接。
当未配置peercacertpath
,发现CLI与节点连接没有用到TLS证书。但由于信息是以纯文本形式进行传送,未经加密,因此极不推荐这种操作。
查询发现服务¶
发现CLI作为一个发现客户端,需要在peer上执行。此过程通过指明 --server flag
来实现。除此之外,查询是在通道范围内进行的,所以必须使用 --channel
flag。
唯一不需要通道的的查询是本地成员节点查询,默认情况下,本地成员节点查询只能由被查询节点的管理员使用。
发现CLI支持所有服务器端的查询:
- 节点成员查询
- 配置查询
- 背书者查询
我们一起来看看这些查询,了解一下它们是如何被调用和语法分析的:
节点成员查询:¶
$ discover --configFile conf.yaml peers --channel mychannel --server peer0.org1.example.com:7051
[
{
"MSPID": "Org2MSP",
"LedgerHeight": 5,
"Endpoint": "peer0.org2.example.com:9051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKTCCAc+gAwIBAgIRANK4WBck5gKuzTxVQIwhYMUwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzIuZXhhbXBsZS5jb20wHhcNMTgwNjE3MTM0NTIxWhcNMjgwNjE0MTM0NTIx\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjAub3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJa0gkMRqJCi\nzmx+L9xy/ecJNvdAV2zmSx5Sf2qospVAH1MYCHyudDEvkiRuBPgmCdOdwJsE0g+h\nz0nZdKq6/X+jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIFZMuZfUtY6n2iyxaVr3rl+x5lU0CdG9x7KAeYydQGTMMAoGCCqGSM49\nBAMCA0gAMEUCIQC0M9/LJ7j3I9NEPQ/B1BpnJP+UNPnGO2peVrM/mJ1nVgIgS1ZA\nA1tsxuDyllaQuHx2P+P9NDFdjXx5T08lZhxuWYM=\n-----END CERTIFICATE-----\n",
"Chaincodes": [
"mycc"
]
},
{
"MSPID": "Org2MSP",
"LedgerHeight": 5,
"Endpoint": "peer1.org2.example.com:10051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc+gAwIBAgIRALnNJzplCrYy4Y8CjZtqL7AwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzIuZXhhbXBsZS5jb20wHhcNMTgwNjE3MTM0NTIxWhcNMjgwNjE0MTM0NTIx\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjEub3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNDopAkHlDdu\nq10HEkdxvdpkbs7EJyqv1clvCt/YMn1hS6sM+bFDgkJKalG7s9Hg3URF0aGpy51R\nU+4F9Muo+XajTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIFZMuZfUtY6n2iyxaVr3rl+x5lU0CdG9x7KAeYydQGTMMAoGCCqGSM49\nBAMCA0cAMEQCIAR4fBmIBKW2jp0HbbabVepNtl1c7+6++riIrEBnoyIVAiBBvWmI\nyG02c5hu4wPAuVQMB7AU6tGSeYaWSAAo/ExunQ==\n-----END CERTIFICATE-----\n",
"Chaincodes": [
"mycc"
]
},
{
"MSPID": "Org1MSP",
"LedgerHeight": 5,
"Endpoint": "peer0.org1.example.com:7051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc6gAwIBAgIQP18LeXtEXGoN8pTqzXTHZTAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xODA2MTcxMzQ1MjFaFw0yODA2MTQxMzQ1MjFa\nMGoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ0wCwYDVQQLEwRwZWVyMR8wHQYDVQQDExZwZWVyMC5vcmcx\nLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKeC/1Rg/ynSk\nNNItaMlaCDZOaQvxJEl6o3fqx1PVFlfXE4NarY3OO1N3YZI41hWWoXksSwJu/35S\nM7wMEzw+3KNNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0j\nBCQwIoAgcecTOxTes6rfgyxHH6KIW7hsRAw2bhP9ikCHkvtv/RcwCgYIKoZIzj0E\nAwIDSAAwRQIhAKiJEv79XBmr8gGY6kHrGL0L3sq95E7IsCYzYdAQHj+DAiBPcBTg\nRuA0//Kq+3aHJ2T0KpKHqD3FfhZZolKDkcrkwQ==\n-----END CERTIFICATE-----\n",
"Chaincodes": [
"mycc"
]
},
{
"MSPID": "Org1MSP",
"LedgerHeight": 5,
"Endpoint": "peer1.org1.example.com:8051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICJzCCAc6gAwIBAgIQO7zMEHlMfRhnP6Xt65jwtDAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xODA2MTcxMzQ1MjFaFw0yODA2MTQxMzQ1MjFa\nMGoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ0wCwYDVQQLEwRwZWVyMR8wHQYDVQQDExZwZWVyMS5vcmcx\nLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoII9k8db/Q2g\nRHw5rk3SYw+OMFw9jNbsJJyC5ttJRvc12Dn7lQ8ZR9hW1vLQ3NtqO/couccDJcHg\nt47iHBNadaNNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0j\nBCQwIoAgcecTOxTes6rfgyxHH6KIW7hsRAw2bhP9ikCHkvtv/RcwCgYIKoZIzj0E\nAwIDRwAwRAIgGHGtRVxcFVeMQr9yRlebs23OXEECNo6hNqd/4ChLwwoCIBFKFd6t\nlL5BVzVMGQyXWcZGrjFgl4+fDrwjmMe+jAfa\n-----END CERTIFICATE-----\n",
"Chaincodes": null
}
]
如上可见,该命令输出了一个JSON,其中包含了被查询节点所在通道上所有节点的成员信息。
被返回的 Identity
是节点的成员增加证书,可被jq
和 openssl
的组合进行语法分析:
$ discover --configFile conf.yaml peers --channel mychannel --server peer0.org1.example.com:7051 | jq .[0].Identity | sed "s/\\\n/\n/g" | sed "s/\"//g" | openssl x509 -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
55:e9:3f:97:94:d5:74:db:e2:d6:99:3c:01:24:be:bf
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=US, ST=California, L=San Francisco, O=org2.example.com, CN=ca.org2.example.com
Validity
Not Before: Jun 9 11:58:28 2018 GMT
Not After : Jun 6 11:58:28 2028 GMT
Subject: C=US, ST=California, L=San Francisco, OU=peer, CN=peer0.org2.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:f5:69:7a:11:65:d9:85:96:65:b7:b7:1b:08:77:
43:de:cb:ad:3a:79:ec:cc:2a:bc:d7:93:68:ae:92:
1c:4b:d8:32:47:d6:3d:72:32:f1:f1:fb:26:e4:69:
c2:eb:c9:45:69:99:78:d7:68:a9:77:09:88:c6:53:
01:2a:c1:f8:c0
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
keyid:8E:58:82:C9:0A:11:10:A9:0B:93:03:EE:A0:54:42:F4:A3:EF:11:4C:82:B6:F9:CE:10:A2:1E:24:AB:13:82:A0
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:29:3f:55:2b:9f:7b:99:b2:cb:06:ca:15:3f:93:
a1:3d:65:5c:7b:79:a1:7a:d1:94:50:f0:cd:db:ea:61:81:7a:
02:20:3b:40:5b:60:51:3c:f8:0f:9b:fc:ae:fc:21:fd:c8:36:
a3:18:39:58:20:72:3d:1a:43:74:30:f3:56:01:aa:26
配置查询:¶
配置查询返回了从MSP(成员服务提供者)ID到orderer端点的映射,还返回了FabricMSPConfig
,它可被用来通过SDK验证所有peer和orderer:
$ discover --configFile conf.yaml config --channel mychannel --server peer0.org1.example.com:7051
{
"msps": {
"OrdererOrg": {
"name": "OrdererMSP",
"root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNMekNDQWRhZ0F3SUJBZ0lSQU1pWkxUb3RmMHR6VTRzNUdIdkQ0UjR3Q2dZSUtvWkl6ajBFQXdJd2FURUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMlY0WVcxd2JHVXVZMjl0TVJjd0ZRWURWUVFERXc1allTNWxlR0Z0CmNHeGxMbU52YlRBZUZ3MHhPREEyTURreE1UVTRNamhhRncweU9EQTJNRFl4TVRVNE1qaGFNR2t4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVFlXNGdSbkpoYm1OcApjMk52TVJRd0VnWURWUVFLRXd0bGVHRnRjR3hsTG1OdmJURVhNQlVHQTFVRUF4TU9ZMkV1WlhoaGJYQnNaUzVqCmIyMHdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUW9ySjVSamFTQUZRci9yc2xoMWdobnNCWEQKeDVsR1lXTUtFS1pDYXJDdkZBekE0bHUwb2NQd0IzNWJmTVN5bFJPVmdVdHF1ZU9IcFBNc2ZLNEFrWjR5bzE4dwpYVEFPQmdOVkhROEJBZjhFQkFNQ0FhWXdEd1lEVlIwbEJBZ3dCZ1lFVlIwbEFEQVBCZ05WSFJNQkFmOEVCVEFECkFRSC9NQ2tHQTFVZERnUWlCQ0JnbmZJd0pzNlBaWUZCclpZVkRpU05vSjNGZWNFWHYvN2xHL3QxVUJDbVREQUsKQmdncWhrak9QUVFEQWdOSEFEQkVBaUE5NGFkc21UK0hLalpFVVpnM0VkaWdSM296L3pEQkNhWUY3TEJUVXpuQgpEZ0lnYS9RZFNPQnk1TUx2c0lSNTFDN0N4UnR2NUM5V05WRVlmWk5SaGdXRXpoOD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
],
"admins": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNDVENDQWJDZ0F3SUJBZ0lRR2wzTjhaSzRDekRRQmZqYVpwMVF5VEFLQmdncWhrak9QUVFEQWpCcE1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVVNQklHQTFVRUNoTUxaWGhoYlhCc1pTNWpiMjB4RnpBVkJnTlZCQU1URG1OaExtVjRZVzF3CmJHVXVZMjl0TUI0WERURTRNRFl3T1RFeE5UZ3lPRm9YRFRJNE1EWXdOakV4TlRneU9Gb3dWakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNURFZOaGJpQkdjbUZ1WTJsegpZMjh4R2pBWUJnTlZCQU1NRVVGa2JXbHVRR1Y0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJCnpqMERBUWNEUWdBRWl2TXQybVdiQ2FHb1FZaWpka1BRM1NuTGFkMi8rV0FESEFYMnRGNWthMTBteG1OMEx3VysKdmE5U1dLMmJhRGY5RDQ2TVROZ2gycnRhUitNWXFWRm84Nk5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3RwpBMVVkRXdFQi93UUNNQUF3S3dZRFZSMGpCQ1F3SW9BZ1lKM3lNQ2JPajJXQlFhMldGUTRramFDZHhYbkJGNy8rCjVSdjdkVkFRcGt3d0NnWUlLb1pJemowRUF3SURSd0F3UkFJZ2RIc0pUcGM5T01DZ3JPVFRLTFNnU043UWk3MWIKSWpkdzE4MzJOeXFQZnJ3Q0lCOXBhSlRnL2R5ckNhWUx1ZndUbUtFSnZZMEtXVzcrRnJTeG5CTGdzZjJpCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
],
"crypto_config": {
"signature_hash_family": "SHA2",
"identity_identifier_hash_function": "SHA256"
},
"tls_root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNORENDQWR1Z0F3SUJBZ0lRZDdodzFIaHNZTXI2a25ETWJrZThTakFLQmdncWhrak9QUVFEQWpCc01Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVVNQklHQTFVRUNoTUxaWGhoYlhCc1pTNWpiMjB4R2pBWUJnTlZCQU1URVhSc2MyTmhMbVY0CllXMXdiR1V1WTI5dE1CNFhEVEU0TURZd09URXhOVGd5T0ZvWERUSTRNRFl3TmpFeE5UZ3lPRm93YkRFTE1Ba0cKQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdUQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjVERWTmhiaUJHY21GdQpZMmx6WTI4eEZEQVNCZ05WQkFvVEMyVjRZVzF3YkdVdVkyOXRNUm93R0FZRFZRUURFeEYwYkhOallTNWxlR0Z0CmNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQk9ZZGdpNm53a3pYcTBKQUF2cTIKZU5xNE5Ybi85L0VRaU13Tzc1dXdpTWJVbklYOGM1N2NYU2dQdy9NMUNVUGFwNmRyMldvTjA3RGhHb1B6ZXZaMwp1aFdqWHpCZE1BNEdBMVVkRHdFQi93UUVBd0lCcGpBUEJnTlZIU1VFQ0RBR0JnUlZIU1VBTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0tRWURWUjBPQkNJRUlCcW0xZW9aZy9qSW52Z1ZYR2cwbzVNamxrd2tSekRlalAzZkplbW8KU1hBek1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lEUG9FRkF5bFVYcEJOMnh4VEo0MVplaS9ZQWFvN29aL0tEMwpvTVBpQ3RTOUFpQmFiU1dNS3UwR1l4eXdsZkFwdi9CWitxUEJNS0JMNk5EQ1haUnpZZmtENEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
]
},
"Org1MSP": {
"name": "Org1MSP",
"root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQU1nN2VETnhwS0t0ZGl0TDRVNDRZMUl3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NUzVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQk41d040THpVNGRpcUZSWnB6d3FSVm9JbWw1MVh0YWkzbWgzUXo0UEZxWkhXTW9lZ0ovUWRNKzF4L3RobERPcwpnbmVRcndGd216WGpvSSszaHJUSmRuU2pYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSU9CZFFMRitjTVdhNmUxcDJDcE8KRXg3U0hVaW56VnZkNTVoTG03dzZ2NzJvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDQyt6T1lHcll0ZTB4SgpSbDVYdUxjUWJySW9UeHpsRnJLZWFNWnJXMnVaSkFJZ0NVVGU5MEl4aW55dk4wUkh4UFhoVGNJTFdEZzdLUEJOCmVrNW5TRlh3Y0lZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
],
"admins": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLakNDQWRDZ0F3SUJBZ0lRRTRFK0tqSHgwdTlzRSsxZUgrL1dOakFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTVM1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NUzVsZUdGdGNHeGxMbU52YlRBZUZ3MHhPREEyTURreE1UVTRNamhhRncweU9EQTJNRFl4TVRVNE1qaGEKTUd3eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVE4d0RRWURWUVFMRXdaamJHbGxiblF4SHpBZEJnTlZCQU1NRmtGa2JXbHVRRzl5Clp6RXVaWGhoYlhCc1pTNWpiMjB3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFqK01MZk1ESnUKQ2FlWjV5TDR2TnczaWp4ZUxjd2YwSHo1blFrbXVpSnFETjRhQ0ZwVitNTTVablFEQmx1dWRyUS80UFA1Sk1WeQpreWZsQ3pJa2NCNjdvMDB3U3pBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRFZSMFRBUUgvQkFJd0FEQXJCZ05WCkhTTUVKREFpZ0NEZ1hVQ3hmbkRGbXVudGFkZ3FUaE1lMGgxSXA4MWIzZWVZUzV1OE9yKzlxREFLQmdncWhrak8KUFFRREFnTklBREJGQWlFQXlJV21QcjlQakdpSk1QM1pVd05MRENnNnVwMlVQVXNJSzd2L2h3RVRra01DSUE0cQo3cHhQZy9VVldiamZYeE0wUCsvcTEzbXFFaFlYaVpTTXpoUENFNkNmCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
],
"crypto_config": {
"signature_hash_family": "SHA2",
"identity_identifier_hash_function": "SHA256"
},
"tls_root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTVENDQWUrZ0F3SUJBZ0lRZlRWTE9iTENVUjdxVEY3Z283UXgvakFLQmdncWhrak9QUVFEQWpCMk1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTVM1bGVHRnRjR3hsTG1OdmJURWZNQjBHQTFVRUF4TVdkR3h6ClkyRXViM0puTVM1bGVHRnRjR3hsTG1OdmJUQWVGdzB4T0RBMk1Ea3hNVFU0TWpoYUZ3MHlPREEyTURZeE1UVTQKTWpoYU1IWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSApFdzFUWVc0Z1JuSmhibU5wYzJOdk1Sa3dGd1lEVlFRS0V4QnZjbWN4TG1WNFlXMXdiR1V1WTI5dE1SOHdIUVlEClZRUURFeFowYkhOallTNXZjbWN4TG1WNFlXMXdiR1V1WTI5dE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMEQKQVFjRFFnQUVZbnp4bmMzVUpHS0ZLWDNUNmR0VGpkZnhJTVYybGhTVzNab0lWSW9mb04rWnNsWWp0d0g2ZXZXYgptTkZGQmRaYWExTjluaXRpbmxxbVVzTU1NQ2JieXFOZk1GMHdEZ1lEVlIwUEFRSC9CQVFEQWdHbU1BOEdBMVVkCkpRUUlNQVlHQkZVZEpRQXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QXBCZ05WSFE0RUlnUWdlVTAwNlNaUllUNDIKN1Uxb2YwL3RGdHUvRFVtazVLY3hnajFCaklJakduZ3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWhBSWpvcldJTwpRNVNjYjNoZDluRi9UamxWcmk1UHdTaDNVNmJaMFdYWEsxYzVBaUFlMmM5QmkyNFE1WjQ0aXQ1MkI5cm1hU1NpCkttM2NZVlY0cWJ6RFhMOHZYUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
],
"fabric_node_ous": {
"enable": true,
"client_ou_identifier": {
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQU1nN2VETnhwS0t0ZGl0TDRVNDRZMUl3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NUzVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQk41d040THpVNGRpcUZSWnB6d3FSVm9JbWw1MVh0YWkzbWgzUXo0UEZxWkhXTW9lZ0ovUWRNKzF4L3RobERPcwpnbmVRcndGd216WGpvSSszaHJUSmRuU2pYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSU9CZFFMRitjTVdhNmUxcDJDcE8KRXg3U0hVaW56VnZkNTVoTG03dzZ2NzJvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDQyt6T1lHcll0ZTB4SgpSbDVYdUxjUWJySW9UeHpsRnJLZWFNWnJXMnVaSkFJZ0NVVGU5MEl4aW55dk4wUkh4UFhoVGNJTFdEZzdLUEJOCmVrNW5TRlh3Y0lZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"organizational_unit_identifier": "client"
},
"peer_ou_identifier": {
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQU1nN2VETnhwS0t0ZGl0TDRVNDRZMUl3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NUzVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQk41d040THpVNGRpcUZSWnB6d3FSVm9JbWw1MVh0YWkzbWgzUXo0UEZxWkhXTW9lZ0ovUWRNKzF4L3RobERPcwpnbmVRcndGd216WGpvSSszaHJUSmRuU2pYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSU9CZFFMRitjTVdhNmUxcDJDcE8KRXg3U0hVaW56VnZkNTVoTG03dzZ2NzJvTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDQyt6T1lHcll0ZTB4SgpSbDVYdUxjUWJySW9UeHpsRnJLZWFNWnJXMnVaSkFJZ0NVVGU5MEl4aW55dk4wUkh4UFhoVGNJTFdEZzdLUEJOCmVrNW5TRlh3Y0lZPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"organizational_unit_identifier": "peer"
}
}
},
"Org2MSP": {
"name": "Org2MSP",
"root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQUx2SWV2KzE4Vm9LZFR2V1RLNCtaZ2d3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1pNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkhUS01aall0TDdnSXZ0ekN4Y2pMQit4NlZNdENzVW0wbExIcGtIeDFQaW5LUU1ybzFJWWNIMEpGVmdFempvSQpCcUdMYURyQmhWQkpoS1kwS21kMUJJZWpYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSUk1WWdza0tFUkNwQzVNRDdxQlUKUXZTajd4Rk1ncmI1emhDaUhpU3JFNEtnTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDWnNSUjVBVU5KUjdJbwpQQzgzUCt1UlF1RmpUYS94eitzVkpZYnBsNEh1Z1FJZ0QzUlhuQWFqaGlPMU1EL1JzSC9JN2FPL1RuWUxkQUl6Cnd4VlNJenhQbWd3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="
],
"admins": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLVENDQWRDZ0F3SUJBZ0lRU1lpeE1vdmpoM1N2c25WMmFUOXl1REFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTWk1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NaTVsZUdGdGNHeGxMbU52YlRBZUZ3MHhPREEyTURreE1UVTRNamhhRncweU9EQTJNRFl4TVRVNE1qaGEKTUd3eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVE4d0RRWURWUVFMRXdaamJHbGxiblF4SHpBZEJnTlZCQU1NRmtGa2JXbHVRRzl5Clp6SXVaWGhoYlhCc1pTNWpiMjB3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVJFdStKc3l3QlQKdkFYUUdwT2FuS3ZkOVhCNlMxVGU4NTJ2L0xRODVWM1Rld0hlYXZXeGUydUszYTBvRHA5WDV5SlJ4YXN2b2hCcwpOMGJIRWErV1ZFQjdvMDB3U3pBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRFZSMFRBUUgvQkFJd0FEQXJCZ05WCkhTTUVKREFpZ0NDT1dJTEpDaEVRcVF1VEErNmdWRUwwbys4UlRJSzIrYzRRb2g0a3F4T0NvREFLQmdncWhrak8KUFFRREFnTkhBREJFQWlCVUFsRStvbFBjMTZBMitmNVBRSmdTZFp0SjNPeXBieG9JVlhOdi90VUJ2QUlnVGFNcgo1K2k2TUxpaU9FZ0wzcWZSWmdkcG1yVm1SbHlIdVdabWE0NXdnaE09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
],
"crypto_config": {
"signature_hash_family": "SHA2",
"identity_identifier_hash_function": "SHA256"
},
"tls_root_certs": [
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTakNDQWZDZ0F3SUJBZ0lSQUtoUFFxUGZSYnVpSktqL0JRanQ3RXN3Q2dZSUtvWkl6ajBFQXdJd2RqRUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIekFkQmdOVkJBTVRGblJzCmMyTmhMbTl5WnpJdVpYaGhiWEJzWlM1amIyMHdIaGNOTVRnd05qQTVNVEUxT0RJNFdoY05Namd3TmpBMk1URTEKT0RJNFdqQjJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRQpCeE1OVTJGdUlFWnlZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTWk1bGVHRnRjR3hsTG1OdmJURWZNQjBHCkExVUVBeE1XZEd4elkyRXViM0puTWk1bGVHRnRjR3hsTG1OdmJUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDkKQXdFSEEwSUFCRVIrMnREOWdkME9NTlk5Y20rbllZR2NUeWszRStCMnBsWWxDL2ZVdGdUU0QyZUVyY2kyWmltdQo5N25YeUIrM0NwNFJwVjFIVHdaR0JMbmNnbVIyb1J5alh6QmRNQTRHQTFVZER3RUIvd1FFQXdJQnBqQVBCZ05WCkhTVUVDREFHQmdSVkhTVUFNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdLUVlEVlIwT0JDSUVJUEN0V01JRFRtWC8KcGxseS8wNDI4eFRXZHlhazQybU9tbVNJSENCcnAyN0tNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUUNtN2xmVQpjbG91VHJrS2Z1YjhmdmdJTTU3QS85bW5IdzhpQnAycURtamZhUUlnSjkwcnRUV204YzVBbE93bFpyYkd0NWZMCjF6WXg5QW5DMTJBNnhOZDIzTG89Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
],
"fabric_node_ous": {
"enable": true,
"client_ou_identifier": {
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQUx2SWV2KzE4Vm9LZFR2V1RLNCtaZ2d3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1pNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkhUS01aall0TDdnSXZ0ekN4Y2pMQit4NlZNdENzVW0wbExIcGtIeDFQaW5LUU1ybzFJWWNIMEpGVmdFempvSQpCcUdMYURyQmhWQkpoS1kwS21kMUJJZWpYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSUk1WWdza0tFUkNwQzVNRDdxQlUKUXZTajd4Rk1ncmI1emhDaUhpU3JFNEtnTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDWnNSUjVBVU5KUjdJbwpQQzgzUCt1UlF1RmpUYS94eitzVkpZYnBsNEh1Z1FJZ0QzUlhuQWFqaGlPMU1EL1JzSC9JN2FPL1RuWUxkQUl6Cnd4VlNJenhQbWd3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"organizational_unit_identifier": "client"
},
"peer_ou_identifier": {
"certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNSRENDQWVxZ0F3SUJBZ0lSQUx2SWV2KzE4Vm9LZFR2V1RLNCtaZ2d3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGd3TmpBNU1URTFPREk0V2hjTk1qZ3dOakEyTVRFMU9ESTQKV2pCek1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFWk1CY0dBMVVFQ2hNUWIzSm5NaTVsZUdGdGNHeGxMbU52YlRFY01Cb0dBMVVFCkF4TVRZMkV1YjNKbk1pNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkhUS01aall0TDdnSXZ0ekN4Y2pMQit4NlZNdENzVW0wbExIcGtIeDFQaW5LUU1ybzFJWWNIMEpGVmdFempvSQpCcUdMYURyQmhWQkpoS1kwS21kMUJJZWpYekJkTUE0R0ExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhTVUVDREFHCkJnUlZIU1VBTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3S1FZRFZSME9CQ0lFSUk1WWdza0tFUkNwQzVNRDdxQlUKUXZTajd4Rk1ncmI1emhDaUhpU3JFNEtnTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFDWnNSUjVBVU5KUjdJbwpQQzgzUCt1UlF1RmpUYS94eitzVkpZYnBsNEh1Z1FJZ0QzUlhuQWFqaGlPMU1EL1JzSC9JN2FPL1RuWUxkQUl6Cnd4VlNJenhQbWd3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"organizational_unit_identifier": "peer"
}
}
},
"Org3MSP": {
"name": "Org3MSP",
"root_certs": [
"CgJPVQoEUm9sZQoMRW5yb2xsbWVudElEChBSZXZvY2F0aW9uSGFuZGxlEkQKIKoEXcq/psdYnMKCiT79N+dS1hM8k+SuzU1blOgTuN++EiBe2m3E+FjWLuQGMNRGRrEVTMqTvC4A/5jvCLv2ja1sZxpECiDBbI0kwetxAwFzHwb1hi8TlkGW3OofvuVzfFt9VlewcRIgyvsxG5/THdWyKJTdNx8Gle2hoCbVF0Y1/DQESBjGOGciRAog25fMyWps+FLOjzj1vIsGUyO457ri3YMvmUcycIH2FvQSICTtzaFvSPUiDtNtAVz+uetuB9kfmjUdUSQxjyXULOm2IkQKIO8FKzwoWwu8Mo77GNqnKFGCZaJL9tlrkdTuEMu9ujzbEiA4xtzo8oo8oEhFVsl6010mNoj1VuI0Wmz4tvUgXolCIiJECiDZcZPuwk/uaJMuVph7Dy/icgnAtVYHShET41O0Eh3Q5BIgy5q9VMQrch9VW5yajhY8dH1uA593gKd5kBqGdLfiXzAiRAogAnUYq/kwKzFfmIm/W4nZxi1kjG2C8NRjsYYBkeAOQ6wSIGyX5GGmwgvxgXXehNWBfijyNIJALGRVhO8YtBqr+vnrKogBCiDHR1XQsDbpcBoZFJ09V97zsIKNVTxjUow7/wwC+tq3oBIgSWT/peiO2BI0DecypKfgMpVR8DWXl8ZHSrPISsL3Mc8aINem9+BOezLwFKCbtVH1KAHIRLyyiNP+TkIKW6x9RkThIiAbIJCYU6O02EB8uX6rqLU/1lHxV0vtWdIsKCTLx2EZmDJECiCPXeyUyFzPS3iFv8CQUOLCPZxf6buZS5JlM6EE/gCRaxIgmF9GKPLLmEoA77+AU3J8Iwnu9pBxnaHtUlyf/F9p30c6RAogG7ENKWlOZ4aF0HprqXAjl++Iao7/iE8xeVcKRlmfq1ASIGtmmavDAVS2bw3zClQd4ZBD2DrqCBO9NPOcLNB0IWeIQiCjxTdbmcuBNINZYWe+5fWyI1oY9LavKzDVkdh+miu26EogY2uJtJGfKrQQjy+pgf9FdPMUk+8PNUBtH9LCD4bos7JSIPl6m5lEP/PRAmBaeTQLXdbMxIthxM2gw+Zkc5+IJEWX"
],
"intermediate_certs": [
"CtgCCkQKIP0UVivtH8NlnRNrZuuu6jpaj2ZbEB4/secGS57MfbINEiDSJweLUMIQSW12jugBQG81lIQflJWvi7vi925u+PU/+xJECiDgOGdNbAiGSoHmTjKhT22fqUqYLIVh+JBHetm4kF4skhIg9XTWRkUqtsfYKENzPgm7ZUSmCHNF8xH7Vnhuc1EpAUgaINwSnJKofiMoyDRZwUBhgfwMH9DJzMccvRVW7IvLMe/cIiCnlRj+mfNVAJGKthLgQBB/JKM14NbUeutyJtTgrmDDiCogme25qGvxJfgQNnzldMMicVyiI6YMfnoThAUyqsTzyXkqIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKiCZ7bmoa/El+BA2fOV0wyJxXKIjpgx+ehOEBTKqxPPJeSogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAESIFYUenRvjbmEh+37YHJrvFJt4lGq9ShtJ4kEBrfHArPjGgNPVTEqA09VMTL0ARKIAQog/gwzULTJbCAoVg9XfCiROs4cU5oSv4Q80iYWtonAnvsSIE6mYFdzisBU21rhxjfYE7kk3Xjih9A1idJp7TSjfmorGiBwIEbnxUKjs3Z3DXUSTj5R78skdY1hWEjpCbSBvtwn/yIgBVTjvNOIwpBC7qZJKX6yn4tMvoCCGpiz4BKBEUqtBJsaZzBlAjBwZ4WXYOttkhsNA2r94gBfLUdx/4VhW4hwUImcztlau1T14UlNzJolCNkdiLc9CqsCMQD6OBkgDWGq9UlhkK9dJBzU+RElcZdSfVV1hDbbqt+lFRWOzzEkZ+BXCR1k3xybz+o="
],
"admins": [
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVUYk13SEZteEpEMWR3SjE2K0hnVnRDZkpVRzdKK2FTYgorbkVvVmVkREVHYmtTc1owa1lraEpyYkx5SHlYZm15ZWV0ejFIUk1rWjRvMjdxRlMzTlVFb1J2QlM3RHJPWDJjCnZLaDRnbWhHTmlPbzRiWjFOVG9ZL2o3QnpqMFlMSXNlCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
]
}
},
"orderers": {
"OrdererOrg": {
"endpoint": [
{
"host": "orderer.example.com",
"port": 7050
}
]
}
}
}
值得注意的是,这里的证书是base64编码的,所以应该用类似以下的方法对其进行解码。
$ discover --configFile conf.yaml config --channel mychannel --server peer0.org1.example.com:7051 | jq .msps.OrdererOrg.root_certs[0] | sed "s/\"//g" | base64 --decode | openssl x509 -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
c8:99:2d:3a:2d:7f:4b:73:53:8b:39:18:7b:c3:e1:1e
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=US, ST=California, L=San Francisco, O=example.com, CN=ca.example.com
Validity
Not Before: Jun 9 11:58:28 2018 GMT
Not After : Jun 6 11:58:28 2028 GMT
Subject: C=US, ST=California, L=San Francisco, O=example.com, CN=ca.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:28:ac:9e:51:8d:a4:80:15:0a:ff:ae:c9:61:d6:
08:67:b0:15:c3:c7:99:46:61:63:0a:10:a6:42:6a:
b0:af:14:0c:c0:e2:5b:b4:a1:c3:f0:07:7e:5b:7c:
c4:b2:95:13:95:81:4b:6a:b9:e3:87:a4:f3:2c:7c:
ae:00:91:9e:32
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Certificate Sign, CRL Sign
X509v3 Extended Key Usage:
Any Extended Key Usage
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
60:9D:F2:30:26:CE:8F:65:81:41:AD:96:15:0E:24:8D:A0:9D:C5:79:C1:17:BF:FE:E5:1B:FB:75:50:10:A6:4C
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:3d:e1:a7:6c:99:3f:87:2a:36:44:51:98:37:11:
d8:a0:47:7a:33:ff:30:c1:09:a6:05:ec:b0:53:53:39:c1:0e:
02:20:6b:f4:1d:48:e0:72:e4:c2:ef:b0:84:79:d4:2e:c2:c5:
1b:6f:e4:2f:56:35:51:18:7d:93:51:86:05:84:ce:1f
背书者查询:¶
要想查询一个链码调用的背书者,必须提供额外的flag:
--chaincode
flag是必需的,它提供了链码名。要查询多链码的调用,必须对所有相关链码重复提供–chaincode
flag。--collection
被用来指明链码预计将使用的私有数据集合。若要把--chaincode
通过的链码映射到数据集合中,应使用以下语法:collection=CC:Collection1,Collection2,...
。
例如,某项链码调用导致了cc1和cc2被调用,同时cc2将该链码调用写入私有数据集合cc1,要想查询该项链码调用,必须指明:--chaincode=cc1 --chaincode=cc2 --collection=cc2:col1
以下显示的是当背书策略为 AND('Org1.peer', 'Org2.peer')
时,链码mycc的背书者查询的输出:
$ discover --configFile conf.yaml endorsers --channel mychannel --server peer0.org1.example.com:7051 --chaincode mycc
[
{
"Chaincode": "mycc",
"EndorsersByGroups": {
"G0": [
{
"MSPID": "Org1MSP",
"LedgerHeight": 5,
"Endpoint": "peer0.org1.example.com:7051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc+gAwIBAgIRANTiKfUVHVGnrYVzEy1ZSKIwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTgwNjA5MTE1ODI4WhcNMjgwNjA2MTE1ODI4\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjAub3Jn\nMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD8jGz1l5Rrw\n5UWqAYnc4JrR46mCYwHhHFgwydccuytb00ouD4rECiBsCaeZFr5tODAK70jFOP/k\n/CtORCDPQ02jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIOBdQLF+cMWa6e1p2CpOEx7SHUinzVvd55hLm7w6v72oMAoGCCqGSM49\nBAMCA0cAMEQCIC3bacbDYphXfHrNULxpV/zwD08t7hJxNe8MwgP8/48fAiBiC0cr\nu99oLsRNCFB7R3egyKg1YYao0KWTrr1T+rK9Bg==\n-----END CERTIFICATE-----\n"
}
],
"G1": [
{
"MSPID": "Org2MSP",
"LedgerHeight": 5,
"Endpoint": "peer1.org2.example.com:10051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc+gAwIBAgIRAIs6fFxk4Y5cJxSwTjyJ9A8wCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzIuZXhhbXBsZS5jb20wHhcNMTgwNjA5MTE1ODI4WhcNMjgwNjA2MTE1ODI4\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjEub3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOVFyWVmKZ25\nxDYV3xZBDX4gKQ7rAZfYgOu1djD9EHccZhJVPsdwSjbRsvrfs9Z8mMuwEeSWq/cq\n0cGrMKR93vKjTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAII5YgskKERCpC5MD7qBUQvSj7xFMgrb5zhCiHiSrE4KgMAoGCCqGSM49\nBAMCA0cAMEQCIDJmxseFul1GZ26djKa6jZ6zYYf6hchNF5xxMRWXpCnuAiBMf6JZ\njZjVM9F/OidQ2SBR7OZyMAzgXc5nAabWZpdkuQ==\n-----END CERTIFICATE-----\n"
},
{
"MSPID": "Org2MSP",
"LedgerHeight": 5,
"Endpoint": "peer0.org2.example.com:9051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICJzCCAc6gAwIBAgIQVek/l5TVdNvi1pk8ASS+vzAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMi5leGFtcGxlLmNvbTAeFw0xODA2MDkxMTU4MjhaFw0yODA2MDYxMTU4Mjha\nMGoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ0wCwYDVQQLEwRwZWVyMR8wHQYDVQQDExZwZWVyMC5vcmcy\nLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Wl6EWXZhZZl\nt7cbCHdD3sutOnnszCq815NorpIcS9gyR9Y9cjLx8fsm5GnC68lFaZl412ipdwmI\nxlMBKsH4wKNNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0j\nBCQwIoAgjliCyQoREKkLkwPuoFRC9KPvEUyCtvnOEKIeJKsTgqAwCgYIKoZIzj0E\nAwIDRwAwRAIgKT9VK597mbLLBsoVP5OhPWVce3mhetGUUPDN2+phgXoCIDtAW2BR\nPPgPm/yu/CH9yDajGDlYIHI9GkN0MPNWAaom\n-----END CERTIFICATE-----\n"
}
]
},
"Layouts": [
{
"quantities_by_group": {
"G0": 1,
"G1": 1
}
}
]
}
]
未使用配置文件¶
在没有配置文件的情况下也可以执行发现CLI,仅将所有需要的配置通过为命令行flag。以下是一个有关载入了管理员证书的本地节点成员查询的例子:
$ discover --peerTLSCA tls/ca.crt --userKey msp/keystore/cf31339d09e8311ac9ca5ed4e27a104a7f82f1e5904b3296a170ba4725ffde0d_sk --userCert msp/signcerts/Admin\@org1.example.com-cert.pem --MSP Org1MSP --tlsCert tls/client.crt --tlsKey tls/client.key peers --server peer0.org1.example.com:7051
[
{
"MSPID": "Org1MSP",
"Endpoint": "peer1.org1.example.com:8051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICJzCCAc6gAwIBAgIQO7zMEHlMfRhnP6Xt65jwtDAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xODA2MTcxMzQ1MjFaFw0yODA2MTQxMzQ1MjFa\nMGoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ0wCwYDVQQLEwRwZWVyMR8wHQYDVQQDExZwZWVyMS5vcmcx\nLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoII9k8db/Q2g\nRHw5rk3SYw+OMFw9jNbsJJyC5ttJRvc12Dn7lQ8ZR9hW1vLQ3NtqO/couccDJcHg\nt47iHBNadaNNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0j\nBCQwIoAgcecTOxTes6rfgyxHH6KIW7hsRAw2bhP9ikCHkvtv/RcwCgYIKoZIzj0E\nAwIDRwAwRAIgGHGtRVxcFVeMQr9yRlebs23OXEECNo6hNqd/4ChLwwoCIBFKFd6t\nlL5BVzVMGQyXWcZGrjFgl4+fDrwjmMe+jAfa\n-----END CERTIFICATE-----\n",
},
{
"MSPID": "Org1MSP",
"Endpoint": "peer0.org1.example.com:7051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc6gAwIBAgIQP18LeXtEXGoN8pTqzXTHZTAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xODA2MTcxMzQ1MjFaFw0yODA2MTQxMzQ1MjFa\nMGoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ0wCwYDVQQLEwRwZWVyMR8wHQYDVQQDExZwZWVyMC5vcmcx\nLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKeC/1Rg/ynSk\nNNItaMlaCDZOaQvxJEl6o3fqx1PVFlfXE4NarY3OO1N3YZI41hWWoXksSwJu/35S\nM7wMEzw+3KNNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0j\nBCQwIoAgcecTOxTes6rfgyxHH6KIW7hsRAw2bhP9ikCHkvtv/RcwCgYIKoZIzj0E\nAwIDSAAwRQIhAKiJEv79XBmr8gGY6kHrGL0L3sq95E7IsCYzYdAQHj+DAiBPcBTg\nRuA0//Kq+3aHJ2T0KpKHqD3FfhZZolKDkcrkwQ==\n-----END CERTIFICATE-----\n",
},
{
"MSPID": "Org2MSP",
"Endpoint": "peer0.org2.example.com:9051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKTCCAc+gAwIBAgIRANK4WBck5gKuzTxVQIwhYMUwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzIuZXhhbXBsZS5jb20wHhcNMTgwNjE3MTM0NTIxWhcNMjgwNjE0MTM0NTIx\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjAub3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJa0gkMRqJCi\nzmx+L9xy/ecJNvdAV2zmSx5Sf2qospVAH1MYCHyudDEvkiRuBPgmCdOdwJsE0g+h\nz0nZdKq6/X+jTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIFZMuZfUtY6n2iyxaVr3rl+x5lU0CdG9x7KAeYydQGTMMAoGCCqGSM49\nBAMCA0gAMEUCIQC0M9/LJ7j3I9NEPQ/B1BpnJP+UNPnGO2peVrM/mJ1nVgIgS1ZA\nA1tsxuDyllaQuHx2P+P9NDFdjXx5T08lZhxuWYM=\n-----END CERTIFICATE-----\n",
},
{
"MSPID": "Org2MSP",
"Endpoint": "peer1.org2.example.com:10051",
"Identity": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc+gAwIBAgIRALnNJzplCrYy4Y8CjZtqL7AwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzIuZXhhbXBsZS5jb20wHhcNMTgwNjE3MTM0NTIxWhcNMjgwNjE0MTM0NTIx\nWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzENMAsGA1UECxMEcGVlcjEfMB0GA1UEAxMWcGVlcjEub3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNDopAkHlDdu\nq10HEkdxvdpkbs7EJyqv1clvCt/YMn1hS6sM+bFDgkJKalG7s9Hg3URF0aGpy51R\nU+4F9Muo+XajTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIFZMuZfUtY6n2iyxaVr3rl+x5lU0CdG9x7KAeYydQGTMMAoGCCqGSM49\nBAMCA0cAMEQCIAR4fBmIBKW2jp0HbbabVepNtl1c7+6++riIrEBnoyIVAiBBvWmI\nyG02c5hu4wPAuVQMB7AU6tGSeYaWSAAo/ExunQ==\n-----END CERTIFICATE-----\n",
}
]
Fabric-CA 命令¶
Hyperledger Fabric CA 是 Hyperledger Fabric 的证书授权中心(Certificate Authority)。fabric-ca 客户端和服务端可以使用的命令如下:
架构参考¶
架构起源¶
注解
本文档描述了 Hyperledger Fabric v1.0的最初架构提案。虽然从概念上讲,Hyperledger Fabric 实现是从架构方案开始的,但是在实现过程中还是修改了一些细节。最初的架构方案是按照最初的准备提出的。有关架构的更准确的技术描述,请参阅 Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains 。
Hyperledger Fabric 架构有以下优势:
- 链码信任的灵活性。该架构将链码(区块链应用程序)的*信任假设*与排序的信任假设分开。换句话说,排序服务可能由一组节点(排序节点)提供,并可以容忍其中一些节点出现故障或不当行为,而且每个链码的背书者可能不同。
- 可扩展性。由于负责特定链码的背书节点与排序节点是无关的,因此系统的 扩展性 会比由相同的节点完成这些功能更好。特别是当不同的链码指定不同的背书节点时,这会让背书节点的链码互相隔离,并允许并行执行链码(背书)。因此,代价高昂的链码执行从排序服务的关键路径中删除了。
- 机密性。本架构有助于部署对其交易的内容和状态更新具有“机密性”要求的链码。
- 共识模块化。该架构是 模块化的,允许可插拔的共识算法(即排序服务)实现。
第一部分:与 Hyperledger Fabric v1相关的架构元素
- 系统架构
- 交易背书的基本流程
- 背书策略
第二部分:v1之后架构版本的元素
- 账本检查点(裁剪)
1. 系统架构¶
区块链是一个分布式系统,由许多相互通信的节点组成。区块链运行链码来保存状态和账本数据,并执行交易。链码是核心元素,因为交易是调用链码的操作。交易必须“背书”,只有背书的交易才可以提交并对状态产生影响。可能存在一个或多个用于管理方法和参数的特殊链码,称之为 系统链码。
1.1. 交易¶
交易可以分为两类:
- 部署交易,创建新的链码并将程序作为参数。当部署交易成功执行时,链码会被安装在区块链上。
- 调用交易,在部署的链码环境中执行操作。调用交易引用链码和它的方法。当成功时,链码执行指定的方法(这可能涉及修改相应的状态),并返回输出。
后边会讲到,部署交易是调用交易的特殊情况,创建新链码的部署交易是系统链码上的调用交易。
备注:本文档目前假设交易要么创建新的链码,要么调用一个已部署链码提供的操作。这个文档还没有描述:a)查询(只读)交易的优化(包含在v1中),b)对跨链码交易的支持(v1之后版本的特性)。
1.2. 区块链数据结构¶
1.2.1. 状态¶
区块链的最新状态(简称 状态)被建模为一个带有版本的键值存储(KVS),其中键是名称,值是任意的。这些条目由区块链上运行的链码(应用程序)通过 put
和 get
KVS 的方式维护。状态被持久地存储,并记录对状态的更新。注意,带有版本的 KVS 被用作状态模型,实现可以使用已有的 KVS,也可以使用 RDBMS 或任何其他解决方案。
更正式地说,状态 s
被映射为 K -> (V X N)
的一个元素,其中:
K
是键的集合V
是值的集合N
是一个带有版本号的有序集合。单映射函数next: N -> N
接收N
并返回下一个版本号。
V
和 N
都包含一个特殊的元素 ⊥ (空类型),以防 N
是最小值。初始时所有键都映射为(⊥, ⊥)。对于 s(k)=(v,ver)
,我们用 s(k).value
表示 v
, 用 s(k).version
表示 ver
。
KVS 操作模型如下:
put(k,v)
其中k
∈K
,v
∈V
,获取区块链状态s
并改变为s'
就是s'(k)=(v,next(s(k).version))
,当k'!=k
时s'(k')=s(k')
。get(k)
返回s(k)
。
状态由 Peer 节点维护,而不是排序节点和客户端。
状态隔离。KVS 中的键可以从它们的名称中识别出属于某个特定链码,因为只有特定链码的交易才可以修改属于这个链码的键。原则上,任何链码都可以读取属于其他链码的键。支持跨链码交易,可以修改属于两个或多个链码的状态,这是一个v1后续版本的特性。
1.2.2 账本¶
账本提供了一个可验证的历史记录,记录了在系统运行期间发生的所有成功的(有效的)和失败的(无效的)状态更改。
账本是由排序服务(参见第1.3.3节)构造的,它是(有效或无效的)交易的 区块 的完全有序哈希链。哈希链强制账本中的区块的顺序,每个区块包含一个完全有序的交易数组。这强制为所有交易指定了顺序。
账本保存在所有的 Peer 节点上,也可以选择保存在部分排序节点上。在排序节点的上下文中,我们将账本称为“排序节点账本”,而在 Peer 节点的上下文中,我们将账本称为“节点账本”。Peer 节点账本
与 排序节点账本
的不同之处在于,Peer 节点在本地维护一个位掩码,该位掩码将有效的交易与无效的交易区分开来(有关详细信息,请参阅XX节)。
如第 XX 节(v1后续版本特性)所述的,Peer 节点可以裁剪 节点账本
。排序节点维护“排序节点账本”以获得容错性和(“Peer 节点账本”的)可用性,并可以决定随时对其进行裁剪,前提是排序服务的属性得到了维护(参见第1.3.3节)。
账本允许节点重放所有交易的历史并重建状态。因此,1.2.1节中描述的状态是一个可选的数据结构。
1.3. 节点¶
节点是区块链的通信实体。“节点”只是一个逻辑功能,因为不同类型的多个节点可以运行在同一个物理服务器上。重要的是节点如何在“信任域”中分组并与控制它们的逻辑实体相关联。
有三种类型的节点:
- 客户端 或 提交客户端:是一个向背书节点提交实际交易调用并向排序服务广播交易提案的客户端。
- Peer 节点:是一个提交交易并维护状态和账本(参见第1.2节)副本的节点。此外,Peer 节点可以扮演一个特殊的 背书人 角色。
- 排序服务节点 或 排序节点:是一个运行实现交付担保的通信服务节点,例如原子性或总顺序广播。
接下来将更详细地解释节点的类型。
1.3.1. 客户端¶
客户端扮演了代表最终用户的实体。它必须连接到与区块链通信的 Peer 节点。客户端可以连接到它所选择的任何 Peer 节点。客户端创建并调用交易。
如第2节所详细介绍的,客户端同时与 Peer 节点和排序服务通信。
1.3.2. Peer 节点¶
Peer 节点接收来自排序服务 区块 形式的有序状态更新,并维护状态和账本。
Peer 节点还可以承担 背书节点 或 背书人 的特殊角色。背书节点 发生在一个特定的链码上,包括在提交事务之前 背书 交易。每个链码都可以指定一个 背书策略,该策略可以引用一组背书节点。策略为有效的交易背书定义了必要条件和充分条件(通常是一组背书人的签名),后面的第2和3节将对此进行讲解。在安装新链码的特殊部署交易的情况下,背书策略指定为系统链码的背书策略。
1.3.3. 排序服务节点(排序节点)¶
排序节点 来自于 排序服务 ,即提供交付担保的通信结构。排序服务可以以不同的方式实现:从集中式服务(例如,在开发和测试中使用)到针对不同网络和节点故障模型的分布式协议。
排序服务为客户端和 Peer 节点提供共享的 通信通道,为包含交易的消息提供广播服务。客户端连接到通道,并可以在通道上向所有 Peer 节点广播消息。该通道支持所有消息以 原子 方式传递,即消息通信是全顺序传递和(特定实现)可靠的。换句话说,通道将消息以相同的逻辑顺序输出给所有与之相连的 Peer 节点。这种原子通信保证在分布式系统中也称为 全顺序广播、原子广播 或 共识。所通信的消息是要包含在区块链状态中的候选交易。
分区(排序服务通道)。排序服务可能支持多个 通道,类似于发布者-订阅者消息系统的 主题。客户端可以连接到给定的通道,然后可以发送消息并获取到达的消息。通道可以看作是分区,连接到一个通道的客户端不知道其他通道的存在,但是客户端可以连接到多个通道。尽管 Hyperledger Fabric 中实现的一些排序服务支持多个通道,但为了简化表示,在本文档的其余部分中,我们假设排序服务由一个通道(主题)组成。
排序服务 API。Peer 节点通过排序服务提供的接口连接到通道。排序服务 API 由两个基本操作(通常称 异步事件)组成:
TODO 添加用于在客户端或 Peer 节点获取指定序列号的区块的 API 部分,。
broadcast(blob)
:客户端调用它来在通道上广播任意消息blob
。在 BFT 中,当向服务发送请求时,也称为request(blob)
。deliver(seqno, prevhash, blob)
:排序服务调用这个来向 Peer 节点发送消息blob
,该消息中包含非负整数序列号(seqno
)和上一个发送的blob
的哈希(prevhash
)。欢句话说,它是排序服务的输出事件。deliver()
在发布者-订阅者系统中称为notify()
,在 BFT 系统中称为commit()
。
账本和区块格式。账本(参见第1.2.2节)中包含了所有排序服务输出的数据。简单来说,它是一个 deliver(seqno, prevhash, blob)
事件的序列,而事件就是根据前面所说的 prevhash
计算的哈希链。
大多数时候,出于效率的考虑,排序服务不会输出单个交易(blob),而是在单个 deliver
事件中将交易分组并输出到 区块 中。在这种情况下,排序服务必须限定每个区块中交易的排序。区块中的交易数可以由排序服务动态选择。
为了便于讲解,下边我们定义了排序服务属性(本节剩余部分)并解释了交易背书工作流(第二节),其中我们假设每个 deliver
事件中只有一个交易。这些很容易扩展到区块上,根据上面提到的区块中交易的确定性顺序,假设一个区块的 deliver
事件对应于一个区块中的每个交易单独的 deliver
事件序列。
排序服务属性
排序服务(或原子广播通道)的保证规定了广播了什么消息,以及传递的消息之间存在什么关系。这些保证如下:
安全(一致性保证):只要 Peer 节点连接到通道的时间足够长(它们可以断开连接或崩溃,但会重新启动和重新连接),它们将看到一个 相同的 已交付的
(seqno, prevhash, blob)
消息序列。这意味着所有 Peer 节点都可以收到 相同顺序 的输出(deliver()
事件),并且相同的序列号都有 相同的内容 (blob
和prevhash
)。注意,这只是一个 逻辑顺序,一个 Peer 节点上的``deliver(seqno, prevhash, blob)`` 不需要与另一个 Peer 节点上输出相同的deliver(seqno, prevhash, blob)
的消息发生实时关联。换句话说,给定一个特定的seqno
,没有 两个正确的 Peer 节点会提供 不同的prevhash
或blob
值。此外,除非某个客户端(Peer 节点)实际调用了broadcast(blob)
,否则不会传递任何blob
,即每个广播过的 blob 只分发 一次。此外,
deliver()
事件包含前一个deliver()
事件中的数据的哈希(prevhash
)。当排序服务实现原子广播保证时,prevhash
是deliver()
事件的参数和序号seqno-1
的哈希。这将在deliver()
事件之间建立一个哈希链,用于帮助验证排序服务输出的完整性,第4和第5节将讨论这个。在第一个deliver()
事件中prevhash
有一个默认值。存活性(交付保证):排序服务的存活性保证由排序服务决定。准确的保证取决于网络和节点故障模型。
原则上,如果提交的客户端没有失败,那么排序服务应该确保连接到排序服务的每个正确的 Peer 节点最终会发送每个提交的交易。
总而言之,排序服务确保以下特性:
- 协议。对于任何两个正确的 Peer 节点上有相同
seqno
的事件deliver(seqno, prevhash0, blob0)
和deliver(seqno, prevhash1, blob1)
,prevhash0==prevhash1
并且blob0==blob1
; - 哈希链完整性。对于任何两个正确的 Peer 节点上的事件
deliver(seqno, prevhash0, blob0)
和deliver(seqno, prevhash1, blob1)
,prevhash = HASH(seqno-1||prevhash0||blob0)
- 不能跳跃。如果排序服务向正确的 Peer 节点 p 输出了
deliver(seqno, prevhash, blob)
,其中seqno>0
。那么 p 肯定已经接收到了deliver(seqno-1, prevhash0, blob0)
。 - 不能创建。正确的Peer 节点上的
deliver(seqno, prevhash, blob)
事件必须在一些(可能不是同一个) Peer 节点上通过broadcast(blob)
事件处理。 - 没有重复(可选,但最好存在)。对于两个事件
broadcast(blob)
和broadcast(blob')
,当两个事件deliver(seqno0, prevhash0, blob)
和deliver(seqno1, prevhash1, blob')
在正确的 Peer 上发生时,并且blob == blob'
,那么seqno0==seqno1
而且prevhash0==prevhash1
。 - 存活性。如果一个正确的客户端调用一个事件
broadcast(blob)
,那么每个正确的 Peer 节点“最终”都会发出一个事件deliver(*, *, blob)
,其中*
表示一个任意值。
2. 交易背书的基本流程¶
在下面的文章中,我们将概述交易请求的整体流程。
注: 注意以下协议并不假设所有交易都是确定性的,即它允许非确定性交易。
2.1. 客户端创建一个交易并将其发送给它所指定的背书节点¶
要执行一个交易,客户端需要向其指定的背书节点发送一个 PROPOSE
(提案)消息(可能不是同时。请参见2.1.2和2.3节)。客户端可以通过 Peer 节点根据背书策略(参阅第三章)得到给定 chaincodeID
的背书节点。例如,交易可以发送给指定 chaincodeID
的 所有 背书节点。也就是说,一些背书节点可以离线,其他的可能不同意或者不背书该交易。提交客户端可以尝试可用的背书节点来满足背书策略。
下边我们将首先详细介绍 PROPOSE
消息格式,然后讨论提交客户端和背书节点之间可能的交互模式。
2.1.1. PROPOSE
消息格式¶
PROPOSE
消息的格式是 <PROPOSE,tx,[anchor]>
,tx
是必选参数而 anchor
是可选参数,解释如下。
tx=<clientID,chaincodeID,txPayload,timestamp,clientSig>
,其中 -clientID
是提交客户端的 ID, -chaincodeID
是提交的交易所引用的链码, -txPayload
是提交的交易所包含的内容, -timestamp
是由客户端维护的单调递增(对于每一个新交易)的整数, -clientSig
是客户端对tx
其他字段的签名。在执行交易和部署交易中
txPayload
的细节所有不同,对于 执行交易,txPayload
包含两个字段:txPayload = <operation, metadata>
,其中 -operation
定义了链码方法和参数, -metadata
定义了执行相关的参数。
对于 部署交易,``txPayload`` 包含三个字段: -
txPayload = <source, metadata, policies>
, 其中source
定义了链码的源码,metadata
定义了相关的链码和应用程序,policies
包含了链码相关的策略,比如背书策略,它可以被所有 Peer 节点访问。 注意,在部署
交易中txPayload
不包含背书策略,但是包含背书策略 ID 和它的参数(参见第三章)。
anchor
包含了 读版本依赖项,具体来说就是“键值-版本”对(即anchor
是KxN
的子集),它将PROPOSE
请求绑定或者“锚定”在 KVS(参见 1.2)中指定的键的版本上。如果客户端指定了anchor
参数,背书节点仅在其本地 KVS 和anchor
对应键的 读 版本号相匹配时才背书交易(更多的细节参见第2.2节)。
所有节点都是用 tx
的哈希作为交易标识符 tid
,即 tid=HASH(tx)
。客户端将 tid
保存在内存中,等待背书节点的响应。
2.1.2. 消息模式¶
客户端决定和背书节点交互的顺序。例如,客户端通常会把 <PROPOSE, tx>``(即没有 ``anchor
参数)发送到一个节点,客户端稍后会将生成的版本依赖(anchor
)作为 提案
消息的参数发送到其他背书节点。另外一个例子就是,客户端也可以直接将 <PROPOSE, tx>``(没有 ``anchor
参数)直接发送给背书节点。客户端可以自由选择不同的通信模式(参看2.3节)。
2.2. 背书节点模拟交易并生成背书签名¶
当收到客户端发来的 <PROPOSE,tx,[anchor]>
消息后,背书节点 epID
首先验证客户端的签名 clientSig
然后再模拟交易。如果客户端指定了 anchor
,那么背书节点只会在它读取到本地 KVS 中 anchor
所指定键值的版本号(即后边会介绍的 readset
)后才会模拟交易。
模拟交易是背书节点调用交易中引用的链码(chaincodeID
)和背书节点本地的状态副本来 执行 交易 (txPayload
)的过程。
执行的结果是背书节点计算出来的 读版本依赖*(``readset``,读集)和 *状态更新 (writeset
,写集 ),在数据库语言中也称为 MVCC + postimage info。
回想一下,状态由键值对组成。所有键值条目都是带有版本的;也就是说,每个条目都含有有序的版本信息,每次更新键对应的值时,版本号都会递增。执行交易的节点保存着链码用于读取或写入的所有键值对,但节点还没有更新状态。具体来说:
- 在背书节点执行交易前给出一个状态
s
,其中保存着交易读取的所有键k
。(k,s(k).version)
被添加到readset
中。 - 另外,交易将所有键
k
的值改变为新值v'
,(k,v')
被添加到writeset
中。
如果客户端在 PROPOSE
消息中指定了 anchor
,那么客户端指定的 anchor
必须和背书节点模拟交易时的 readset
一致。
然后节点在内部根据 背书逻辑 向其他背书节点转发 tran-proposal
(或者叫做 tx
)。默认情况下节点的背书逻辑只接收并背书 tran-proposal
。然而背书逻辑可以解释任何功能,比如,以 tran-proposal
和 tx
作为输入和系统交互来判断是否能够背书一笔交易。
If endorsing logic decides to endorse a transaction, it sends
<TRANSACTION-ENDORSED, tid, tran-proposal,epSig>
message to the
submitting client(tx.clientID
), where:
如果背书逻辑决定背书一笔交易,它会发送 <TRANSACTION-ENDORSED, tid, tran-proposal,epSig>
消息给提交客户端 (tx.clientID
),其中:
tran-proposal := (epID,tid,chaincodeID,txContentBlob,readset,writeset)`, ``txContentBlob
是链码(交易)指定的信息。目的是让txContentBlob
和tx
有相同的表达方式(例如txContentBlob=tx.txPayload
)。epSig
是背书节点在tran-proposal
上的签名。
另外,当背书逻辑拒绝为交易背书时,背书节点 可能 会给提交客户端发送一个 (TRANSACTION-INVALID, tid, REJECTED)
消息。
注意,背书节点在这一步不会改变状态,在背书环境中模拟执行交易产生的结果不会影响状态!
2.3. 提交客户端收集交易背书并向排序服务广播¶
提交客户端一直等待接收到“足够多”的消息和 (TRANSACTION-ENDORSED, tid, *, *)
的签名后才可以确认交易提案背书完成了。就像在2.1.2节中所讨论的,这一步可能会和背书节点有多次交互。
“足够多”的含义取决于背书策略(参见第三章)。如果满足了背书策略,就表明交易被 背书 了。注意,还没有提交。从背书节点收集的 TRANSACTION-ENDORSED
消息的签名就称为 背书
。
如果提交客户端没有收到交易提案的背书,它就会放弃该交易,并且可以选择稍后重试。
对于成功背书的交易,我们现在就要开始使用排序服务了。提交客户端通过 broadcast(blob)
调用排序服务,其中 blob=endorsement
。如果客户端不能直接调用排序服务,它可以通过其他节点代理它的广播。这个节点必须是客户端信任的节点,确保节点不会从 endorsement
中删除任何信息,否则交易会被验证失败。需要提醒的是,代理节点无法伪造有效的 背书
。
2.4. 排序服务将交易发送给节点¶
当发生 deliver(seqno, prevhash, blob)
事件,并且节点上的状态已经更新到比 seqno
小的序号时,节点会有如下操作:
- 它会根据链码(
blob.tran-proposal.chaincodeID
)的背书策略来检查blob.endorsement
的有效性。 - 一般情况下,它还会验证依赖项(
blob.endorsement.tran-proposal.readset
)没有被改变。在更复杂的用例中,背书中的tran-proposal
字段可能会不一样,这时背书策略(参见第三章)会决定对状态的操作。
依赖项的验证根据状态更新选择的一致性属性或者“隔离保证”可以有多种不同实现。有序性 是默认的隔离保证,除非背书策略指定了一个。当所要求的 readset
中 每一个 键的版本和状态中键的版本一致的时候就提供了有序性,并将拒绝不符合要求的交易。
- 如果通过了所有检查,就认为交易是 有效的 或者是 已提交的。这时,节点会在
节点账本
的位掩码中将该交易标记为1,将blob.endorsement.tran-proposal.writeset
应用到区块链账本(如果tran-proposals
是一致的,否则背书策略逻辑会让函数验证blob.endorsement
)。 - 如果
blob.endorsement
的背书策略验证失败,交易就是无效的并且节点会在节点账本
的位掩码中将该交易标记为0。有必要提醒一下,无效交易不会改变状态。
注意,这足够使所有(正确的)节点在处理完给定序号的区块后得到相同的状态。也就是说,通过排序节点的保证,所有正确的节点都将收到相同顺序的 deliver(seqno, prevhash, blob)
事件。无论交易是否有效,通过背书策略和 读集
中的版本依赖节点都将得到一样的结果。因此,所有节点以同样的方式提交和应用相同顺序的交易来更新它们的状态。

Figure 1. 一般的交易流程示意图。
3. 背书策略¶
3.1. 背书策略说明¶
背书策略 是 背书 一笔交易的条件。区块链节点预置了一些背书策略,用来处理安装特定链码的 部署
交易。背书策略可以通过 部署
交易来指定。
为了保证安全性,背书策略 应该是一组被证实过的策略,其中包含一组有限的方法,以此确保执行时间可控,可以出现确定性结果,有良好的性能以及拥有安全保证。
动态添加背书策略(例如,在安装链码时的 部署
交易)会影响其安全性。目前不允许动态添加背书策略,以后会增加这项功能。
3.2. 根据背书策略的交易评估¶
只有当交易的背书满足背书策略时交易才是有效的。链码的执行交易会首先获得符合链码策略的 背书,否则不会被提交。这个过程发生在提交客户端和背书节点之间,详细过程参见第二章。
从形式上来讲,背书策略是背书的依据,并且背书策略更进一步的评估状态是正确的还是错误的。对于部署交易,背书包含在系统层面的策略中(例如,来自系统链码)。
背书策略的依据是引用特定的变量。它可能引用:
- 和链码相关的键或标示(可以在链码的元数据中找到),例如,背书者集合;
- 链码更进一步的元数据;
endorsement
和endorsement.tran-proposal
中的元素;- 其他元素。
上边列出的内容是根据易读性和负责性排序递增的顺序排序的,也就是说,只引用键和节点标示的策略会相对简单。
背书策略的评估标准必须是确定的。背书的评估可能在每一个本地节点上执行,这些节点 不 必要和其他节点交互,但所有正确的节点仍以相同的方式评估背书。
3.3. 背书策略示例¶
背书策略的条件可能会包含用来判断正确和错误的逻辑语句。一般来说,判断条件会使用交易中的签名,该签名由链码的背书节点签发。
假设链码指定了背书者集合 E = {Alice, Bob, Charlie, Dave, Eve, Frank, George}
。以下是一些示例策略:
- 同一个
tran-proposal
上有效签名的条件是:E 中所有成员都签名。 - 有效签名的条件是:E 中任何一个成员签名。
- 同一个
tran-proposal
上有效签名的条件是:(Alice OR Bob) AND (any two of: Charlie, Dave, Eve, Frank, George)
。 - 同一个
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}
和 George 之外的任何X
,或者{everyone together except Alice}
。等等。 - 上边所提到的权重可以是静态的(固定在链码元数据中)也可以是动态的(例如,根据在执行过程中链码的状态)。
- 有效签名的条件是:
tran-proposal1
满足Alice OR Bob
并且tran-proposal2
满足(any two of: Charlie, Dave, Eve, Frank, George)
,其中tran-proposal1
和``tran-proposal2`` 的区别在于背书节点和状态更新。
这些策略的用处取决于应用程序,关系到当背书节点故障、作恶或者出现其他状况时系统的弹性。
4 (v1之后)。 已验证账本和节点账本检查点(裁剪)¶
4.1. 已验证账本(Validated ledger,VLedger)¶
为了维护只包含了有效的和已提交的交易的账本的抽象,Peer 节点在状态和账本之外维护了一个 已验证账本。这是从账本中过滤掉无效交易之后的哈希链。
已验证账本区块(VLedger blocks, vBlocks)处理过程如下。因为 节点账本
可能包含无效交易(例如,交易的背书无效或者依赖版本无效),这些交易在加入到 vBlock 之前就别过滤掉了。每个节点自己完成这一步(例如,根据 节点账本
中相关的掩码)。已验证账本区块的定义是,不包含无效交易的区块。这些区块的大小是动态的并且可能为空。vBlock 的结构定义如下图:

图2。已验证账本区块(vBlock)和账本(PeerLedger)区块结构的区别。
vBlock 是通过每一个 Peer 节点连接在一起的哈希链。确切地说,每一个 vBlock 包含:
- 前一个 vBlock 的哈希。
- vBlock 序号。
- 上一个 vBlock 生成之后所有已提交交易的有序列表。
- 生成当前 vBlock 的相关区块(在
PeerLedger
中)哈希。
所有这些信息连接在一起并由 Peer 节点计算哈希,从而得到已验证账本中 vBlock 的哈希。
4.2. PeerLedger
检查点(Checkpointing)¶
包含无效交易的账本没有必要永久保存。但是 Peer 节点不能简单地丢弃 PeerLedger
区块,因此当构造完相应 vBlock 之后会对 PeerLedger
进行裁剪。也就是说,在这种情况下,如果一个新节点加入到了网络中,其他节点不能向新节点发送将会丢弃的区块,也不能向新节点证明它们的 vBlock 的有效性。
针对裁剪 PeerLedger
,本文档简介了 检查点 机制。这个机制建立了跨节点网络的 vBlock 验证并允许带检查点的 vBlock 替换丢弃的 PeerLedger
。这样就不用存储无效交易,减少了存储空间。同样也减小了新加入的节点重新构建状态的工作量(它们在重新执行 PeerLedger
中的交易来重构状态的过程中,不用验证每笔独立交易的有效性,但是可能需要需要重新执行已验证账本中的状态更新)。
4.2.1. 检查点协议¶
每一个 CHK 区块,Peer 节点都会定期执行检查点,CHK 是一个可配置参数。要初始化一个检查点,Peer 节点要想其他节点广播消息 <CHECKPOINT,blocknohash,blockno,stateHash,peerSig>
,其中 blockno
是当前区块号,blocknohash
是区块哈希,stateHash
是根据区块 blockno
的验证得到的最新状态(比如,Merkle 哈希)的哈希,peerSig
是 Peer 节点在 (CHECKPOINT,blocknohash,blockno,stateHash)
上的签名,表明这是已验证的账本。
节点收集 验证点
消息,直到它收集到了足够多正确的和 blockno
、 blocknohash
、 stateHash
相匹配的签名信息,然后它就会创建一个 有效的检查点 (参见 4.2.2 节)。
一个节点要对包含 blocknohash
的区块号 blockno
创建一个有效的检查点,它就要:
- 如果
blockno>latestValidCheckpoint.blockno
,节点要指定latestValidCheckpoint=(blocknohash,blockno)
, - 将构成一个有效检查点的相关节点的签名集合保存在
latestValidCheckpointProof
中, - 将和
stateHash
相关的状态保存在latestValidCheckpointedState
中, - (可选)修剪到区块号为
blockno
(包含) 的PeerLedger
。
4.2.2. 有效检查点¶
显然,检查点协提出了以下问题:什么时候节点可以裁剪它的 ``PeerLedger`` ?多少 ``CHECKPOINT`` 信息是 “足够多”? 。这些定义在 检查点有效性策略 中,其中包含(至少)两种方案,可以是组合使用:
- 本地(特定节点)检查点有效性策略(Local checkpoint validity policy,LCVP)。*在一个给定节点 *p 的本地策略中,可以指定节点 p 新人的节点集合和谁的
CHECKPOINT
消息可以满足构件有效检查点。例如,Alice 节点的 LCVP 定义为 Alice 需要接收到 Bob 的或者 Charlie 和 Dave 两个人的CHECKPOINT
消息。 - *全局检查点有效性策略(Global checkpoint validity policy,GCVP)。*检查点有效性策略可以指定为全局的。这和本地节点策略类似,只是这个是规定在系统(区块链)粒度,而不是节点粒度。GCVP可以像这样定义:
- 所有节点会信任经过 11 个不同节点的确认的检查点。
- 在一些特定的部署场景下,在同一个机制(例如,信任域)中,每一个排序节点都搭配了一个 Peer 节点,其中 f 个节点可能会发生(拜占庭)错误,所有节点会信任经过 f+1 个和排序节点搭配的不同节点确认的检查点。
交易流程¶
本文讲解在一个标准的资产交换中的交易机制。该场景包含两个客户端 A 和 B,他们分别代表萝卜的买方和卖方。他们在网络上都有一个 Peer 节点,他们通过该节点来发送交易和与账本交互。

假设
该流程中,假设已经设置了一个通道,并且该通道正常运行。应用程序的用户已经使用组织的 CA 注册和登记完成,并且拿到了用于在网络中用确认身份的加密材料。
链码(包含了萝卜商店初始状态的键值对)已经在 Peer 节点上安装并在通道上完成了实例化。链码中的逻辑定义了萝卜的交易和定价规则。链码也设置了一个背书策略,该策略是每一笔交易都必须被 peerA
和 peerB
都签名。

- 客户端 A 启动一笔交易
将会发生什么?客户端 A 发送一个采购萝卜的请求。该请求会到达 peerA
和 peerB
,他们分别代表客户端 A 和客户端 B。背书策略要求所有交易都要两个节点背书,因此请求要到经过 peerA
和 peerB
。
然后,要构建一个交易提案。应用程序使用所支持的 SDK(Node,Java,Python)中的 API 生成一个交易提案。提案是带有确定输入参数的调用链码方法的请求,该请求的作用可能是读取或者更新账本。
SDK 的作用是将交易提案打包成合适的格式(gRPC 使用的 protocol buffer)以及根据用户的密钥对交易提案生成签名。

- 背书节点验证签名并执行交易
背书节点验证(1)交易提案的格式完整,(2)且验证该交易提案之前没有被提交过(重放攻击保护),(3)验证签名是有效的(使用MSP),(4)验证发起者(在这个例子中是客户端A)是已经被授权在该通道上执行该操作(也就是说,每个背书节点确保发起者满足通道 Writers 策略)。背书节点将交易提案输入作为调用的链码函数的参数。然后针对当前状态数据库执行链码,生成交易结果,包括响应值、读集和写集(即表示要创建或更新的资产的键值对)。目前没有对账本进行更新。这些值,以及背书节点的签名,一起作为“提案响应”传递回SDK,SDK为应用程序解析出来这些数据再使用。
注解
MSP 是节点的组件,它允许 Peer 节点验证来自客户端的交易请求,并签署交易结果(即背书)。写入策略在通道创建时就会定义,用来确定哪些用户有权向该通道提交交易。有关成员关系的更多信息,请查看 成员 文档。

- 检查提案响应
应用程序验证背书节点的签名,并比较多个提案响应,以确定提案响应是否相同。如果链码只查询账本,应用程序将检查查询响应,并且通常不会将交易提交给排序服务。如果客户端应用程序打算向排序服务提交交易以更新账本,则应用程序在提交之前需确定是否已满足指定的背书策略(即 peerA 和 peerB 都要背书)。该体系结构是这样的,即使应用程序选择不检查响应或以其他方式转发未背书的交易,背书策略仍将由节点强制执行,并在提交验证阶段遵守该策略。

- 客户端将背书结果封装进交易
应用程序将交易提案和“交易消息”中的交易响应“广播”给排序服务。交易会包含读/写集,背书节点的签名和通道ID。排序服务不需要为了执行其操作而检查交易的整个内容,它只是从网络中的所有通道接收交易,将它们按时间按通道排序,并将每个通道的交易打包成区块。

- 验证和提交交易
交易区块被“发送”给通道上的所有 Peer 节点。对区块内的交易进行验证,以确保满足背书策略,并确保自交易执行生成读集以来,没有对读集变量的账本状态进行更改。块中的交易会被标记为有效或无效。

- 账本更新
每个 Peer 节点都将区块追加到通道的链上,对于每个有效的交易,写入集都提交到当前状态数据库。系统会发出一个事件,通知客户端应用程序本次交易(调用)已被不可更改地附加到链上,同时还会通知交易验证结果是有效还是无效。
注解
应用程序应该在提交交易后监听交易事件,例如使用 submitTransaction
API,它会自动监听交易事件。如果不监听交易事件,您将不知道您的交易是否已经被排序、验证并提交到账本。
查看 sequence diagram 来更好的理解交易流程。
Hyperledger Fabric SDK¶
Hyperledger Fabric 希望提供多种编程语言的 SDK。目前提供了 Node.js 和 Java 两种 SDK。我们希望在后续的版本中提供 Python、 REST 和 Go SDK。
服务发现¶
为什么我们需要服务发现?¶
为了在 Peer 节点上执行链码、向排序节点提交交易以及更新交易的状态,应用程序需要连接到 SDK 暴露的 API。
然而,SDK 为了让应用程序连接到网络中对应的节点需要很多信息。除了 CA 已经通道中的排序节点和 Peer 节点的 TLS 证书,包括他们的 IP 地址和端口号,还有它必须知道相应的背书策略以及哪些 Peer 节点安装了链码(这样应用程序才知道应该将链码提案发送到哪些 Peer 节点)。
在 v1.2 之前,这些信息是静态编码的。然而,这种方式不能动态适应网络的变化(比如增加了安装相应链码的 Peer 节点,或者某些节点临时离线)。静态配置同样也不能让应用程序适应背书策略的变化(比如当一个组织加入到通道的时候,可能会改变背书策略)。
另外,客户端应用程序也没办法知道哪些 Peer 节点更新了账本,哪些没有更新账本。应用程序可能会向没有同步账本的 Peer 节点提交提案,会导致交易在提交过程中验证失败,造成资源浪费。
发现服务 让 Peer 动态计算需要的信息并且发送给 SDK,从而改进了这个过程。
Fabric 中的服务发现是如何运作的¶
应用程序在启动时会知道一组应用程序开发者或者管理员信任的 Peer 节点,以便对发现查询提供可信的响应。客户端应用程序的备选节点最好在同一个组织中。注意,为了让 Peer 知道发现服务,必须定义 EXTERNAL_ENDPOINT
。要想知道如何定义,请参考 服务发现命令行界面 文档。
应用程序向发现服务发送一个配置查询,来获得所有的静态信息,如果没有获取到,就需要和网络中的其他节点进行通信。这些信息可以通过查询 Peer 节点的发现服务来更新。
发现服务运行在 Peer 节点上,而不是应用程序上,通过使用 gossip 通信层维护的网络元数据信息来查找在线的 Peer 节点。它也会从 Peer 节点的状态数据库中获取信息,比如相关的背书策略。
有了服务发现,应用程序就不需要在指定要哪些 Peer 来背书了。SDK 可以简单地向发现服务发送一个通道名和链码 ID 来查询需要哪些 Peer 节点。发现服务会计算出包含下边两个对象的描述:
- 布局(Layouts):一个 Peer 节点分组的列表和每组被选择的 Peer 节点的数量。
- Peer 节点和组的映射:从分布中的组到通道中的 Peer 节点。实际上,每个组中的节点都应该是同一个组织的,但是因为服务 API 是通用的并且没有区分组织,所以这里用“组”的概念。
下边是 AND(Org1, Org2)
策略的一个示例,其中每个组织有两个 Peer 节点。
Layouts: [
QuantitiesByGroup: {
“Org1”: 1,
“Org2”: 1,
}
],
EndorsersByGroups: {
“Org1”: [peer0.org1, peer1.org1],
“Org2”: [peer0.org2, peer1.org2]
}
换句话说,背书策略需要分别来自 Org1 和 Org2 中的一个 Peer 节点的签名。它还提供了这些组织中可用背书节点的名字(Org1 和 Org2 中的 peer0
和 peer1
)。
然后 SDK 从列表中随机选取一个布局。在上边的示例中,背书策略是 Org1 AND
Org2。如果使用 OR
的策略,SDK 就会随机选取 Org1 或 Org2,因为只要任意一个组织的 Peer 节点的签名就能够满足该策略。
在 SDK 选定布局之后,它会根据客户端定义的条件从布局中选出 Peer 节点(SDK 能做到这点,是因为它可以访问像账本高度这样的元数据)。例如,它可以根据布局中每个组的 Peer 节点序号选择账本高度较高的节点,或者排除离线的节点。如果选出的节点不止一个,SDK 会随机从中选取一个节点。
发现服务的作用¶
发现服务可以支持如下查询:
- 配置查询:返回通道中所有组织和排序节点的
MSPConfig
。 - 节点成员查询:返回加入通道的 Peer 节点。
- 背书查询:返回通道中指定链码的背书描述。
- 本地节点成员查询:返回响应查询的 Peer 节点的本地成员信息。默认情况下,需要管理员身份的客户端响应该查询。
特殊要求¶
当 Peer 启用了 TLS 的时候,客户端要连接 Peer 节点也必须提供 TLS 证书。如果 Peer 节点没有要求验证客户端证书(clientAuthRequired 设置为 false),这个 TLS 证书就可以是自签名的。
通道¶
Hyperledger Fabric 中的通道是两个或两个以上特定网络成员之间通信的专用“子网”,用于进行私有和机密的交易。通道由成员(组织)、每个成员的锚点节点、共享账本、链码应用程序和排序服务节点定义。网络上的每个交易都在一个通道上执行,在这个通道上,每一方都必须经过身份认证和授权才能在该通道上进行交易。加入通道的每个 Peer 节点都有自己的身份,由成员服务提供者(MSP)提供,MSP 向其通道 Peer 节点和服务认证每个 Peer 节点。
要创建一个新通道,客户端 SDK 调用配置系统链码并引用属性,如“锚点节点”和成员(组织)。这个请求为通道账本创建一个“创世区块”,它存储关于通道策略、成员和锚点节点的配置信息。当将新成员添加到现有通道时,可以与新成员共享这个创世区块,如果适用,也可以共享最近的重配置区块。
注解
有关配置交易的属性和原型结构的更多细节,请参见 通道配置(configtx) 部分。
为通道上的每个成员选择一个“领导节点”,确定哪个节点代表该成员与排序服务通信。如果没有标识出领导,可以使用算法来标识。共识服务对交易进行排序,并将它们以区块的形式交付给每个领导节点,然后由每个领导节点将该区块分发给它的成员节点,并使用 gossip
协议跨通道分发。
尽管任何一个锚点节点都可以属于多个通道,因此可以维护多个账本,但是没有账本数据可以从一个通道传递到另一个通道。这种按通道划分的账本是由配置链码、身份成员服务和 gossip 数据传播协议定义和实现的。数据的传播,包括交易、账本状态和通道成员的信息,仅限于通道上拥有可验证成员资格的节点。这种按通道隔离节点和账本数据的方法,允许需要私有和机密交易的网络成员与业务竞争对手和其他受限制的成员在同一个区块链网络上共存。
Capability Requirements 能力需求 ———————–
Because Fabric is a distributed system that will usually involve multiple organizations (sometimes in different countries or even continents), it is possible (and typical) that many different versions of Fabric code will exist in the network. Nevertheless, it’s vital that networks process transactions in the same way so that everyone has the same view of the current network state.
由于Fabric是一个分布式系统,通常会涉及多个组织(有时在不同的国家甚至大洲),所以网络中可能(而且是典型的)存在许多不同版本的Fabric代码。然而,重要的是,网络以相同的方式处理交易,这样每个人对当前网络状态都有相同的看法。
This means that every network – and every channel within that network – must
define a set of what we call “capabilities” to be able to participate in
processing transactions. For example, Fabric v1.1 introduces new MSP role types
of “Peer” and “Client”. However, if a v1.0 peer does not understand these new
role types, it will not be able to appropriately evaluate an endorsement policy
that references them. This means that before the new role types may be used, the
network must agree to enable the v1.1 channel
capability requirement,
ensuring that all peers come to the same decision.
这意味着每个网络,以及该网络中的每个通道,必须定义一组我们称为“功能”的东西,以便能够参与处理交易。例如,Fabric v1.1引入了新的MSP角色类型“Peer”和“Client”。但是,如果1.0版本的节点不理解这些新角色类型,那么它将无法恰当地验证引用它们的背书策略。这意味着在使用新角色类型之前,网络必须同意启用v1.1的“通道”功能需求,确保所有节点都做出相同的决定。
Only binaries which support the required capabilities will be able to participate in the channel, and newer binary versions will not enable new validation logic until the corresponding capability is enabled. In this way, capability requirements ensure that even with disparate builds and versions, it is not possible for the network to suffer a state fork.
只有支持所需功能的二进制文件才能参与通道,较新的二进制版本在启用相应功能之前不会启用新的验证逻辑。通过这种方式,功能需求确保即使使用不同的构建和版本,网络也不可能出现状态分支。
Defining Capability Requirements 定义功能需求 ================================
Capability requirements are defined per channel in the channel configuration (found in the channel’s most recent configuration block). The channel configuration contains three locations, each of which defines a capability of a different type.
功能需求是在通道配置中为每个通道定义的(可以在通道的最新配置区块中找到)。通道配置包含三个位置,每个位置定义了一个不同类型的功能。
Capability Type | Canonical Path | JSON Path |
---|---|---|
Channel | /Channel/Capabilities | .channel_group.values.Capabilities |
Orderer | /Channel/Orderer/Capabilities | .channel_group.groups.Orderer.values.Capabilities |
Application | /Channel/Application/Capabilities | .channel_group.groups.Application.values. Capabilities |
- Channel: these capabilities apply to both peer and orderers and are located in
the root
Channel
group. - Orderer: apply to orderers only and are located in the
Orderer
group. - Application: apply to peers only and are located in the
Application
group. - **通道:**这些能力适用于Peer和orders,并且位于根
通道
组中。 - **排序节点:**仅适用于orders,并且位于
Orderer
组中。 - **应用程序:**仅适用于peers,并且位于
Application
组中。
The capabilities are broken into these groups in order to align with the existing administrative structure. Updating orderer capabilities is something the ordering orgs would manage independent of the application orgs. Similarly, updating application capabilities is something only the application admins would manage. By splitting the capabilities between “Orderer” and “Application”, a hypothetical network could run a v1.6 ordering service while supporting a v1.3 peer application network.
为了与现有的管理结构保持一致,这些能力被分解到这些组中。更新orderer能力是ordering组织自己的事,独立与application orgs。 类似的,更新application能力只是application管理员的事情, 与ordering无关。 通过将“Orderer”和“Application”之间的能力分离,假设一个网络可以运行在v1.6的ordering service, 同时又支持运行在v1.3的peer网络。
However, some capabilities cross both the ‘Application’ and ‘Orderer’ groups. As we saw earlier, adding a new MSP role type is something both the orderer and application admins agree to and need to recognize. The orderer must understand the meaning of MSP roles in order to allow the transactions to pass through ordering, while the peers must understand the roles in order to validate the transaction. These kinds of capabilities – which span both the application and orderer components – are defined in the top level “Channel” group.
然而,有些能力跨越了“Application”和“Orderer”组。正如我们前面看到的,添加新的MSP角色类型是orderer管理员和application管理员都同意并且需要认识到的。 orderer必须理解MSP角色的含义,以便允许事务通过排序,而peers必须理解角色,以便验证事务。 这种能力 – 它跨越了applitaion和orderer组件 – 在顶层“Channel”组中定义。
注解
It is possible that the channel capabilities are defined to be at version v1.3 while the orderer and application capabilities are defined to be at version 1.1 and v1.4, respectively. Enabling a capability at the “Channel” group level does not imply that this same capability is available at the more specific “Orderer” and “Application” group levels.
注解
channel能力可能被定义为v1.3版本,而orderer和application分别被定义为1.1版本和v1.4版本。 在“Channel”组级别启用能力并不意味着在特定的“Orderer”和“Application”组级别可以使用相同的能力。
Setting Capabilities 设置能力 ====================
Capabilities are set as part of the channel configuration (either as part of the initial configuration – which we’ll talk about in a moment – or as part of a reconfiguration).
能力被设置为通道配置的一部分(或者作为初始配置的一部分(我们将稍后讨论),或者作为重新配置的一部分)。
注解
We have a two documents that talk through different aspects of channel reconfigurations. First, we have a tutorial that will take you through the process of 向通道添加组织. And we also have a document that talks through 更新通道配置 which gives an overview of the different kinds of updates that are possible as well as a fuller look at the signature process.
注解
我们有两个文档讨论了通道重新配置的不同方面。首先,我们有一个教程,为您演示 向通道添加组织 。我们也有另一个文档,讨论了如何 更新通道配置, 它给出了各种更新的概述以及对签名过程的更全面的了解 Updating a Channel Configuration 。
Because new channels copy the configuration of the Orderer System Channel by default, new channels will automatically be configured to work with the orderer and channel capabilities of the Orderer System Channel and the application capabilities specified by the channel creation transaction. Channels that already exist, however, must be reconfigured.
因为在默认情况下,新通道会复制Orderer系统通道的配置,因此在新通道创建时会使用和Orderer系统通道 一样的Orderer和channel能力,以及application能力自动配置新通道。 然而,已经存在的通道必须重新配置。
The schema for the Capabilities value is defined in the protobuf as:
Capabilities在protobuf中定于的结构如下:
message Capabilities {
map<string, Capability> capabilities = 1;
}
message Capability { }
As an example, rendered in JSON:
用JSON格式举例:
{
"capabilities": {
"V1_1": {}
}
}
Capabilities in an Initial Configuration 初始化配置中的Capabilities ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In the configtx.yaml
file distributed in the config
directory of the release
artifacts, there is a Capabilities
section which enumerates the possible capabilities
for each capability type (Channel, Orderer, and Application).
Fabric源代码 config
路径下的 configtx.yaml
文件中, 在 Capabilities
部分列举了每种能力类型 (Channel, Orderer, and Application)。
The simplest way to enable capabilities is to pick a v1.1 sample profile and customize it for your network. For example:
启用能力的最简单方法是选择一个v1.1示例概要文件并为您的网络定制它。例如:
SampleSingleMSPSoloV1_1:
Capabilities:
<<: *GlobalCapabilities
Orderer:
<<: *OrdererDefaults
Organizations:
- *SampleOrg
Capabilities:
<<: *OrdererCapabilities
Consortiums:
SampleConsortium:
Organizations:
- *SampleOrg
Note that there is a Capabilities
section defined at the root level (for the channel
capabilities), and at the Orderer level (for orderer capabilities). The sample above uses
a YAML reference to include the capabilities as defined at the bottom of the YAML.
注意,在根级别(用于channel capabilities)和在Orderer级别(用于Orderer能力)定义了一个 Capabilities 部分。 上面的示例使用YAML引用的方式将定义在文件底部的capabilities部分包含进来。
When defining the orderer system channel there is no Application section, as those
capabilities are defined during the creation of an application channel. To define a new
channel’s application capabilities at channel creation time, the application admins should
model their channel creation transaction after the SampleSingleMSPChannelV1_1
profile.
在定义orderer系统通道时,不存在Application部分,因为这些能力是在创建application通道时定义的。 要在通道创建时定义新通道的application能力,application管理员应该在 SampleSingleMSPChannelV1_1
中对其通道创建事务建模。
SampleSingleMSPChannelV1_1:
Consortium: SampleConsortium
Application:
Organizations:
- *SampleOrg
Capabilities:
<<: *ApplicationCapabilities
Here, the Application section has a new element Capabilities
which references the
ApplicationCapabilities
section defined at the end of the YAML.
Applicatoin部分的 Capabilities
元素引用了定义在YAML文件底部的 ApplicationCapabilities
部分。
注解
The capabilities for the Channel and Orderer sections are inherited from the definition in the ordering system channel and are automatically included by the orderer during the process of channel creation.
注解
应用通道中的Channel和Orderer capabilities继承自ordering系统通道中的定义,在创建通道的时候被自动包含进来。
使用 CouchDB 作为状态数据库¶
状态数据库选项¶
状态数据库选项包括 LevelDB 和 CouchDB 。LevelDB 是默认嵌入在 Peer 程序中的键-值数据库。 CouchDB 是一个可选的额外扩展的状态数据库。就像 LevelDB 键-值存储一样,CouchDB 可以存储任何在链码中建模的二进制数据(CouchDB 附件功能在内部用于非 JSON 二进制数据)。但是作为一个 JSON 文档存储数据库,当链码值(比如,资产)以 JSON 数据建模时,CouchDB 额外支持链码数据的富查询。
LevelDB 和 CouchDB 都支持链码操作,比如获取或者设置一个键(资产),以及基于键查询。键可以按范围查询,以复合键建模就可以支持针对多个参数的等效查询。比如一个复合键 owner,asset_id
就可以查询一个实体的所有资产。这些基于键的查询可以用来在账本上做只读查询,同样可以用于在交易中更新账本
如果你以 JSON 建模资产并使用 CouchDB,你就可以在链码中使用 CouchDB JSON 来使用富查询。这种方式对于理解账本上存了什么内容是非常好的。这些类型查询的提案响应对于客户端应用程序是很有用的,但是对于提交到排序服务的交易就不是太有价值了。事实上,无法保证在链码执行和提交的时候富查询的结果不被改变,因此富查询不适合用于账本更新的交易中,除非你的应用程序可以保证查询的结果不会被改变,并且在子交易中可以很好的处理潜在的变化。例如,如果你想以富查询的方式查询 Alice 所有的资产,然后转移给 Bob,在链码执行和提交期间可能会有其他交易向 Alice 发送新的资产,这时你就是是去这些“幻像”。
CouchDB 以独立的进程和 peer 一起运行,因此需要一些额外的配置来设置、管理和操作。也许你想以 LevelDB 开始,但是当你需要富查询的时候再更换到 CouchDB。以 JSON 的方式建模脸链码资产数据是一个好的方式,这样你就可以在需要富查询的时候进行切换了。
注解
CouchDB JSON 文档只能包含合法的 UTF-8 字符串并且不能以下划线开头(“_”)。无论你使用 CouchDB 还是 LevelDB 都不要在键中使用 U+0000 (空字节)。
CouchDB JSON 文档中不能使用一下值作为顶字段的名字。这些名字为内部保留字段。
- 任何以下划线开头的字段,“_”
- ~version
从链码中使用 CouchDB¶
链码查询¶
链码 API 中大部分方法在 LevelDB 或者 CouchDB 状态数据库中都可用,例如 GetState
、PutState
、GetStateByRange
、GetStateByPartialCompositeKey
。另外当你使用 CouchDB 作为状态数据库并且在链码中以 JSON 建模资产的时候,你可以使用 GetQueryResult
通过向 CouchDB 发送查询字符串的方式使用富查询。查询字符串请参考 CouchDB JSON 查询语法 。
marbles02 示例 演示了如何从链码中使用 CouchDB 查询。它包含了一个 queryMarblesByOwner()
方法,通过向链码传递所有者 id 来演示如何通过参数查询。它还使用 JSON 查询语法在状态数据中查询符合 “docType” 的弹珠的所有者 id:
{"selector":{"docType":"marble","owner":<OWNER_ID>}}
CouchDB 分页¶
Fabric 支持对富查询和范围查询结果的分页。API 支持范围查询和富查询使用页大小和书签进行分页。要支持高效的分页,必须使用 Fabric 的分页 API。特别地,CouchDB 不支持 limit
关键字,分页是由 Fabric 来管理并隐式地按照 pageSize 的设置进行分页。
如果是通过查询 API (GetStateByRangeWithPagination()
、GetStateByPartialCompositeKeyWithPagination()
、和 GetQueryResultWithPagination()
)来指定 pageSize 的,返回给链码的结果(以 pageSize 为范围)会带有一个书签。该书签会返回给调用链码的客户端,客户端可以根据这个书签来查询结果的下一“页”。
分页 API 只能用于只读交易中,查询结果旨在支持客户端分页的需求。对于需要读和写的交易,请使用不带分页的链码查询 API。在链码中,您通过迭代的方式来获取你想要的深度。
无论是否使用了分页 API,所有链码查询都受限于 core.yaml
中的 totalQueryLimit
(默认 100000)。这是链码将要迭代并返回给客户端最多的结果数量,以防意外或者恶意地长时间查询。
注解
无论链码中是否使用了分页,节点都会根据 core.yaml
中的 ``internalQueryLimit``(默认 1000) 来查询 CouchDB。 这样就保证了在执行链码的时候有合理大小的结果在节点和 CouchDB 之间,以及链码和客户端之间传播。
在 使用 CouchDB 教程中有一个使用分页的示例。
CouchDB 索引¶
CouchDB 中的索引用来提升 JSON 查询的效率以及按顺序的 JSON 查询。索引可以在 /META-INF/statedb/couchdb/indexes
文件夹中和链码打包在一起。每一个索引文件必须定义在一个扩展名为 *.json
的文本文件中,文件内容符合 CouchDB 索引 JSON 语法 。例如,要想支持上边提到的弹珠查询,提供了一个 docType
和 owner
字段的简单索引文件:
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
索引文件可以在 这里 找到。
在链码的 META-INF/statedb/couchdb/indexes
文件夹下的所有索引都会打包到链码中以便部署。当链码在 Peer 节点上安装和在通道上实例化后,索引会自动部署在 Peer 节点的通道和链码指定的状态数据库中(如果它被配置为使用 CoucbDB)。如果你先在 Peer 节点上安装了链码,之后才会在通道上实例化链码,索引会在链码 实例化 的时候部署。如果链码已经在通道上实例化了,你之后又在 Peer 节点上安装了链码,索引会在链码 安装 的时候部署。
部署之后,调用链码查询的时候会自动使用索引。CouchDB 会根据查询的字段选择使用哪个索引。或者,在查询选择器中通过 use_index
关键字指定要使用的索引。
安装的不同版本的链码可能会有相同版本的索引。要更改索引,需要使用相同的索引名称但是不同的索引定义。在安装或者实例化完成的时候,索引就会重新被部署到 Peer 节点的状态数据库了。
如果你已经有了大量的数据,然后才安装或者初始化链码,在安装或初始化的过程中索引的创建可能会花费一些时间。在索引创建的过程中请不要调用来嘛查询状态数据库。在交易的过程中,区块提交到账本后索引会自动更新。
CouchDB 配置¶
通过在 stateDatabase
状态选项中将 goleveldb 切换为 CouchDB 可以启用 CouchDB 状态数据库。另外配置 couchDBAddress
来指向 Peer 节点所使用的 CouchDB。如果 CouchDB 设置了用户名和密码,也需要在配置中指定。其他的配置选项在 couchDBConfig
部分也都有相关说明。重启 Peer 节点就可以使 core.yaml 文件立马生效。
你也可以使用环境变量来覆盖 core.yaml 中的值,例如 CORE_LEDGER_STATE_STATEDATABASE
和 CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS
。
下边是 core.yaml 中的 stateDatabase
部分:
state:
# stateDatabase - options are "goleveldb", "CouchDB"
# goleveldb - default state database stored in goleveldb.
# CouchDB - store state database in CouchDB
stateDatabase: goleveldb
# Limit on the number of records to return per query
totalQueryLimit: 10000
couchDBConfig:
# It is recommended to run CouchDB on the same server as the peer, and
# not map the CouchDB container port to a server port in docker-compose.
# Otherwise proper security must be provided on the connection between
# CouchDB client (on the peer) and server.
couchDBAddress: couchdb:5984
# This username must have read and write authority on CouchDB
username:
# The password is recommended to pass as an environment variable
# during start up (e.g. LEDGER_COUCHDBCONFIG_PASSWORD).
# If it is stored here, the file must be access control protected
# to prevent unintended users from discovering the password.
password:
# Number of retries for CouchDB errors
maxRetries: 3
# Number of retries for CouchDB errors during peer startup
maxRetriesOnStartup: 10
# CouchDB request timeout (unit: duration, e.g. 20s)
requestTimeout: 35s
# Limit on the number of records per each CouchDB query
# Note that chaincode queries are only bound by totalQueryLimit.
# Internally the chaincode may execute multiple CouchDB queries,
# each of size internalQueryLimit.
internalQueryLimit: 1000
# Limit on the number of records per CouchDB bulk update batch
maxBatchUpdateSize: 1000
# Warm indexes after every N blocks.
# This option warms any indexes that have been
# deployed to CouchDB after every N blocks.
# A value of 1 will warm indexes after every block commit,
# to ensure fast selector queries.
# Increasing the value may improve write efficiency of peer and CouchDB,
# but may degrade query response time.
warmIndexesAfterNBlocks: 1
Hyperledger Fabric 提供的 CouchDB docker 镜像可以通过 Docker Compose 脚本来定义 COUCHDB_USER
和 COUCHDB_PASSWORD
环境变量,从而设置 CouchDB 管理员的用户名和密码。
如果没有使用 Fabric 提供的 docker 镜像安装 CouchDB,必须编辑 local.ini 文件 来设置管理员的用户名和密码。
Docker Compose 脚本只能在创建容器的时候设置用户名和密码。在容器创建之后,必须使用 local.ini 文件来修改用户名和密码。
注解
每次 Peer 节点启动的时候都会读取 CouchDB 节点的选项。
查询练习¶
避免对将导致扫描整个 CouchDB 数据库的;链码查询。全长数据库扫描将导致较长的响应时间,并将降低您的网络性能。您可以采取以下一些步骤来避免长时间查询:
使用 JSON 查询:
- 确保在链码包中创建了索引。
- 不要使用
$or
、$in
和$regex
之类会扫描整个数据库的操作。
对于范围查询、复合键查询和 JSON 查询:
- 使用分页查询(就像 v1.3 中所说),不要使用一个大的查询结果。
如果在您的应用中想创建一个仪表盘(dashboard)或者聚合数据,您可以将区块链数据复制到链下的数据库中,通过链下数据库来查询或分析区块链数据,以此来优化数据存储,并防止网络性能的降低或交易的终端。要实现这个功能,可以通过区块或链码事件将交易数据写入链下数据库或者分析引擎。对于每一个接收到的区块,区块监听应用将遍历区块中的每一个交易并根据每一个有效交易的
读写集
中的键值对构建一个数据存储。文档 基于通道的节点事件服务 提供了可重放事件,以确保下游数据存储的完整性。
基于通道的节点事件服务¶
概览¶
在 Fabric 之前的版本中,节点事件服务被称为事件仓库(event hub)。当 Peer 节点的账本中新增一个区块的时候该服务就会发送一个事件,无论该区块属于哪个通道,只有组织成员中运行事件节点才可以访问该事件(例如,连接到事件的节点)。
从v1.1开始,有两个新的服务提供事件。这些服务使用完全不同的设计来为每个通道提供事件。这意味着事件的注册发生在通道的级别,而不是 Peer 节点,允许对 Peer 节点数据的访问进行细粒度的控制。通过 Peer 节点的组织之外的标识(由通道配置定义)来接受接收事件的请求。这还提供了更高的可靠性和接收可能错过的事件的方法(无论是由于连接问题还是因为 Peer 节点正在加入网络)。
启动服务¶
Deliver
该服务发送已经提交到账本的所有区块。可以在区块的 ChaincodeActionPayload
中查看链码设置的所有事件。
DeliverFiltered
该服务发送“经筛选”的区块,已经提交到账本的区块信息的最小集合。它用在 Peer 节点希望外部客户端主要用来接收交易的信息和状态的网络中。可以在区块的 FilteredChaincodeAction
中查看链码设置的所有事件。
注解
链码事件的负载不会包含在筛选出的区块中。
如何注册事件¶
通过向 Peer 节点发送一个带有搜索种子信息的信封(envelope)来完成对任意服务的注册,该信息中包含了所需的开始和结束的位置和搜索行为(直到准备好前阻塞或没有准备好就失败)。SeekOldest
和 SeekNewest
变量用来表明是账本中最新的(最后的)或者最旧的(第一个)区块。要让服务无限期地发送事件,SeekInfo
消息应该包含 MAXINT64
的停止位置。
注解
如果节点启用了 TLS,就必须在信封的通道头部中设置 TLS 证书。
默认情况下,两个服务都使用通道读者的策略(Channel Readers policy)来决定哪些客户端可以接收事件。
发送响应信息概览¶
事件服务发回 DeliverResponse
信息。
没条消息包含如下内容:
- 状态(status) – HTTP 状态码。两个服务在发生错误是都会返回相应的状态码;否则会返回
200 - SUCCESS
表示服务发送完成了SeekInfo
信息中请求的内容。- 区块(block) – 只有
Deliver
服务返回。- 筛选出的区块(filtered block) – 只有
DeliverFiltered
服务返回。
筛选出的区块包含:
- 通道 ID。
- 序号 (例如区块号)。
- 筛选出的交易数组。
- 交易 ID.
- 类型 (例如
ENDORSER_TRANSACTION
,CONFIG
)。- 交易验证码。
- 筛选出的交易活动。
- 筛选出的链码活动数组。
- 交易的链码事件(有效负载为零)。
私有数据¶
注解
本主题假设你已经理解了在 私有数据文档 中所描述概念。
私有数据集合定义¶
一个集合定义包含了一个或者多个集合,每个集合具有一个策略列出了在集合中的所有组织,还包括用来控制在背书阶段私有数据的传播所使用的属性,另外还有一个可选项来决定数据是否会被删除。
这个集合的定义会在链码实例化(或者升级)的时候被部署到通道中。如果使用的是 peer CLI 来初始化链码的话,会使用 --collections-config
标识将这个集合定义文件发送给链码的实例化过程。如果使用的是一个客户端 SDK,查看 SDK 文档 来了解更多关于如何提供集合定义的内容。
集合定义由下边的属性组成:
name
: 集合的名字.policy
: 私有数据集合分发策略,它通过Signature
策略语法定义了允许哪些组织的 Peer 节点持久化集合数据。为了支持读/写交易,私有数据的分发策略必须要定义一个比链码背书策略更大范围的一个组织的集合,因为 Peer 节点必须要拥有这些私有数据才能来对这些交易提案进行背书。比如,在一个具有10个组织的通道中,其中5个组织可能会被包含在一个私有数据集合的分发策略中,但是背书策略可能会调用其中任意的3个组织来进行背书。requiredPeerCount
: 在节点为背书签名并将提案响应返回给客户端前,每个背书节点必须将私有数据分发到的节点(在被授权的组织当中)的最小数量。将传播作为背书的一个条件可以确保即使背书背书不可用,私有数据在网络中也还是可用的。requiredPeerCount
设为0
代表分发并不是 必须 的,但是如果maxPeerCount
比0大的话,就需要分发。通常不建议requiredPeerCount
设为0
通常,因为那会造成在背书节点不可用的时候,网络中的私有数据可能会丢失。通常在背书的时候你会希望分发私有数据到多个节点以保证网络中私有数据的冗余存储。maxPeerCount
: 为了数据的冗余存储,每个背书节点将会尝试将私有数据分发到的其他节点(在被授权的组织中) 的最大数量。如果在背书和提交之间一个背书节点不可用了,其他节点就可以在背书的时候从已经收到私有数据的节点拉取私有数据。如果这个值被设置为0
,私有数据在背书的时候就不会被分发,这会在提交的时候强制节点从授权的背书节点上拉取私有数据。blockToLive
: 代表了数据应该以区块的形式在私有数据库中存在多久。数据会在私有数据库上存在指定数量个区块,然后它会被删除,然后链码就无法查询到该数据,其他节点也无法请求到该数据。如果要无限期的保持私有数据,也就是从来不删除私有数据的话,将blockToLive
设置为0
。memberOnlyRead
:true
值表示节点自动会强制只有属于这些集合的组织的客户端才可以读取私有数据。如果一个非成员组织的客户端试图执行一个链码方法来读取私有数据的话,会结束链码的调用并产生错误。如果你想在单独的链码方法中进行更细粒度的访问控制的话,可以使用false
值。
下边是一个集合定义的 JSON 文件示例,一个包含了两个集合定义的数组:
[
{
"name": "collectionMarbles",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":1000000,
"memberOnlyRead": true
},
{
"name": "collectionMarblePrivateDetails",
"policy": "OR('Org1MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive":3,
"memberOnlyRead": true
}
]
这个例子使用了来自于 BYFN 示例网络中的组织 Org1
和 Org2
。在 collectionMarbles
定义中的策略对两个组织都授予了私有数据的权限。这个是在链码数据需要与排序服务节点保持私有化的时候的一种典型配置。然而,在 collectionMarblePrivateDetails
定义中的策略却将访问控制在了通道中 (在这里指的是 Org1
) 中组织的子集。在真实场景中,通道中会有很多组织,在每个集合中的两个或者多个组织间会彼此共享数据。
私有数据分发¶
由于私有数据不会被包含在提交到排序服务的交易中,因此也就不会被包含在区块中,背书节点扮演着将私有数据分发给其他授权组织的节点的重要角色。这确保了私有数据在背书节点完成背书之后变成不可用的时候的可用性。为了辅助分发,在集合定义中的 maxPeerCount
和 requiredPeerCount
属性控制了在背书的时候分发的数量。
如果背书节点不能够成功地将私有数据分发到至少 requiredPeerCount
的要求,它将会返回一个错误给客户端。背书节点会尝试将私有数据分发到不同组织的节点,来确保每个被授权的组织具有私有数据的一个副本。因为交易在链码执行期间还没有被提交,背书节点和接收节点除了在它们的区块链之外,还在一个本地的 临时存储(transient store)
中存储了一个私有数据副本,直到交易被提交。
当一个被授权的节点在提交的时候,如果他们的临时存储中没有私有数据的副本(或者是因为他们不是一个背书节点,或者是因为他们在背书的时候没有接收到私有数据),他们会尝试从其他的被授权的节点那里拉取私有数据,尝试会*持续一个可配置的时间长度* ,在时间可以通过节点配置文件 core.yaml
中的属性 peer.gossip.pvtData.pullRetryThreshold
进行配置。
注解
只有当提出请求的节点是私有数据分发策略定义的集合中的一员的时候,被询问的节点才会返回私有数据。
当使用 pullRetryThreshold
时候需要考虑的问题:
- 如果提出请求的节点能够在
pullRetryThreshold
时间内拿到私有数据的话,它将会把交易提交到自己的账本(包括私有数据的哈希值),并且将私有数据存储在与其他的通道状态数据进行了逻辑隔离的状态数据库中。 - 如果提出请求的节点没能在
pullRetryThreshold
时间内拿到私有数据的话,它将会把交易提交到自己的账本(包括私有数据的哈希值),但是不会存储私有数据。 - 如果某个节点有资格拥有私有数据,却没有得到的话,这个节点就无法为将来会引用这个丢失的私有数据的交易进行背书,背书时会发现无法查询到键 (基于在状态数据库中主键的哈希值),并且链码将会收到一个错误。
因此,将 requiredPeerCount
和 maxPeerCount
设置成足够大的值来确保在你的通道中的私有数据的可用性是非常重要的。比如,如果在交易提交之前,每个背书节点都不可用了,requiredPeerCount
和 maxPeerCount
属性将会确保私有数据在其他的节点上是可用的。
注解
为了让集合能够工作,正确配置跨组织的 gossip 非常重要的。请阅读 Gossip 数据传播协议,尤其注意“锚节点”这部分。
从链码中引用集合¶
我们可以用 shim APIs 设置和取回私有数据。
相同的链码数据操作也可以应用到通道状态数据和私有数据上,但是对于私有数据,要在链码 API 中指定和数据相关的集合的名字,比如 PutPrivateData(collection,key,value)
和 GetPrivateData(collection,key)
。
一个链码可以引用多个集合。
如何在一个链码提案中传递私有数据¶
因为链码提案被存储在区块链上,不要把私有数据包含在链码提案中也是非常重要的。在链码提案中有一个特殊的字段 transient
,可以用它把私有数据来从客户端(或者链码将用来生成私有数据的数据)传递给节点上的链码调用。链码可以通过调用 GetTransient() API 来获取 transient
字段。这个 transient
字段会从通道交易中被排除。
私有数据的访问控制¶
直到1.3版本,基于集合成员的私有数据的访问控制仅限制在 Peer 节点。基于链码提案的提交者所在组织的访问控制需要编码在链码逻辑中。从v1.4开始,集合配置中的选项 memberOnlyRead
能够自动地强制使用基于链码提案提交者组织的访问控制。关于集合配置定义以及如何设置的更多信息,请查看 `Private data collection definition`_ 章节。
注解
如果你想要更细粒度的访问控制,你可以将 memberOnlyRead
设置为 false。然后你可以在链码中应用你自己的访问控制逻辑,比如通过调用链码 API GetCreator() 或者使用客户端身份 链码库 。
查询私有数据¶
私有集合数据能够像常见的通道数据那样使用 shim API 来进行查询:
GetPrivateDataByRange(collection, startKey, endKey string)
GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string)
对于 CouchDB 状态数据库,可以使用 shim API 查询 JSON 内容:
GetPrivateDataQueryResult(collection, query string)
限制:
- 客户端调用执行范围查询或者富查询链码的时候应该知道,根据上边关于私有数据分发部分的解释,如果他们查询的节点有丢失的私有数据的话,他们可能会接收到结果集的一个子集。客户端可以查询多个节点并且比较返回的结果,以确定一个节点是否丢失了结果集中的部分数据。
- 不支持在单个交易中既执行范围查询或者富查询并且更新数据,因为查询结果无法在以下类型的节点上进行验证:不能访问私有数据的节点或者对于那些他们可以访问相关的私有数据但是私有数据是丢失的。如果一个链码的调用既查询又更新私有数据的话,这个提案请求将会返回一个错误。如果你的应用程序能够容忍在链码执行和验证/提交阶段结果集的变动,那么你可以调用一个链码方法来执行这个查询,然后再调用第二个链码方法来执行变更。注意,调用 GetPrivateData() 来获取单独的键值可以跟 PutPrivateData() 调用放在同一个交易中,因为所有的节点都能够基于键版本的哈希来验证键的读取。
在集合中使用索引¶
使用 CouchDB 作为状态数据库 章节讲解了可以在安装阶段,通过将索引打包在一个 META-INF/statedb/couchdb/indexes
的路径下的方式,将索引应用到通道的状态数据库。类似的,也可以通过将索引打包在一个 META-INF/statedb/couchdb/collections/<collection_name>/indexes
路径下的方式将索引应用到私有数据集合中。一个索引的实例可以查看 这里。
使用私有数据时的思考¶
私有数据的删除¶
Peer 可以周期性地删除私有数据。更多细节请查看上边集合定义属性中的 blockToLive
。
另外,重申一下,在提交之前,私有数据存储在 Peer 节点的本地临时数据存储中。这些数据在交易提交之后会自动被删除。但是如果交易没有被提交,私有数据就会一直保存在临时数据存储中。Peer 节点会根据配置文件 core.yaml
中的 peer.gossip.pvtData.transientstoreMaxBlockRetention
的配置周期性的删除临时存储中的数据。
升级一个集合定义¶
如果要升级一个集合定义或者增加一个新的定义,你可以将链码更新到新版本并将集合配置新的集合配置传递给链码更新交易。例如在 CLI 中使用 --collections-config
标示。如果在链码更新期间指定了集合配置,所有已存在的集合的定义也必须包含其中。
升级链码的时候,你可以新增和更新私有数据集合,例如向已存在的集合中添加新成员或者改变一个集合定一个的属性。注意,你不能更新集合名称和 blockToLive 属性,因为无论节点区块高度如何,都需要一个一致的 blockToLive 。
在一个 Peer 节点提交包含链码更新交易的区块时,集合的更新才会生效。注意,集合是不能够被删除的,因为在通道的区块链上可能有之前的私有数据的哈希,而这些哈希值是不能被删除的。
私有数据对账¶
从 v1.4 开始,加入到已存在的集合中的 Peer 节点在私有数据加入到集合之前,可以自动获取提交到集合的私有数据。
私有数据“对账”也应用在 Peer 节点上,用于确认该接收却未接收到的私有数据,比如由于网络原因没有收到的。以此来追踪在区块提交期间“丢失”的私有数据。
私有数据对账根据 core.yaml 文件中的属性 peer.gossip.pvtData.reconciliationEnabled
和 peer.gossip.pvtData.reconcileSleepInterval
周期性的发生。Peer 节点会从集合成员节点中定期获取私有数据。
注意私有数据对账特性只适用于 v1.4 以上的 Fabric 节点。
读写集语义¶
本文档讨论了目前实现的读写集语义的细节。
交易模拟和读写集¶
背书节点
在模拟交易期间,会为交易准备一个读写集。读集
包含了模拟期间交易读取的键和键的版本的列表。写集
包含了交易写入的键(尽管可以与读取集中的键重叠)的新值。如果交易是删除一个键,该键就会被增加一个删除标识(在新值的位置)。
如果一个交易多次为同一个键写入数据,只有最后一个写入的数据会记录下来。通过,如果交易读取一个键的值,会返回这个键的已提交状态的值,即使在读之前在同一个交易中更新了键值。换句话说,不支持“读你所写”的语义。
就像前面所说的,键的版本只记录在读集中;写集只包含键和交易设置的键的最新值。
版本的实现有很多种。版本设计的最小需求是,给定一个键,不能有重复的版本号。例如单调递增的数字。在现在的实现中,我们使用交易的高度来作为版本号,即提交的交易的高度作为交易所更改的键的版本号。这样区块中交易的高度通过一个元组来表示(txNumber 是区块中交易的高度)。这种方式比递增的数字有更多好处,主要有,它可以让其他组件比如状态数据库、交易模拟和验证有更多的设计选择。
下边是为模拟一个交易所准备的读写集示例。为了简化说明,我们使用了一个递增的数字来表示版本。
<TxReadWriteSet>
<NsReadWriteSet name="chaincode1">
<read-set>
<read key="K1", version="1">
<read key="K2", version="1">
</read-set>
<write-set>
<write key="K1", value="V1"
<write key="K3", value="V2"
<write key="K4", isDelete="true"
</write-set>
</NsReadWriteSet>
<TxReadWriteSet>
另外,如果交易在模拟中执行的是一个范围查询,范围查询和它的结果都会被记录在读写集的 查询信息(query-info)
中。
交易验证和使用读写集更新世界状态¶
提交者
使用读写集中的读集来验证交易,使用写集来更新受影响的键的版本和值。
在验证阶段,如果读集中键的版本和世界状态中键的版本一致就认为该交易是 合法的
,这里我们假设所有 有效
的交易(包括同一个区块中之前的交易)都会被提交(提交状态)。当读写集中包含一个或多个查询信息(query-info)时,需要执行额外的验证。
这种额外的验证需要确保在根据查询信息获得的结果的超集(多个范围的合并)中没有插入、删除或者更新键。换句话说,如果我们在模拟执行交易期间重新执行任何一个范围,我们应该得到相同的结果。这个检查保证了如果交易在提交期间出了虚项,该交易就会被标记为无效的。这种检查只存在于范围查询中(例如链码中的 GetStateByRange
方法)其他查询中没有实现(例如链码中的 GetQueryResult
方法)。其他查询仍会存在出现虚项的风险,我们应该只在不向排序服务提交的只读交易中使用查询,除非应用程序能保证模拟的结果和验证/提交时的结果一致。
如果交易通过了有效性验证,提交节点就会根据写集更新世界状态。在更新阶段,会根据写集更新世界状态中对应的键的值。然后,世界状态中键的版本会更新到最新的版本。
模拟和验证示例¶
本章节通过示例场景帮助你理解读写集语义。在本例中,k
表示键,在世界状态中表示一个元组 (k,ver,val)
, ver
是键 k
的版本, val
是值。
现在假设有五个交易 T1、T2、T3、T4, 和 T5
,所有的交易模拟都基于同一个世界状态的快照。下边的步骤展示了世界状态和模拟这些交易时的读写活动。
World state: (k1,1,v1), (k2,1,v2), (k3,1,v3), (k4,1,v4), (k5,1,v5)
T1 -> Write(k1, v1'), Write(k2, v2')
T2 -> Read(k1), Write(k3, v3')
T3 -> Write(k2, v2'')
T4 -> Write(k2, v2'''), read(k2)
T5 -> Write(k6, v6'), read(k5)
Now, assume that these transactions are ordered in the sequence of T1,..,T5 (could be contained in a single block or different blocks)
现在,假设这些交易的顺序是从 T1 到 T5(他们可以在同一个区块,也可以不同区块)
T1
通过了验证,因为它没有执行任何读操作。然后世界状态中的键k1
和k2
被更新为(k1,2,v1'), (k2,2,v2')
T2
没有通过验证,因为它读了键k1
,在交易T1
中改变了k1
T3
通过了验证,因为它没有执行任何读操作。然后世界状态中的键k2
被更新为(k2,3,v2'')
T4
没有通过验证,因为它读了键k2
,在交易T1
中改变了k2
T5
通过了验证,因为它读了键k5
,但是k2
没有被其他任何交易改变
Note: 不支持有多个读写集的交易。
Gossip 数据传播协议¶
Hyperledger Fabric 通过将工作负载拆分为交易执行(背书和提交)节点和交易排序节点的方式来优化区块链网络的性能、安全性和可扩展性。对网络操作这样的分割就需要一个安全、可靠和可扩展的数据传播协议来保证数据的完整性和一致性。为了满足这个需求,Fabric 实现了 Gossip 数据传播协议 。
Gossip 协议¶
Peer 节点通过 gossip 协议来广播来传播账本和通道数据。Gossip 消息是持续的,通道中的每一个 Peer 节点不断地从多个节点接受当前一致的账本数据。每一个 gossip 消息都是带有签名的,因此拜占庭成员发送的伪造消息很容易就会被识别,并且非目标节点也不会接受与其无关的消息。Peer 节点会受到延迟、网络分区或者其他原因影响而丢失区块,这时节点会通过从其他拥有这些丢失区块的节点处同步账本。
基于 gossip 的数据传播协议在 Fabric 网络中有三个主要功能:
- 通过持续的识别可用成员节点来管理节点发现和通道成员,还有检测离线节点。
- 向通道中的所有节点传播账本数据。所有没有和当前通道的数据同步的节点会识别丢失的区块,并将正确的数据复制过来以使自己同步。
- 通过点对点的数据传输方式,使新节点以最快速度连接到网络中并同步账本数据。
Peer 节点基于 Gossip 的数据广播操作接收通道中其他的节点的信息,然后将这些信息随机发送给通道上的一些其他节点,随机发送的节点数量是一个可配置的常量。Peer 节点可以用“拉”的方式获取信息而不用一致等待。这是一个重复的过程,以使通道中的成员、账本和状态信息同步并保持最新。在分发新区块的时候,通道中**主**节点从排序服务拉取数据然后开始在它所在的组织的节点中分发。
领导者选举¶
领导者的选举机制用于在每一个组织中**选举**出一个用于链接排序服务和初始分发新区块的节点。领导者选举使得系统可以有效地利用排序服务的带宽。领导者选举模型有两种模式可供选择:
- 静态模式:系统管理员手动配置一个节点为组织的主节点。
- 动态模式:组织中的节点自己选举出一个主节点。
静态主节点选举¶
静态主节点选举允许你手动设置组织中的一个或多个节点节点为主节点。请注意,太多的节点连接到排序服务可能会影响带宽使用效率。要开启静态主节点选举模式,需要配置 core.yaml
中的如下部分:
peer:
# Gossip related configuration
gossip:
useLeaderElection: false
orgLeader: true
另外,这些配置的参数可以通过环境变量覆盖:
export CORE_PEER_GOSSIP_USELEADERELECTION=false
export CORE_PEER_GOSSIP_ORGLEADER=true
注解
下边的设置会使节点进入**旁观者**模式,也就是说,它不会试图成为一个主节点:
export CORE_PEER_GOSSIP_USELEADERELECTION=false
export CORE_PEER_GOSSIP_ORGLEADER=false
不要将 CORE_PEER_GOSSIP_USELEADERELECTION
和 CORE_PEER_GOSSIP_ORGLEADER
都设置为 true
,这将会导致错误。
在静态配置组织中,主节点失效或者崩溃都需要管理员进行处理。
动态主节点选举¶
动态主节点选举使组织中的节点可以**选举**一个节点来连接排序服务并拉取新区块。这个主节点由每个组织单独选举。
动态选举出的的主节点通过向其他节点发送**心跳**信息来证明自己处于存活状态。如果一个或者更多的节点在一个段时间内没有收到**心跳**信息,它们就会选举出一个新的主节点。
在网络比较差有多个网络分区存在的情况下,组织中会存在多个主节点以保证组织中节点的正常工作。在网络恢复正常之后,其中一个主节点会放弃领导权。在一个没有网络分区的稳定状态下,会只有**唯一**一个活动的主节点和排序服务相连。
下边的配置控制主节点**心跳**信息的发送频率:
peer:
# Gossip related configuration
gossip:
election:
leaderAliveThreshold: 10s
为了开启动态节点选举,需要配置 core.yaml
中的以下参数:
peer:
# Gossip related configuration
gossip:
useLeaderElection: true
orgLeader: false
同样,这些配置的参数可以通过环境变量覆盖:
export CORE_PEER_GOSSIP_USELEADERELECTION=true
export CORE_PEER_GOSSIP_ORGLEADER=false
锚节点¶
gossip 利用锚节点来保证不同组织间的互相通信。
当提交了一个包含锚节点更新的配置区块时,Peer 节点会连接到锚节点并获取它所知道的所有节点信息。一个组织中至少有一个节点联系到了锚节点,锚节点就可以获取通道中所有节点的信息。因为 gossip 的通信是固定的,而且 Peer 节点总会被告知它们不知道的节点,所以可以建立起一个通道上成员的视图。
例如,假设我们在一个通道有三个组织 A、B`和`C,一个组织 C 定义的锚节点 peer0.orgC。当 peer1.orgA 联系到 peer0.orgC 时,它将会告诉 peer0.orgC 有关 peer0.orgA 的信息。稍后等 peer1.orgB 联系到 peer0.orgC 时,后者也会告诉前者关于 peer0.orgA 的信息。就像之前所说的,组织 A 和 B 可以不通过 peer0.orgC 而直接交换成员信息。
由于组织间的通信依赖于 gossip,所以在通道配置中必须至少有一个锚节点。为了系统的可用性和冗余性,我们强烈建议每个组织都提供自己的一些锚节点。注意,锚节点不一定和主节点是同一个节点。
外部和内部端点¶
为了让 gossip 高效地工作,Peer 节点需要包含其所在组织以及其他组织的端点信息。
当一个 Peer 节点启动的时候,它会使用 core.yaml
文件中的 peer.gossip.bootstrap
来广播自己并交换成员信息,并建立所属组织中可用节点的视图。
core.yaml
文件中的 peer.gossip.bootstrap
属性用于在 一个组织内部 启动 gossip。如果你要使用 gossip,通常会为组织中的所有节点配置为一个指向一组启动节点(使用空格隔开的节点列表)。内部端点通常是由 Peer 节点自动计算的,或者在 core.yaml
中的 core.peer.address
指明。
启动信息也同样需要建立**跨组织**的通信。初始的跨组织启动信息通过上面所说的“锚节点”设置提供。如果想让其他组织知道你所在组织中的其他节点,你需要设置 core.yaml
文件中的 peer.gossip.externalendpoint
。如果没有设置,节点的端点信息就不会广播到其他组织的 Peer 节点。
这些属性的设置如下:
export CORE_PEER_GOSSIP_BOOTSTRAP=<a list of peer endpoints within the peer's org>
export CORE_PEER_GOSSIP_EXTERNALENDPOINT=<the peer endpoint, as known outside the org>
Gossip 消息传递¶
在线的节点通过持续广播“存活”消息来表明可用,每一条消息都包含了“公钥基础设施(PKI)”ID 和发送者的签名。节点通过收集这些存活的消息来维护通道成员。如果没有节点收到某个节点的存活信息,这个“死亡”的节点会被从通道成员关系中剔除。因为“存活”的消息是经过签名的,恶意节点无法假冒其他节点,因为他们没有根 CA 授权的签名密钥。
除了自动转发接收到的消息之外,状态协调过程还会在每个通道上的 Peer 节点之间同步**世界状态**。每个 Peer 节点都持续从通道中的其他节点拉取区块,来修复他们缺失的状态。因为基于 gossip 的数据分发不需要固定的连接,所以该过程可靠地提供共享账本的一致性和完整性,包括对节点崩溃的容忍。
因为通道是隔离的,所以一个通道中的节点无法和其他通道通信或者共享信息。尽管节点可以加入多个通道,但是分区消息传递通过基于 Peer 节点所在通道的应用消息路由策略,来防止区块被分发到其他通道的 Peer 节点。
注解
- 通过 Peer 节点 TLS 层来处理点对点消息的安全性,不需要使用签名。Peer 节点通过 CA 签发的证书来授权。尽管没有使用 TLS 证书,但在 gossip 层使用了经过授权的 Peer 节点证书。账本区块通过排序服务签名,然后被分发到通道上的主节点。
- 通过 Peer 节点的成员服务提供者来管理授权。当 Peer 节点第一次连接到通道时,TLS 会话将与成员身份绑定。这就利用网络和通道中成员的身份来验证了与 Peer 节点相连的节点的身份。
常见问题¶
背书¶
背书架构:
问题: | 在一个网络中需要多少个 Peer 节点来对一笔交易进行背书? |
---|---|
回答: | 为一笔交易进行背书所需 Peer 节点的数量取决于部署链码的时候所指定的背书策略。 |
问题: | 一个应用程序客户端需要连接所有的 Peer 节点吗? |
回答: | 客户端只需要连接链码的背书策略所需数量的 Peer 节点即可。 |
安全和访问控制¶
问题: | 我如何来确保数据隐私? |
---|---|
回答: | 对于数据隐私,这里有很多个方面。首先,你可以使用通道来隔离你的网络,每个通道代表网络成员的一个子集,这些成员查看部署到该通道上的链码的相关数据。 其次,你可以使用 私有数据 来保护组织的数据隐私。一个私有数据集合规定了通道中的一部分组织可以背书、提交或者查询私有数据,而不需要额外创建一个通道。在这个通道上其他的参与者只会收到数据的哈希值。更多信息可以参考 在 Fabric 中使用私有数据 教程。注意,在本文档的“关键概念”章节也解释了 什么时候应该使用私有数据而不是通道 。 第三,作为 Fabric 使用私有数据来对于数据进行哈希的替代方案,客户端应用可以在调用链码之前将数据进行哈希或者加密。如果你将数据进行哈希运算的话,那么你需要提供一个方式来共享原始的数据。如果你将信息进行加密的话,那么你需要提供一个方式来共享解密秘钥。 第四,你可以通过在链码逻辑中增加访问控制的方式,将数据的访问权限限制在你的组织中的某些角色上。 第五,账本数据还可以通过在 Peer 节点上的文件系统加密的方式来进行加密,在数据在交换的过程中是通过 TLS 来进行加密的。 |
问题: | 排序节点能够看到交易数据吗? |
回答: | 不,排序节点仅仅对交易进行排序,他们不会打开交易。如果你不想让数据经过排序节点,那么应该使用 Fabric 的私有数据功能。或者,你可以在调用链码之前在客户端应用中对数据进行哈希运算或加密。如果你对数据进行加密的话,那么你需要提供一种方式来共享解密的秘钥。 |
应用程序端编程模型¶
问题: | 应用程序客户端如何知道一笔交易的结果? |
---|---|
回答: | 模拟交易的结果会在提案响应中通过背书节点返回给客户端。如果有多个背书节点,那么客户端会检查所有的反馈是否一样,并且提交结果和背书来进行交易的排序和提交。最终提交节点将会验证这笔交易是否有效,并且客户端会通过 SDK 获取交易的最终结果。 |
问题: | 我应该如何查询账本的数据? |
回答: | 在链码中,你可以基于键来查询。键可以按照范围来查询,复合键还可以通过多参数的组合进行查询。比如可以用一个复合键(owner, asset_id)查询所有某个实体拥有的资产。这些基于键的查询可以用来对于账本进行只读查询,也可以在更新账本的交易中使用。 如果你将资产数据在链码中定义为 JSON 格式并且使用 CouchDB 作为状态数据库的话,你也可以在链码中使用 CouchDB JSON 查询语句来对链码的数据进行富查询。应用程序客户端可以进行只读查询,这些反馈通常不会作为交易的一部分被提交到排序服务。 |
问题: | 我应该如何查询历史数据来了解数据的来源? |
回答: | 链码 API |
问题: | 如何保证查询的结果是正确的,尤其是当被查询的 Peer 节点可能正在恢复并且在获取缺失的区块? |
回答: | 客户端可以查询多个 Peer 节点,比较他们的区块高度、查询结果,选择具有更高的区块高度的节点。 |
链码(智能合约和数字资产)¶
问题: | Hyperledger Fabric 支持智能合约吗? |
---|---|
回答: | 是的。我们将这个功能称为链码。它是我们对于智能合约的实现,并且带有一些额外的功能。 链码是部署在网络上的程序代码,它会在共识过程中被链的验证者执行并验证。开发者可以使用链码来开发业务合约、资产定义以及共同管理的去中心化的应用。 |
问题: | 我如何创建一个业务合约? |
回答: | 通常有两种方式开发业务合约:第一种方式是将单独的合约编码到独立的链码实例中。第二种方式,也可能是更有效率的一种方式,是使用链码来创建去中心化的应用,来管理一个或者多个类型的业务合约的生命周期,并且让用户在这些应用中实例化这些合约的实例。 |
问题: | 我应该如何创建资产? |
回答: | 用户可以使用链码(对于业务规则)和成员服务(对于数字通证)来设计资产,以及管理这些资产的逻辑。 在大多区块链解决方案中由两种流行的方式来定义资产:无状态的 UTXO 模型,账户余额会被编码到过去的交易记录中;账户模型,账户的余额会被保存在账本的状态存储空间中。 每种方式都带有他们自己的好处及坏处。本区块链技术不主张任何一种方式。相反,我们的第一个需求就是确保两种方式都能够被轻松实现。 |
问题: | 支持哪些语言的链码开发? |
回答: | 链码能够使用任何的编程语言来编写并且在容器中执行。当前,支持 Golang、node.js 和 java 链码。 也可以使用 Hyperledger Composer 来构建 Hyperledger Fabric 应用。 |
问题: | Hyperledger 有原生的货币吗? |
回答: | 没有。然而,如果你的网络真的需要一个原生的货币的话,你可以通过链码来开发你自己的原生货币。对于原生货币的一个常用属性就是交易会引起余额的变动。 |
近期发布版本中的不同¶
问题: | 我在哪里能够看到在不同的发布版本中都有哪些变动? |
---|---|
回答: | 发布版本中的变动记录在 版本发布 中。 |
问题: | 如果还要其他问题的话,我在哪里可以获得技术上的帮助? |
回答: | 请使用 StackOverflow 。 |
排序服务¶
问题: | 我有一个正在运行的排序服务,如果我想要转换共识算法,我该怎么做? |
---|---|
回答: | 这个是不支持的。 |
问题: | 什么是排序节点系统通道? |
---|---|
回答: | 排序节点系统通道(有时被称为排序服务系统通道)是排序节点初始化时被启动的通道。它被用来编排通道的创建。排序节点系统通道定义了联盟以及新通道的初始配置信息。在通道被创建的时候,在联盟中定义的组织、/Channel 组中的值和策略以及 /Channel/Orderer 组中的值和策略,会被合并到一起来形成一个新的初始的通道定义。 |
问题: | 如果我更新了我的应用程序通道,我是否需要更新我的排序系统通道? |
---|---|
回答: | 一旦一个应用程序通道被创建,它的管理独立于其他任何的通道(包括排序节点系统通道)。基于所做的改动,变动可能需要也可能不需要被放置到其他的通道。一般来说,MSP 的变动应该被同步到所有的通道,而策略的变动一般是针对一个特定通道的。 |
问题: | 我可以有一个既作为一个排序节点又作为应用程序角色的组织吗? |
---|---|
回答: | 尽管这是可能的,但是我们强烈不建议这样配置。默认的 /Channel/Orderer/BlockValidation 策略允许任何具有有效的证书的排序组织来为区块签名。如果一个组织既是排序节点又是应用程序的话,那么这个策略就应该被更新为只有被授权来排序的证书的子集才可以为区块签名。 |
问题: | 我想要实现一个针对于 Fabric 的共识,我应该如何开始? |
---|---|
回答: | 一个共识的插件需要实现在 consensus 包 中定义 Consenter 和 Chain 接口。针对于这些接口已经有了两个插件:solo 和 kafka。你可以学习他们来为你自己的实现寻求线索。排序服务的代码可以在 orderer 包 中找到。 |
问题: | 我想要改变我的排序服务配置,比如批处理的超时时间,当我启动了网络之后,我该如何做? |
---|---|
回答: | 这属于网络的配置。请参考 configtxlator 。 |
Solo¶
问题: | 我如何在生产环境部署 Solo? |
---|---|
回答: | Solo 不该用于生产环境。它不是并且永远也不会是容错的。 |
Kafka¶
问题: | 我如何从排序服务中删除一个节点? |
---|---|
回答: | 这个流程有两步:
|
问题: | 我之前从来没有部署过一个 Kafka/ZK 集群,我想使用基于 Kafka 的排序服务。我应该如何做? |
---|---|
回答: | Hyperledger Fabric 文档假设阅读者大体上已经有了维护的经验来创建、设置和管理一个 Kafka 集群(查看 郑重声明)。如果没有这样的经验你还要继续的话,你应该在尝试基于 Kafka 的排序服务之前完成,至少 Kafka 快速开始指南 中的前六步。你也可以查看 这个示例配置文件 来了解一个关于 Kafka/ZooKeeper 的合理默认值简单的解释。 |
问题: | 我从哪里能够找到使用基于 Kafka 的排序服务的 Docker 组件? |
---|---|
回答: | 查看 端到端的 CLI 示例. |
问题: | 为什么在基于 Kafka 的排序服务中会有对于 ZooKeeper 的依赖? |
---|---|
回答: | Kafka 在内部使用 ZooKeeper 来在它的 brokers 之间进行协调。 |
问题: | 我尝试按照 BYFN 示例,但是遇到一个 “service unavailable” 错误,我应该怎么做? |
---|---|
回答: | 查看排序服务的日志。“Rejecting deliver request because of consenter error” 的错误日志一般是和 Kafka 集群的连接错误引起的。请确保 Kafka 集群设置正确并且排序服务节点可以与之连接。 |
欢迎贡献!¶
我们欢迎以各种形式来为 Hyperledger 做贡献,这里有很多事可以做!
参与之前,请先回顾一下 行为准则 。保证文明合法是很重要的。
贡献的方法¶
不管作为普通用户还是开发者,这里都有很多为 Hyperledger Fabric 做贡献的方法。
作为普通用户:
- 提出功能或改进的建议
- 反馈错误
- 帮助测试在 发布路线 (release map)上即将发布的史诗(Epic)。将问题通过 Jira 或者 RocketChat 反馈给开发者。
作为开发者:
- 如果你的时间不多,可以考虑选择一些 “help-wanted” (需要帮助的)任务,参考 修复问题和认领正在进行的故事 (Story)。
- 如果你是全职开发,可以提出一个新的特性(参考 提出功能或改进的建议 )并带领一个团队来实现它,或者加入已存在的史诗的团队。如果你在 发布路线 发现了一个你感兴趣的史诗,请及时通过 Jira 或者 RocketChat 联系分配到任务的人,和他们一起完成这个史诗。
获取 Linux Foundation 账号¶
为了参与到 Hyperledger Fabric 项目开发中来,你首先需要一个 Linux Foundation 账号 。你要使用你的 LF ID 来访问所有 Hyperledger 社区的工具,包括 Gerrit , Jira , RocketChat ,和 Wiki (仅用于编辑)。
项目管理¶
正如我们的 章程 中所说,Hyperledger Fabric 是在一个开放治理的模型下管理的。项目和子项目由维护者主导。新的子项目可以指定一组初始的维护人员,这些维护人员将在项目首次批准时由顶级项目的现有维护人员批准。
成为一名维护者¶
项目的维护者会时不时地考虑添加或者删除维护者。现有的维护者可以提交变更到 MAINTAINERS.rst 文件中。一个提名的维护者可以由大多数现有的维护者批准通过成为正式的维护者。一旦批准通过,变更就会被合并同时该维护者也会被添加(或者移除)到维护者组中。维护者可能会因为明确的辞职、长时间的不活动(超过3个月或者更长的时间),或者因为违反相关的 行为准则 或持续表现出糟糕的判断而被移出维护者的队列。在恢复贡献和评审(一个月或更长时间)之后,应恢复因不活动而被移除的维护人员。
发布节奏¶
Fabric 的维护者已经确定了每个季度大致的发布节奏(请参考 发布说明)。我们也在积极考虑采用LTS(long term support)的发布过程,虽然这些细节需要由具体的维护者决定。相关细节请参考在 Chat 的 #fabric-maintainers 中的讨论。
提出功能或改进的建议¶
首先,请查看 JIRA 确保之前没有开启或者关闭的相同功能的提案。如果没有,为了开启一个提案,我们建议创建一个 Jira 的史诗(Epic)或者故事(Story),选择一个最合适的环境,并附上一个链接或者内嵌一个提案的页面,说明这个特性是做什么的,如果可能的话,描述一下它应该如何实现。这有助于说明为什么应该添加这个特性,例如确定需要该特性的特定用例,以及如果实现该特性的好处。一旦 Jira 的 issue 被创建了,并且描述中添加了附加的或者内嵌的页面或者一个公开的可访问的文档链接,就可以向 fabric@lists.hyperledger.org 邮件列表发送介绍性的电子邮件,邮件中附上 Jira issue 的链接,并等待反馈。
对建议性的特性的讨论应该在 JIRA issue 本身中进行,这样我们就可以在社区中有一个统一的方式来找到这个设计的讨论。
获得3个或者更多的维护者对新特性的支持将会大大提高该特性相关的变更申请被合并到下一次发布的可能性。
维护者会议¶
维护者会在每隔一周周三的东部时间9点在 Zoom 上举行双周会议。请参考 community calendar 获取具体信息。
维护者的会议的目的是为了计划以及审查发布的进度,同时讨论项目或者子项目的技术以及操作方向上的事宜。
如上所述新特性或增强的建议应该在维护者的会议上进行探讨、反馈和接受。
交流¶
我们使用 RocketChat 来进行交流或者实用 Google Hangouts™ 进行屏幕分享。我们的开发计划和优先级在 JIRA 上进行发布,同时我们也花大量的时间在 mailing list 上进行讨论才做决定。
贡献指南¶
安装准备¶
在我们开始之前,如果你还没有这样做那你可能需要检查一下您是否已经在将要开发区块链应用或者运行 Hyperledger Fabric 的平台上安装了运行所需的环境。
获得帮助¶
如果你试图寻找一种途径来寻找专家援助或者解决一些问题,我们的 社区 会为您提供帮助。我们在 Chat ,IRC(#hyperledger on freenode.net)以及 mailing lists 中都可以找到。我们大多数人都很乐意提供帮助。唯一愚蠢的是你不去问。问题实际上是帮助改进项目很好的方法,因为它们使我们的文档更加清晰。
反馈错误¶
如果你是一个用户,并且发现了错误,请使用 JIRA 来提交问题。在您创建新的 JIRA 问题之前,请尝试搜索是否有人已经提过类似的问题,确保之前没有人报告过。如果之前有人报告过,那么你可以添加评论表明你也期望这个问题被修复。
注解
如果缺陷与安全相关,请遵循 Hyperledger 安全问题处理流程 。
如果以前没有报告过,请创建一个新的 JIRA。请尝试为其他人提供足够多的信息以重现该问题。该项目的维护人员会在24小时之内回复您的问题。如果没有,请通过评论提出问题,并要求对其进行评审。您还可以在 Hyperledger Chat 中将问题发布到相关的 Hyperledger Fabric 频道中。比如,可以将文档问题在 #fabric-documentation
中进行广播,数据存储问题可以在 #fabric-ledger
中广播,以此类推。
提交你的修复¶
如果你在 JIRA 上提交了你刚刚发现的问题,并希望修复它,我们很乐意并且非常欢迎。请将 JIRA 问题分配给自己,然后您可以提交变更请求(CR)。
注解
如果你在提交第一个 CR 的时候需要帮助,我们已经为你创建了一个简短的 教程 。
修复问题和认领正在进行的故事¶
查看 问题列表 找到你感兴趣的内容。您也可以从 “求助” 列表中寻找。明智的做法是从相对直接和可实现的任务开始,并且这个任务是未被分配的。如果没有分配给别人,请将问题分配给自己。如果你无法在合理的时间内完成,请加以考虑并且取消认领,如果你需要更多的时间,请添加评论加以说明,你正在积极处理问题。
审核提交的变更请求(CR)¶
另一种贡献和了解 Hyperledger Fabric 的方法是帮助维护人员审查开放的 CR。实际上维护者的工作很辛苦,他们需要审查所有正在提交的 CR 并且评估他们是否应该被合并。您可以查看代码或者修改文档,测试更改的内容,并告知提交者和维护者您的想法。完成审核或测试后,只需要添加评论和投票,即可完成回复 CR。评论“我在系统 X 上尝试过这个 CR,是正确的”或者“我在系统 X 上运行这个 CR 发现了一些错误”将帮助维护者进行评估。因此,维护人员也能够更快地处理 CR,并且每个人都能从中获益。
浏览 Gerrit 上开放的 CR 开始你的贡献。
什么是更好的变更请求?¶
- 一次只包含一个变更。不是5个,3个,或者10个。仅仅一个变更。为什么呢?因为它限定了问题的范围。如果我们有一个回归,相对影响了很多代码的复杂更改,我们更容易识别错误的提交。
- 在 JIRA 的故事中包含一个链接。为什么?因为 a)我们希望追踪你的进度以便更好地判断我们可以传递什么信息。 b) 因为我们可以证明这次变更是有效的。在很多情况下,会有很多围绕提交变更的讨论,我们希望将它链接到它的本身。
- 每次变更都包含单元或者集成测试(或者对已有测试的修改)。不仅仅是正确的测试。同样也要包括一些异常测试来捕获错误。在你写代码的时候,你有责任去测试它并且证明你的变更是正确的。为什么呢?因为没有这些,我们无法知道你的代码是否真的正确地工作。
- 单元测试不要有额外的依赖。你应该使用
go test
或者等价的语言测试方式来运行单元测试。任何需要额外依赖的测试(例如需要用脚本来运行另一个组件)需要适当的 mocking。任何除了单元测试以外的测试根据定义都是集成测试。为什么?因为很多开源软件开发者都实用测试驱动的开发方式。他们关注一个目录下的测试用例,一旦代码变更了他们采用测试去判断他们的代码是否正确。相比当代码变更后运行整个项目来说,这是非常高效的。请参考 单元测试的定义 在脑海中建立单元测试的标准,以此来写出高效的单元测试。 - 最小化每个 CR 的代码行数。为什么?因为维护者每天同样也有工作。如果你发送1000或者2000行代码,你认为维护者需要多久才能审查完你的代码?尽可能地保证你的变更在200-300行左右。如果你有一个比较大的变更,可以将它分解为比较小的几个独立的变更。如果要添加一组新功能来满足一个需求,请在测试中分别添加它们,然后编写满足需求的代码。当然,总会有意外。如果你增加一些小变动然后添加了300行测试,你将会被宽恕;-)如果你需要做一个变更,而且影响比较广或者生成了很多代码(protobufs 等)。同样,也是个例外。
注解
大的变更,例如那些大于300行的 CR 将更有可能收到 -2,并且你可能被要求重构以符合本指南。
- 不要堆叠你的变更请求(例如在先前的变更请求的本地分支提交你的变更)除非它们是相关联的。这将最大幅度减少合并冲突,并且更快地合并。如果你堆叠你的变更请求,由于前面的请求中的审核注释,你后续的请求将被搁置。
- 写一个有意义的提交信息。包括55个或者更少字符的标题,后面跟一行空行,然后跟上更全面的关于变更的描述。 每个变更必须包括对应的变更的 JIRA 编号(例如[FAB-1234])。这个可以在标题中,但是同样需要包括在消息正文中。查看可接受的变更的 完整要求 。
注解
Gerrit 会自动创建超级链接到 JIRA 的条目。例如
[FAB-1234] fix foobar() panic
Fix [FAB-1234] added a check to ensure that when foobar(foo string)
is called, that there is a non-empty string argument.
最后,要有回应。不要让一个变更请求因为评审意见而不了了之,这样会导致它需要进行 rebase。这只会进一步延迟合并,给你带来更多的工作,比如修复合并冲突。
法律材料¶
Note: 每一个源文件必须包括 Apache Software License 2.0。可以参考 license header.
我们尽可能努力让贡献变得简单。这个协议为我们提供了贡献相关的法律相关的知识。我们使用和 Linux® Kernel 社区 一样的管理贡献的方法 Developer’s Certificate of Origin 1.1 (DCO) 来管理Hyperledger Fabric。
我们只要求在提交要审查的补丁时,开发者在 commit 消息中带上他们的离线签名即可。
这里是一个离线签名的签名例子,表示提交者接受 DCO 约定:
Signed-off-by: John Doe <john.doe@example.com>
你可以使用 git commit -s
在提交的时候自动带上你的签名。
相关主题¶
维护者¶
Active Maintainers
Name | Gerrit | GitHub | Chat | |
---|---|---|---|---|
Alessandro Sorniotti | ale-linux | ale-linux | aso | ale.linux@sopit.net |
Artem Barger | c0rwin | c0rwin | c0rwin | bartem@il.ibm.com |
Binh Nguyen | binhn | binhn | binhn | binh1010010110@gmail.com |
Chris Ferris | ChristopherFerris | christo4ferris | cbf | chris.ferris@gmail.com |
Dave Enyeart | denyeart | denyeart | dave.enyeart | enyeart@us.ibm.com |
Gari Singh | mastersingh24 | mastersingh24 | garisingh | gari.r.singh@gmail.com |
Greg Haskins | greg.haskins | ghaskins | ghaskins | gregory.haskins@gmail.com |
Jason Yellick | jyellick | jyellick | jyellick | jyellick@us.ibm.com |
Jonathan Levi | JonathanLevi | hacera | JonathanLevi | jonathan@hacera.com |
Keith Smith | smithbk | smithbk | smithbk | bksmith@us.ibm.com |
Kostas Christidis | kchristidis | kchristidis | kostas | kostas@gmail.com |
Manish Sethi | manish-sethi | manish-sethi | manish-sethi | manish.sethi@gmail.com |
Matthew Sykes | sykesm | sykesm | sykesm | sykesmat@us.ibm.com |
Srinivasan Muralidharan | muralisr | muralisrini | muralisr | srinivasan.muralidharan99@gmail.com |
Yacov Manevich | yacovm | yacovm | yacovm | yacovm@il.ibm.com |
Release Managers
Name | Gerrit | GitHub | Chat | |
---|---|---|---|---|
Chris Ferris | ChristopherFerris | christo4ferris | cbf | chris.ferris@gmail.com |
Dave Enyeart | denyeart | denyeart | dave.enyeart | enyeart@us.ibm.com |
Gari Singh | mastersingh24 | mastersingh24 | garisingh | gari.r.singh@gmail.com |
Retired Maintainers
Gabor Hosszu | hgabre | gabre | hgabor | gabor@digitalasset.com |
Sheehan Anderson | sheehan | srderson | sheehan | sranderson@gmail.com |
Tamas Blummer | TamasBlummer | tamasblummer | tamas | tamas@digitalasset.com |
Jim Zhang | jimthematrix | jimthematrix | jimthematrix | jim_the_matrix@hotmail.com |
Yaoguo Jiang | jiangyaoguo | jiangyaoguo | jiangyaoguo | jiangyaoguo@gmail.com |
通过 Jira 来了解当前的工作¶
本文档旨在让你进一步了解基于社区路线图的 Hyperledger Fabric v1 架构方面的工作进展。我们通过 Jira 来管理路线图。
我们用冲刺(sprint)来管理,并且按照反馈列表中问题实现的优先级进行排序展示。我们使用面板来实现这些功能。通过点击 Boards -> Manage Boards 来查看这些面板:

Jira boards
现在点击屏幕左侧的 全部面板 :

Jira boards
在这里,你可以看到所有公共(或者受限制的)面板。如果你想看当前冲刺下的美容,请点击 可见性 为 全部用户,而且 面板类型 是 Scrum 的条目。举个例子, 面板名称 Consensus:

Jira boards
当你点击了 面板名称 下的 Consensus 之后,你会被重定向到下面的列中:

Jira boards
这些列的含义如下:
- Backlog – 为当前冲刺计划的条目(冲刺的定义是两周一个迭代), 但是不在当前的进程中。
- In progress – 由某些人正在进行的条目。
- In Review – 在 Gerrit 中正在等待审查和合并的条目。
- Done – 在这个冲刺中已经合并完成的条目。
如果你希望查看某一个特性的 backlog 的所有条目,点击导航栏左侧的行即可:

Jira boards
这张图展示了当前的冲刺的条目,所有的底部的待办的条目。所有的条目都按优先级排列。
如果有一个你感兴趣的条目正在进行,你有一些问题或者想了解一些信息,或者你觉得有些条目有必要提高优先级,请在 Jira上 直接评论。感谢所有有帮助的反馈和帮助。
设置开发环境¶
概览¶
在 v1.0.0 之前,使用 Vagrant 运行 Ubuntu 镜像来作为开发环境来加载 Docker 镜像,以此让使用不同操作系统的开发者有相同的开发体验,比如 macOS, Windows, Linux 或者其他系统。大多数操作系统都已经原生支持 Docker 了,比如 macOS、Windows。因此我们已经开始使用 Docker 来进行构建了。但是我们仍然维护着 Vagrant 的方法,因为有一些旧版本的 macOS 和 Windows 系统不支持 Docker。我们强烈建议不要再使用 Vagrant 开发了。
值得提醒的是,基于 Vagrant 的开发不能部署在云环境中,但是基于 Docker 可以,目前支持的云平台有 AWS、Azure、Google 和 IBM 等。请参考下边的内容在 Ubuntu 下构建。
前置工作¶
- Git 客户端
- Go - 版本 1.11.x
- (macOS) 必须安装 Xcode
- Docker - 17.06.2-ce 或更高版本
- Docker Compose - 1.14.0 或更高版本
- Pip
- (macOS) 你必须安装 gnutar,macOS 默认使用 bsdtar,但是构建使用了一些 gnutar 的参数。你可以使用 Homebrew 来安装它:
brew install gnu-tar --with-default-names
- (macOS) Libtool 。你可以使用 Homebrew 来安装它:
brew install libtool
- (只有使用 Vagrant 的需要安装) - Vagrant - 1.9 或更高版本
- (只有使用 Vagrant 的需要安装) - VirtualBox - 5.0 或更高版本
- BIOS 开启虚拟化,因硬件而异
- 注意: BIOS虚拟化设置可能在 BIOS 的 CPU 或者 Security 设置里。
pip
¶
pip install --upgrade pip
步骤¶
设置 GOPATH¶
确保你已经设置了你主机的 GOPATH 环境变量 。只有设置了环境变量才能在你的主机或者虚拟机中编译。
如果你安装的 Go 不在默认位置,请确保你设置了 GOROOT 环境变量 。
Windows 用户请注意¶
如果你用的是 Windows 系统,在运行 git clone
命令之前,请先运行如下命令。
git config --get core.autocrlf
如果 core.autocrlf
为 true
, 你必须将它设置为 false
git config --global core.autocrlf false
如果你将 core.autocrlf
设置为 true
,vagrant up
命令将会报错:
./setup.sh: /bin/bash^M: bad interpreter: No such file or directory
克隆 Hyperledger Fabric 项目源代码¶
因为 Hyperledger Fabric 是用 Go
编写的,所以你需要将它克隆到 $GOPATH/src 目录。如果你有多个 $GOPATH,请选择第一个 $GOPATH。有一些内容需要设置:
cd $GOPATH/src
mkdir -p github.com/hyperledger
cd github.com/hyperledger
重申一下,我们使用 Gerrit
来管理源码,Gerrit
内部有自己的 git 仓库。因此我们需要从 Gerrit 克隆。命令如下:
git clone ssh://LFID@gerrit.hyperledger.org:29418/fabric && scp -p -P 29418 LFID@gerrit.hyperledger.org:hooks/commit-msg fabric/.git/hooks/
注意: 当然你要将 LFID
替换为你的 Linux Foundation ID 。
使用 Vagrant 启动虚拟机¶
如果你想用 Vagrant 来开发,你需要执行如下步骤。再说一次,我们只建议在不支持 Docker 的系统中使用该方法。
cd $GOPATH/src/github.com/hyperledger/fabric/devenv
vagrant up
来杯咖啡。。。这需要点时间。完成后,你就可以通过 ssh
进入刚才创建的 Vagrant 虚拟机了。
vagrant ssh
进入到虚拟机之后,你就可以看到 $GOPATH/src/github.com/hyperledger/fabric
下边的源码了。它被挂载在 /hyperledger
目录。
注意事项¶
注意: 你修改的 fabric 文件(在 $GOPATH/src/github.com/hyperledger/fabric
目录下)会同步在虚拟机中的 fabric 文件夹中更新。
注意: 如果你想在 HTTP 代理下运行开发环境,你就需要配置客户端。你可以通过 vagrant-proxyconf 来完成。通过 vagrant plugin install vagrant-proxyconf
进行安装,然后在执行 vagrant up
之前 配置 VAGRANT_HTTP_PROXY and VAGRANT_HTTPS_PROXY 环境变量。详情请查看这里: here: https://github.com/tmatilai/vagrant-proxyconf/
注意: 第一次运行的时候会比较耗时(可能会超过30分钟),在这期间你会以为它什么都没做。如果没有报错,就让它继续运行。
Windows 10的用户请注意: T在 Windows 10下有一个已知的 vagrant 问题(查看 hashicorp/vagrant#6754)。如果 vagrant up
命令执行失败,可能是因为你没有安装 Microsoft Visual C++ Redistributable。你可以在这个地址下载缺失的软件包:http://www.microsoft.com/en-us/download/details.aspx?id=8328
注意: 编译 fabric 的过程中 miekg/pkcs11 依赖 ltdl.h。请确认您安装了 libtool 和 libltdl-dev。否则你会遇到丢失 ltdl.h 头文件错误。你可以通过该命令安装缺失的软件包: sudo apt-get install -y build-essential git make curl unzip g++ libtool
。
构建 Hyperledger Fabric¶
下面的教程假设你已经设置好了你的 开发环境 。
构建 Hyperledger Fabric:
cd $GOPATH/src/github.com/hyperledger/fabric
make dist-clean all
运行单元测试¶
使用下面的命令运行期所有单元测试
cd $GOPATH/src/github.com/hyperledger/fabric
make unit-test
要它运行一部分测试的话,要设置环境变量 TEST_PKGS。指定一个包的列表(用空格隔开),例如:
export TEST_PKGS="github.com/hyperledger/fabric/core/ledger/..."
make unit-test
要运行指定的测试,需要使用 -run RE
参数,RE 的意思是正则匹配测试用例的名字。使用 -v
参数显示输出。例如运行测试用例 TestGetFoo
,切换到包含 foo_test.go
的目录,然后执行:
go test -v -run=TestGetFoo
运行 Node.js 客户端 SDK 的单元测试¶
要保证 Node.js client SDK 没有因为你的修改而出问题,你就必须运行 Node.js 单元测试。 要运行 Node.js 单元测试,请遵循`下述 <https://github.com/hyperledger/fabric-sdk-node/blob/master/README.md>`__ 指南。
在 Vagrant 之外构建¶
在 Vagrant 之外构建工程和运行节点也是可以的。你需要将 Vagrant 配置文件 “翻译” 成与你选择的平台匹配的内容。
在 Z 上构建¶
为了方便快捷地在 Z 上构建,我们提供了 这个脚本 (和 Vagrant 的 配置文件 类似)。该脚本只在 RHEL 7.2 上测试过,并且有一些设置你可能需要参考(防火墙设置,使用 root 用户开发等)。它仅仅适用于在个人开发者的虚拟机上进行开发。
从一个新安装的系统开始:
sudo su
yum install git
mkdir -p $HOME/git/src/github.com/hyperledger
cd $HOME/git/src/github.com/hyperledger
git clone http://gerrit.hyperledger.org/r/fabric
source fabric/devenv/setupRHELonZ.sh
后边的步骤请参考 Vagrant 开发环境。
cd $GOPATH/src/github.com/hyperledger/fabric
make peer unit-test
在 Power 平台上构建¶
我们已经实现了在 vagrant 意外的 Power(ppc64le)系统上构建,参考 `这里<#building-outside-of-vagrant>`__ 。要想方便地在 Ubuntu 环境中构建,请使用 root 用户执行 这个脚本 。这个脚本已经在 Ubuntu 16.04 上验证过了,但是也需要设置一些内容(比如,安装系统仓库,防火墙设置等等),这个脚本可以进一步改进。
要开始在 Ubuntu 上安装的 Power 服务器上构建,你首先要确保设置好了 GOPATH 环境变量 。然后执行如下命令构建 Fabric 。
mkdir -p $GOPATH/src/github.com/hyperledger
cd $GOPATH/src/github.com/hyperledger
git clone http://gerrit.hyperledger.org/r/fabric
sudo ./fabric/devenv/setupUbuntuOnPPC64le.sh
cd $GOPATH/src/github.com/hyperledger/fabric
make dist-clean all
在 Centos 7 上构建¶
你必须从源码安装 CouchDB,因为该发行版上没有 CouchDB 的安装包。如果你想使用多个排序节点,你同样需要从源码安装 Apache Kafka。Apache Kafka 包括 Zookeeper 和 Kafka 的可执行程序和相关配置文件。
export GOPATH={directory of your choice}
mkdir -p $GOPATH/src/github.com/hyperledger
FABRIC=$GOPATH/src/github.com/hyperledger/fabric
git clone https://github.com/hyperledger/fabric $FABRIC
cd $FABRIC
git checkout master # <-- only if you want the master branch
export PATH=$GOPATH/bin:$PATH
make native
如果你不想使用构建 Docker 镜像,那你就需要原生的。
配置¶
peer 程序需要使用 core.yaml 作为配置文件。很多配置都可以被带有 ‘CORE_’ 前缀的环境变量覆盖。例如,通过环境变量设置日志级别:
CORE_PEER_LOGGING_LEVEL=CRITICAL peer
申请 Linux Foundation 账号¶
向 Hyperledger Fabric 贡献代码需要 Linux Foundation 账户,如果你没有的话,请根据下面的步骤创建一个。
创建 Linux Foundation ID¶
- 登录 Linux Foundation ID 网站 。
- 选择
I need to create a Linux Foundation ID
选项,然后填写出现的表单。 - 等待几分钟,查看邮箱中的邮件是否有如下内容:”Validate your Linux Foundation ID email”。
- 打开收到的链接来验证你的邮箱。
- 验证你的浏览器显示了如下信息:
You have successfully validated your e-mail address
。 - 点击
Sign In
来登录 Gerrit, 然后使用你的 Linux Foundation account ID 进行登录。
在 Gerrit 上配置 SSH¶
Gerrit 使用 SSH 来和 Git 客户端交互。如果你已经有了 SSH 密钥对,那么你可以跳过这个部分。
下述步骤解释了如何在 Linux 环境下生成 SSH 密钥对,不同操作系统的命令有所不同,请在你的操作系统上进行相应的操作。
首先,使用如下命令创建 SSH 密钥对:
ssh-keygen -t rsa -C "John Doe john.doe@example.com"
Note: 这会提示你输入一个密码来保护这个唯一的私钥。请对密码进行保密,不要填写空格。
生成的 SSH 密钥对可以在 ~/.ssh/id_rsa
和 ~/.ssh/id_rsa.pub
文件中找到。
接下来,把在 id_rsa
文件中的私钥添加到密钥库中,例如:
ssh-add ~/.ssh/id_rsa
最后,将生成的密钥对的公钥添加到 Gerrit 服务端,按照如下步骤:
- 进入 Gerrit 。
- 点击右上角你的账号名。
- 从弹出的菜单中选择
Settings
。 - 在左侧的菜单上,点击
SSH Public Keys
。 - 复制你的公钥的内容
~/.ssh/id_rsa.pub
点击Add key
。
Note: id_rsa.pub
文件可以在文本编辑器中打开,确保选中该文件所有的内容,复制粘贴到 Gerrit 的 Add SSH key
窗口中。
Note: SSH 密钥生成指南是假设你用默认命名的前提下操作的。当然也可以使用不同的命名生成多个 SSH 密钥对。参考 ssh-keygen 文档进行操作。一旦你生成了一个不是默认名字的密钥对,你需要配置 SSH 对 Gerrit 使用正确的密钥。在这种情况下,你需要仿照下面的模板创建 ~/.ssh/config
配置文件。
host gerrit.hyperledger.org
HostName gerrit.hyperledger.org
IdentityFile ~/.ssh/id_rsa_hyperledger_gerrit
User <LFID>
LFID 是你的 Linux Foundation ID,IdentityFile 是你生成的公钥的名字。
Warning: 潜在的危险! 不要拷贝你的私钥 ~/.ssh/id_rsa
仅仅使用公钥 ~/.ssh/id_rsa.pub
。
检出源代码¶
一旦你设置了之前讲的 SSH,你可以通过下面的命令克隆源代码:
git clone ssh://<LFID>@gerrit.hyperledger.org:29418/fabric fabric
你已经成功地在你的电脑上检出了源代码。
Gerrit 的使用¶
按照这些说明通过 Gerrit 审查系统在 Hyperledger Fabric 上进行协作。
请确保你已经订阅了 邮件列表 。当然如果你需要帮助的话也可以在 chat 寻求帮助。
Gerrit 为用户分配了以下角色:
- 提交者(Submitters): 可以提交变更,审查别人的代码变更,分别通过投票 +1 或者 -1 接受或者拒绝的建议。
- 维护者(Maintainers): 基于审查者的反馈进行 +2 或者 -2 的投票,以此来批准或者拒绝变更。
- 构建者(Builders): (例如 Jenkins)可以使用自动化基础架构来验证变更。
维护者应当熟悉 审核流程 。但是,非常欢迎任何人来审核变更,这样才会发现文档的价值。
Git-review¶
有一个非常有用的和 Gerrit 合作的工具叫做 git-review 。这个命令行工具可以帮你执行大部分后续工作。当然,强烈推荐阅读以下内容,以便了解幕后发生的事情。
配合本地仓库使用¶
无论处理新特性还是错误:
- 打开 Gerrit 的 项目页面 。
- 选择一个你希望工作的项目。
- 打开一个终端窗口,然后使用
Clone with git hook
URL 克隆项目。确定使用ssh
,这将会简化授权:
git clone ssh://LFID@gerrit.hyperledger.org:29418/fabric && scp -p -P 29418 LFID@gerrit.hyperledger.org:hooks/commit-msg fabric/.git/hooks/
注解
如果要克隆项目仓库,你需要将它克隆到 $GOPATH/src/github.com/hyperledger
目录下,这样它才能构建,并使用 Vagrant 开发环境 。
- 在你克隆的仓库创建一个分支,分支的名字最好能描述该分支的功能。
cd fabric
git checkout -b issue-nnnn
- 提交你的代码。要创建可以深入探讨的提交,请阅读 如何提交更改的文档 。
git commit -s -a
请输入准确的可读的消息然后提交。
- 任何影响文档的代码变更都应该有对文档和测试的相应更改(或者添加)。这保证了如果变更被合并了,那同样所有的有关的文档和测试都会更新。
使用 git review¶
注解
如果你希望,你可以使用 git-review 工具替代以下内容。例如
添加以下部分 .git/config
,并用你的gerrit id替换 <USERNAME>
。
[remote "gerrit"]
url = ssh://<USERNAME>@gerrit.hyperledger.org:29418/fabric.git
fetch = +refs/heads/*:refs/remotes/gerrit/*
然后用 git review
提交你的更改。
$ cd <your code dir>
$ git review
当你更新补丁的时候,你可以使用 git commit --amend
来提交,然后重复 git review
命令。
不使用git review¶
请参考 构建源代码指南
当准备好提交变更的时候,Gerrit要求将变更推送到特殊的分支上。 这个特殊的分支需要包含当代码被接受之后被合并的对最终代码分支的引用。
对于Hyperledger Fabric的仓库来说,特殊的分支叫做``refs/for/master`` 。
打开本地仓库的根目录的终端窗口,推送本地开发分支的代码到服务器上:
cd <your clone dir>
git push origin HEAD:refs/for/master
如果命令正确执行了,输出将和下述类似:
Counting objects: 3, done.
Writing objects: 100% (3/3), 306 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: Processing changes: new: 1, refs: 1, done
remote:
remote: New Changes:
remote: https://gerrit.hyperledger.org/r/6 Test commit
remote:
To ssh://LFID@gerrit.hyperledger.org:29418/fabric
* [new branch] HEAD -> refs/for/master
Gerrit服务器生成了一个可以被追踪的链接。
使用Gerrit进行审核¶
- Add: 这个按钮可以让提交者添加进行审查的人员的名字; 开始输入一个名字,系统会基于注册的用户和系统的权限进行自动补全。 如果你请求他们来审查代码,他们会收到邮件。
- Abandon: 这个按钮仅提供给提交者使用;它允许提交者放弃更改并将其从合并队列中删除。
- Change-ID: 这个ID由Gerrit(或者系统)生成。 当审核过程中确定你的提交必须被修改时,将会变得有用。 你需要提交一个新的版本;如果 Change-ID是同样的,Gerrit会记住,并且呈现同一个变更的另一个版本。
- Status: 目前,示例已经进入审查状态,在左上角显示 “Needs Verified” 。 审查者将会发表他们的意见,如果同意则+1,不同意则-1。 具有维护者角色的Gerrit用户可以通过投票+2或者-2来表示同意或者拒绝合并。
- 通知将发送到您的提交消息的Signed-by-by行中的电子邮件地址。访问您的
- Gerrit 仪表盘 ,检查您的请求进度。
Gerrit中的历史记录将显示内嵌注释和审阅者信息。
查看待定的更改¶
点击左上角 All --> Changes
查看所有待定的变更,或者
打开这个链接 。
如果你在多个项目中协作,你可能希望通过右上方的搜索栏限制搜索特定分支。
添加 project:fabric 过滤器来限制仅显示Hyperledger Fabric的更改。
通过选择 My --> Changes
或者 打开这个链接
列出你提交的所有变更。
向 Gerrit 上提交变更¶
在向 Hyperledger Fabric 代码库提交更改之前,请仔细检查以下内容。这些指导原则适用于刚接触开源的开发人员,也适用于经验丰富的开源开发人员。
变更需求¶
本节包含提交要审核的代码变更的指南。有关如何使用 Gerrit 提交更改的更多信息,请参见这里 Gerrit 的使用。
所有对 Hyperledger Fabric 的更改都是在 Git 通过 Gerrit 提交时提交的。每次提交必须包含:
- 一个简短的描述性的主题行,长度不超过55个字符,后面跟一行空行,
- 一个变更描述,包括您的变更的逻辑或推理,后面跟一行空行,
- Signed-off-by 签名行,后跟冒号(Signed-off-by:),和
- Change-Id 标识符行,后跟冒号(Change-Id:)。如果没有这个标识符,Gerrit将不会接受补丁。
一个好的提交应具有上述细节。
注解
您不需要为新提交提供 Change-Id 标识符;这是由与仓库关联的 Git hook 的 commit-msg
自动添加的。如果随后修改了提交内容并使用与之前相同的 Change-Id 重新提交, Gerrit 将会识别出该提交和之前的提交有关。
发送给 Gerrit 的所有更改和主题都要必须有一个规范的格式。除了上述提交的强制性内容外,提交信息还应包括:
- what 这个变更做了什么,
- why 你为什么选择这种方法,和
- how 你如何知道它有作用,例如,你做了什么测试。
提交必须在相互应用时能够 正确地编译,从而避免破坏耦合性。每个提交都应该处理一个可识别的 JIRA 问题,并且应该在逻辑上是自包含的。例如,一个提交可能修复空格问题,另一个提交可能重命名一个函数,而第三个提交可能更改一些代码的功能。
一份好的提交详细说明如下:
[FAB-XXXX] purpose of commit, no more than 55 characters
You can add more details here in several paragraphs, but please keep
each line less than 80 characters long.
Change-Id: IF7b6ac513b2eca5f2bab9728ebd8b7e504d3cebe1
Signed-off-by: Your Name <commit-sender@email.address>
在 Signed-off-by:
行的名字和你的邮箱必须与变更的作者信息相符。确保你个人的 .gitconfig
文件设置正确。
当一个支持其他变更的变更包含在集合中,但它不属于最终集合的一部分时,请让审核人员知道这一点。
用 CI 流程检验您的变更请求是可行的¶
为了确保代码的稳定性并限制可能的回归,我们使用了基于 Jenkins 的持续集成(CI)过程,该过程在多个平台上都能触发构建,它针对提交的每个更改请求运行测试。您有责任检查您的 CR 是否通过这些测试。如果 CR 没有通过测试,那么它就不会被合并,在他们通过 CI 测试之前,任何人都不会关注您的CR。
要检查 CI 流程的状态,只需查看 Gerrit 上的 CR,并按照上一步中推送的结果提供给您的 URL 执行即可。页面底部的 History 部分将显示一组由 “Hyperledger Jobbuilder” 执行的操作,这些操作对应于正在执行的 CI 进程。
完成后,如果成功,”Hyperledger Jobbuilder” 将添加到 CR 一个 +1 投票,否则将添加 -1 投票。
如果失败了,请查看从 CR 历史记录中的日志。如果您发现 CR 有问题,请修改提交并推送更新,这将再次自动启动 CI 进程。
如果您的 CR 没有任何问题,那么 CI 过程可能只是由于没有与您的更改关联起来而失败。在这种情况下,您可以向 CR 发送带有 “reverify” 的回复来重新启动 CI 进程。更多信息和选择请查看 CI 管理页面 。
审核变更¶
- 点击链接来审核输入或输出。
- 变更的详细信息及其当前状态加载如下:
- 状态(Status): 显示变更的当前状态。在下面的示例中,状态为:Needs verify。
- 回复(Reply): 审核结束后点击此按钮,添加审核信息和分数, -1、0 或 +1。
- 补丁集(Patch Sets): 如果存在一个补丁的多个版本,此按钮可以查看补丁的不同版本。默认情况下,将显示最新的版本。
- 下载(Download): 此按钮将打开一个新窗口,其中有多个选项可以下载或检出当前更改集。右边的按钮将命令复制到剪贴板。您可以轻松地将其粘贴到 git 中,来使用您想要的版本。
在提交信息的下面,显示了这个补丁修改的文件。
- 单击文件名即可审核该文件。选择要对比的文件。默认值通常是
Base
。 - 审核页面显示对文件所做的更改。在页面的顶部,显示了一些通用的导航选项。使用右上角的箭头在补丁集中导航,可以转到上一个或下一个文件,也可以返回到主更改屏幕。单击黄色的便利贴,将注释添加到整个文件中。
页面的焦点是比较窗口。所做的更改在右侧以绿色显示,而在左侧以基本版本显示。双击可高亮显示实际更改中的文本,以对代码的特定部分提供反馈。代码突出显示后,按 c 向该部分添加注释。
- 添加评论之后,评论会作为 草稿(Draft) 保存下来。
- 审核完所有文件并提交反馈之后,单击右上角的 绿色向上箭头 回到主页面。点击
Reply``按钮,写下一些最终的评论,并提交补丁集的评分。点击 ``Post
按钮,提交每个被审核文件的评论,以及你的最终评论和评分。Gerrit 向变更提交者和所有列出的审核者发送电子邮件。最后,它会记录审核信息以供将来参考。所有人的评论在点击Post
按钮之前都先被保存为草稿。
推荐的 Gerrit 练习¶
本文提供了一些最佳实践来帮助您更有效地使用 Gerrit。其目的是展示如何轻松提交内容。使用推荐的实践来减少故障排除时间,提高社区的参与度。
浏览 Git 树¶
访问 Gerrit 然后选择 Projects --> List --> SELECT-PROJECT --> Branches
。选择一个你感兴趣的分支,点击右手边的 gitweb
。现在,gitweb
会加载你选择的内容,并进行重定向。
提交信息¶
Gerrit 遵循 Git 提交消息格式。确保标题位于底部,并且彼此之间不包含空行。下面的示例显示了提交消息中期望的格式和内容:
简短的(不超过55个字符)一行描述。
详细总结所做的更改,包括为什么(动机)、更改了什么以及如何进行测试。还请注意任何更改都要保持代码和文档的一致性,文本的格式应是72字符/行。
Gerrit 服务器提供了一个预提交钩子(hook)来自动生成一个一次性的 Change-Id。
推荐阅读: 如何编写 Git 提交信息
持续跟踪变更¶
- 设置 Gerrit 自动发送邮件给你:
- 如果开发人员将您添加为审核者,或者您对特定的补丁集进行了评论,Gerrit 会将您添加到变更的电子邮件分发列表中。
- 在 Gerrit 的审核接口中打开更改是一种快速跟踪更改的方法。
- 在
Gerrit
中的 Gerrit projects 部分查看其中的项目,至少选择 New Changes, New Patch Sets, All Comments 和 Submitted Changes 这几种类型。
一直跟踪你正在做的项目;也可以查看反馈或者评论邮件列表来学习或者帮助其他人提升。
主题分支¶
主题分支是临时分支,您可以向它推送一组有依赖关系的提交:
从 REMOTE/master
树推送变更到 Gerrit ,并作为一个名为 TopicName 的主题来审核,其命令如下:
- ::
- $ git push REMOTE HEAD:refs/for/master/TopicName
这个主题将出现在审核 ``UI``和 ``Open Changes List``中。当主树的内容合并时,主题分支将从主树中消失。
为主题写简介¶
你可以决定是否让简介出现在历史记录里。
- 要让历史记录里出现简介,可以使用以下命令:
git commit --allow-empty
编辑提交消息,该消息将成为简介。使用的命令不会更改源码树中的任何文件。
- 要使历史记录里不出现简介,可以遵循以下步骤:
- 将空提交放在提交列表的末尾,它会被忽视不必重新 rebase。
现在添加你的提交
git commit ...
git commit ...
git commit ...
- 最后,将提交推到主题分支。下面的命令是一个例子:
git push REMOTE HEAD:refs/for/master/TopicName
如果您已经提交了,但是您想要设置一个简介,那么为简介创建一个空的提交,并移动提交,使其成为列表中的最后一个提交。以下边命令为例:
git rebase -i HEAD~#Commits
在移动提交之前,请注意取消注释。#Commits
是提交和新简介的总和。
找到可用的主题¶
$ ssh -p 29418 gerrit.hyperledger.org gerrit query \ status:open project:fabric branch:master \
| grep topic: | sort -u
- gerrit.hyperledger.org 是项目所在的当前 URL。
- status 指示主题的当前状态: open、merged、abandoned、draft、merge conflict。
- project 指项目的当前名称,本例中是 fabric。
- branch 在本分支中搜索主题。
- topic 特定主题的名称,留空可以包含它们全部。
- sort 将找到的主题排序,本例中是使用update (-u)。
下载或者检出一个变更¶
在审核界面右上角, Download 链接提供了一个命令列表和检出或下载差异或文件的超链接。
我们建议使用 git review 插件。 安装 git review 的步骤超出了本文档的范围。 参考 git review 文档 进行安装。
使用 Git 检出特定的更改,通常可以使用以下命令:
git review -d CHANGEID
如果你没有安装 Git-review,可参考以下命令:
git fetch REMOTE refs/changes/NN/CHANGEIDNN/VERSION \ && git checkout FETCH_HEAD
例如,第4版本的变更2464,NN 是指变更 ID 的前两位数字(24):
git fetch REMOTE refs/changes/24/2464/4 \ && git checkout FETCH_HEAD
使用草稿分支¶
您可以使用草稿分支在发布变更之前添加特定的审核人员。草稿分支会被推送到 refs/drafts/master/TopicName
。
下面的命令确保创建一个本地分支:
git checkout -b BRANCHNAME
下面的命令可以将你的变更推送到 TopicName 名下的草稿分支:
git push REMOTE HEAD:refs/drafts/master/TopicName
使用沙箱分支¶
你可以创建自己的分支来开发功能。这些分支可以推动到这个位置 refs/sandbox/USERNAME/BRANCHNAME
。
这些命令可以在 Gerrit 的服务器里面创建这个分支。
git checkout -b sandbox/USERNAME/BRANCHNAME
git push --set-upstream REMOTE HEAD:refs/heads/sandbox/USERNAME/BRANCHNAME
通常,创建内容的流程是:
- 开发代码,
- 将信息分散在各个小的提交里,
- 提交变更,
- 应用反馈,
- rebase。
下面这条命令强行推送,无需审核:
git push REMOTE sandbox/USERNAME/BRANCHNAME
你也可以强行推送要求审核:
git push REMOTE HEAD:ref/for/sandbox/USERNAME/BRANCHNAME
更新变更的版本¶
在审核过程中,可能会要求您更新变更。可以提交同一变更的多个版本。每个版本的变更都称为一个补丁集。
请保持使用分配的 Change-Id。例如,有一个提交列表,c0…c7,作为主题分支提交:
git log REMOTE/master..master
c0
...
c7
git push REMOTE HEAD:refs/for/master/SOMETOPIC
你得到审核者的反馈之后,有 c3 和 **c4**必须修复。如果修复时需要 rebasing,rebasing 会改变commit ID,查看 rebasing 获取更多信息。但是,你必须保证 Change-Id 相同并且重新推送变更:
git push REMOTE HEAD:refs/for/master/SOMETOPIC
这个新推送会为补丁重新创建一个版本,然后清除本地历史记录。不过,您仍然可以在 Gerrit 的 review UI
模块查看变更的历史记录。
允许在推送新版本时添加多个提交。
Rebasing¶
Rebasing 通常是将更改推入 Gerrit 之前的最后一步;这允许你添加必需的 Change-Id。必须保持*Change-Id* 不变。
- squash: 将两个或多个提交混合到一个提交中。
- reword: 改变提交信息。
- edit: 改变提交的内容。
- reorder: 允许您改变内部各个提交的顺序。
- rebase: 在主分支顶部入栈各个提交。
拉取时 Rebasing¶
在将 rebase 推给主分支之前,请确保历史记录具有连续的顺序。
如,你的 REMOTE/master
有从 a0 到 a4 的提交列表;然后,你的变更 c0…c7 在**a4** 上面,像这样:
git log --oneline REMOTE/master..master
a0
a1
a2
a3
a4
c0
c1
...
c7
如果 REMOTE/master
接受了 a5、a6、a7 三个新的提交。拉取时要加上 rebase,像这样:
git pull --rebase REMOTE master
这样就拉取了 a5-a7 并且重新把 c0-c7 应用到它们顶部:
$ git log --oneline REMOTE/master..master
a0
...
a7
c0
c1
...
c7
从 Git 获取更好的日志¶
使用这些命令来更改 Git 的配置,以便生成更好的日志:
git config log.abbrevCommit true
上面的命令将日志设置为提交的哈希的缩写。
git config log.abbrev 5
上面的命令将缩写长度设置为哈希的最后5个字符。
git config format.pretty oneline
上面的命令避免在 Author 行之前插入不必要的行。
要针对当前 Git 用户进行这些配置更改,必须将 path 选项 --global
添加到 config
中,像这样:
git config --global
编程指南¶
生成 gRPC 代码¶
如果你修改任何 .proto
文件,运行下面的命令来生成/更新各自的 .pb.go
文件。
cd $GOPATH/src/github.com/hyperledger/fabric
make protos
术语表¶
专业术语很重要,所以每一个 Hyperledger Fabric 项目的用户和开发人员,都要理解我们所说的每一个术语的含义,比如什么是链码。本文档将会按需引用这些术语,如果你愿意的话可以一次性读完整本文,这会非常有启发!
锚节点¶
被 gossip 用来确保在不同组织中的 Peer 节点能够知道彼此。
当一个包含锚节点更新的配置区块被提交的时候,Peer 节点会连接到锚节点并获得它所知道的所有 Peer 节点信息。一旦每个组织中至少有一个 Peer 节点已经连接到了一个或多个锚节点的话,锚节点就会知道在这个通道中的每个 Peer 节点。因为 gossip 通信是不变的,并且因为 Peer 节点总是会被告知他们所不知道的其他的存在的 Peer 节点,所以可以建立一个通道的成员视图。
比如,我们假设通道中有三个组织 A、 B 和 C,和组织 C 定义的一个的锚节点 peer0.orgC。当组织 A 中的 peer1.orgA 连接到 peer0.orgC 的时候,它会告诉 peer0.orgC 关于 peer0.orgA 的信息。然后,当 peer1.orgB 连接到 peer0.orgC 的时候,peer0.orgC 会告诉 peer1.orgB 关于 peer0.orgA 的信息。从此之后组织 A 和 B 就可以开始直接地交换成员信息而不需要 peer0.orgC 的帮助了。
由于组织间的通信是基于 gossip 的,所以在通道配置中至少要定义一个锚节点。为了高可用和冗余,非常建议每个组织应该提供他们自己的一组锚节点。
ACL¶
ACL(Access Control List),或称访问控制列表,将特定节点资源(例如系统链码的 API 或事件服务)的访问与策略(指定需要多少和哪些类型的组织或角色)相关联。ACL 是通道配置的一部分。 因此,它会保留在通道的配置区块中,并可使用标准配置更新机制进行更新。
ACL 被格式化为键值对列表,其中键标识了我们希望控制其访问权限的资源,值标识了允许访问它的通道策略(组)。 例如, lscc/GetDeploymentSpec: /Channel/Application/Readers
定义了只有满足 /Channel/Application/Readers
策略的标识才可以访问生命周期链码 GetDeploymentSpec
API(资源)。
configtx.yaml
文件中提供了一组默认 ACL,configtxgen 使用该文件来构建通道配置。可以在 configtx.yaml
的“Application”部分中设置默认值,也可以在“Profiles”部分中按每个配置文件来覆盖默认值。
区块¶
一个区块包含了一组有序的交易。他们通过加密与前一个区块相连,同时也和后续的区块相连。这条链的第一个区块被称为 创世区块。区块由排序系统创建,并由 Peer 节点进行验证。
链¶
账本的链是交易区块通过“哈希连接”的结构化的交易日志。Peer 节点从排序服务接受交易区块,基于背书策略和并发冲突来标注区块中的交易为有效或者无效状态,并且将区块追加到 Peer 节点文件系统的哈希链中。
并发控制版本检查(Concurrency Control Version Check,CCVC)¶
CCVC 是保持通道中各节点间状态同步的一种方法。节点并行的执行交易,在交易提交至账本之前,节点会检查交易在执行期间读到的数据是否被修改。如果读取的数据在执行和提交之间被改变,就会引发 CCVC 冲突,该交易就会在账本中被标记为无效,并且值不会更新到状态数据库中。
配置区块¶
包含为系统链(排序服务)或通道定义成员和策略的配置数据。对某个通道或整个网络的配置修改(比如,成员离开或加入)都将导致生成一个新的配置区块并追加到适当的链上。这个配置区块会包含创始区块和新增的内容。
共识¶
一个涵盖整个交易流程的术语,用来为区块中的交易生成一个大家认可的顺序并确认交易的正确性。
共识者集合¶
在 Raft 排序服务中,它们是积极参与到通道的共识机制的排序节点。如果其他排序节点在系统通道中,但是不在(应用)通道中,它们就不属于该通道的共识者集合。
联盟¶
联盟是区块链网络上非定序的组织集合。这些是创建和加入通道及拥有节点的组织。虽然区块链网络可以有多个联盟,但大多数区块链网络都只有一个联盟。在通道创建时,添加到通道的所有组织都必须是联盟的一部分。但是,未在联盟中定义的组织也可以添加到现有通道。
动态成员¶
Hyperledger Fabric 支持成员、节点、排序服务节点的添加或移除,而不影响整个网络的操作性。当业务关系调整或因各种原因需添加或移除实体时,动态成员至关重要。
背书¶
背书是指特定节点执行一个链码交易并返回一个提案响应给客户端应用的过程。提案响应包含链码执行后返回的消息,结果(读写集)和事件,同时也包含证明该节点执行链码的签名。链码应用具有相应的背书策略,其中指定了背书节点。
背书策略¶
定义了在一个通道上对于某个链码应用,必须执行交易的 Peer 节点和必要的响应(背书)。一个策略要包含背书一笔交易所需节点的最少数量、最小比例、或者全部节点都需要背书。背书策略可以基于应用程序和节点对抵御不良行为(有意无意)的期望水平来组织管理。提交的交易必须符合背书策略才会被交易为有效交易。安装和实例化交易时,也需要一个明确的背书策略。
跟随者¶
在一个基于领导者的共识协议中,比如 Raft,其他节点要从领导节点哪里复制日志项。在 Raft 中,跟随者还接收领导节点的“心跳”信息。当领导者在设定的时间内没有发送心跳信息的时候,跟随者节点会从新选举出一个领导节点。
创世区块¶
创世区块是初始化区块链网络或通道的配置区块,也是链上的第一个区块。
Gossip 协议¶
Gossip 数据传输协议有三项功能: 1)管理节点发现和通道成员; 2)在通道上的所有节点间广播账本数据; 3)在通道上的所有节点间同步账本数据。 更多内容请参考 Gossip 话题。
Hyperledger Fabric CA¶
Hyperledger Fabric CA(证书授权中心)是默认的认证授权管理组件,它向网络成员的组织及其用户颁发基于 PKI 的证书。CA 为每个成员颁发一个根证书(rootCert),为每个授权用户颁发一个注册证书(ECert)。
初始化¶
初始化链码程序的方法。
安装¶
将链码放到节点文件系统的过程。
实例化¶
在特定通道上启动和初始化链码应用的过程。实例化完成后,装有链码的节点可以接受链码调用。
调用¶
用于调用链码内的函数。客户端应用通过向节点发送交易提案来调用链码。节点会执行链码并向客户端应用返回一个背书提案。客户端应用会收集充足的提案响应来判断是否符合背书策略,该结构会被用于交易的排序、验证和提交阶段。客户端应用也可以不提交交易结果。比如,查询账本的调用,一般来说客户端应用是不会提交这种只读性交易的,除非基于审计目的,需要记录访问账本的日志。调用中包含了通道标识符,调用的链码函数,以及一个包含参数的数组。
领导者¶
在一个基于领导者的共识协议中,比如 Raft,领导者用于生成新的日志项,将它们复制到排序节点,并在需要提交的时候进行管理。它不是一个特殊 类型 的排序节点。它是排序节点在特定时期的一种角色,并且在一个周期内只有一个领导者点。
账本¶
账本由两个不同但相关的部分组成——“区块链”和“状态数据库”,也称为“世界状态”。与其他账本不同,区块链是 不可变 的,也就是说,一旦将一个区块添加到链中,它就无法更改。相反,“世界状态”是一个数据库,其中包含的是经过区块链验证和提交的用于添加、修改或删除的键值对当前值的交易。
我们可以认为网络中每个通道都有一个 逻辑 账本。实际上,通道中的每个节点都维护着自己的账本副本,该副本通过共识与所有其他节点的副本保持一致。术语 分布式账本技术(DLT) 通常与这种账本相关,这种账本在逻辑上是单一的,但在一组网络节点(节点和排序服务)上分布有许多相同的副本。
日志项¶
Raft 排序服务中主要的工作单元,日志项从领导节点分发到跟随节点。这样的事项的完账序列称为“日志”。日志保证了成员对于事项和其顺序的一致性要求。
成员服务提供者¶
成员服务提供者(Membership Service Provider,MSP)是指为客户端和节点加入 Hyperledger Fabric 网络提供证书的抽象组件。客户端用证书来认证他们的交易;节点用证书背书交易处理结果。该接口与系统的交易处理组件密切相关,旨在定义成员服务组件,以这种方式可选实现平滑接入而不用修改系统的交易处理组件核心。
成员服务¶
成员服务在许可区块链网络上用于认证、授权和身份管理。运行于节点和排序服务的成员服务代码均会参与认证和授权区块链操作。它是 MSP 基于 PKI 的实现。
排序服务¶
预先定义好的一组将交易排序放入区块的节点。排序服务独立于节点流程之外,并以先到先处理的方式为网络上所有通道做交易排序。排序服务是可插拔的,目前默认实现了 SOLO 、Kafka 和 Raft。排序服务与整个网络相绑定,包含与每个 成员 相关的加密材料。
组织¶
也称为“成员”,区块链服务提供者邀请组织加入区块链网络。组织通过将成员服务提供者( MSP )添加到网络的方式来加入网络。MSP 定义了网络中的其他成员如何验证一个有效的签名(例如交易上的签名)。MSP 中身份的特定访问权限由策略控制,并且加入网络的组织都同意该策略。组织可以像跨国公司一样大,也可以像个人一样小。 组织的交易终点是 Peer节点 。多个组织组成了一个 联盟 。虽然网络上的所有组织都是成员,但并非每个组织都会成为联盟的一部分。
策略¶
策略是包含数字身份属性的描述,例如:Org1.Peer OR Org2.Peer
。它们用来限制对区块链网络资源的访问权限。例如,它们会指定谁可以读取或写入通道数据,或者谁可以根据 ACL 使用特定的链码 API。策略可以在启动排序服务或者创建通道之前定义在 configtx.yaml
文件中。示例 configtx.yaml
文件中的默认策略适用于大多数网络。
私有数据¶
存储在每个授权节点的私有数据库中的机密数据,在逻辑上与通道账本数据分开。通过私有数据集合定义,对数据的访问仅限于通道上的一个或多个组织。未经授权的组织将在通道账本上使用私有数据的哈希作为交易的证据。此外,为了进一步保护隐私,通过 排序服务 的是私有数据的哈希值而不是私有数据本身,因此这使得私有数据对排序者保密。
私有数据集合¶
用于管理通道上的两个或多个组织希望与该通道上的其他组织保持私密的机密数据。集合的定义描述了通道上一组有权存储私有数据的组织的子集,意味着只有这些组织才能与私有数据进行交易。
提案¶
通道中针对特定节点的背书请求。每个提案要么是链码的实例化,要么是链码的调用(读写)请求。
查询¶
查询是一个只读账本当前状态不写入账本的链码调用。链码函数可以查询账本上特定的键名,也可以查询账本上的一组键名。由于查询不改变账本状态,因此客户端应用通常不会提交这类只读交易做排序、验证和提交。不过,特殊情况下,客户端应用还是会选择提交只读交易做排序、验证和提交。比如,客户需要账本链上保留可审计证据,就需要链上保留某一特定时间点的特定账本的状态。
法定人数¶
规定了确认一个提案中交易的顺序所需要的最小成员数量。对于每一个共识者集合来说,法定人数为 大多数 节点。在有五个节点的集群中,必须有三个才能达到法定人数。无论任何原因不能达到法定人数,该集群都不能进行读写操作并且不难提交新日志。
Raft¶
v1.4.1 更新,Raft 是一个基于 etcd library 实现了 Raft 协议 的崩溃容错排序服务(Crash Fault Tolerant,CFT)。Raft 使用 “领导者跟随者”模型,每个通道选举一个领导者并且向跟随者分发它的决定。Raft 排序服务比基于 Kafka 的排序服务更容易设置和管理,并且它的设计可以让组织向分布式排序服务共享节点。
软件开发包 (SDK)¶
Hyperledger Fabric 客户端软件开发包(SDK)为开发人员提供了一个结构化的库环境,用于编写和测试链码应用程序。SDK 完全可以通过标准接口实现配置和扩展。它的各种组件:签名加密算法、日志框架和状态存储,都可以轻松地被替换。SDK 提供 API 进行交易处理、成员服务、节点遍历以及事件处理。
目前,两个官方支持的 SDK 为 Node.js 和 Java,而另外三个:Python、Go 和 REST 尚非正式,但仍可以下载和测试。
智能合约¶
智能合约是由区块链网络外部的客户端应用程序调用的代码,用于管理世界状态中的键值对的访问和修改。在 Hyperledger Fabric 中,智能合约被称为链码。智能合约链码安装在节点上在一个或多个通道上实例化。
状态数据库¶
为了高效的读取和查询链码,当前的状态存储在状态数据库中。支持的数据库包括 levelDB 和 couchDB。
系统链¶
一个在系统层面定义网络的配置区块。系统链存在于排序服务中,与通道类似,具有包含以下信息的初始配置:MSP 信息、策略和配置详情。网络中的任何变化(例如新的组织加入或者新的排序节点加入)将导致新的配置区块被添加到系统链中。
系统链可以看作是和一个或一组通道的绑定。例如,金融机构的集合可以形成一个财团(表现为系统链),然后根据其相同或不同的业务计划创建通道。
交易¶
将调用或者实例化结果提交到排序、验证和提交程序。调用是从账本中读取或写入数据的请求。实例化是在通道中启动并初始化链码的请求。客户端应用从背书节点收集调用或实例化响应,并将结果和背书打包到交易中,即把结构提交到做排序,验证和提交程序。
世界状态¶
世界状态也称为“当前状态”,是 Hyperledger Fabric 账本的一个组件。世界状态表示交易日志中所包含的所有键的最新值。链码针对世界状态数据执行交易提案,因为通过世界状态可以直接访问对这些密钥的最新值,而不是通过遍历整个交易日志来计算它们。每当键的值发生变化时(例如,当汽车的所有权(“键”)从一个所有者转移到另一个所有者(“值”)或添加新键(创造汽车)时,世界状态就会改变。因此,世界状态对交易流程至关重要,因为必须先知道键值对的当前状态才能更改。处理过的区块中包含的每个有效事务,节点都会将最新值提交到世界状态中。
版本发布¶
Hyperledger Fabric 的版本发布记录在 Fabric github 页面.
还有问题?¶
我们正尝试维护一个面向大多数读者的一个综合性的文档。但是我们发现,经常会有一些问题我们不能覆盖到。关于 Hyperledger Fabric 的技术问题我们不会在这里回答,请使用 StackOverflow ,也可以把你的问题发送到邮件 列表 (hyperledger-fabric@lists.hyperledger.org),或者在 RocketChat 的 #fabric 或 #fabric-questions 频道中提问。
注解
请在您提问的时候,描述清楚您的运行环境,包括操作系统,使用的 Docker 版本等。
注解
如果您有其他该文档未谈及的疑问,或者在任何一个教程中遇到问题,请访问 还有问题? 页面了解如何获取额外帮助。