深度探索区块链：Hyperledger技术与应用 (区块链技术丛书)
Author: 张增骏
Date: 2017-12-31 16:00:00+00:00
Publisher: 北京华章图文信息有限公司

区块链技术丛书

深度探索区块链：Hyperledger技术与应用

张增骏　等著

ISBN：978-7-111-58932-7

本书纸版由机械工业出版社于2018年出版，电子版由华章分社（北京华章图文信息有限公司，北京奥维博世图书发行有限公司）全球范围内制作与发行。

版权所有，侵权必究

客服热线：+ 86-10-68995265

客服信箱：service@bbbvip.com

官方网址：www.hzmedia.com.cn

新浪微博 @华章数媒

微信公众号 华章电子书（微信号：hzebook）

目录
序一
序二
序三
前言
第一篇　准备篇
第1章　区块链概述
1.1　区块链的前世今生
1.2　区块链概念
1.3　区块链技术平台
1.4　区块链的商用之道
1.5　本章小结
第2章　超级账本初体验
2.1　基础环境安装
2.2　超级账本部署调用
2.3　节点的配置参数传递规则
2.4　本章小结
第二篇　核心篇
第3章　超级账本的系统架构
3.1　系统逻辑架构
3.2　网络节点架构
3.3　典型交易流程
3.4　消息协议结构
3.5　策略管理和访问控制
3.6　本章小结
第4章　基于Gossip的P2P数据分发
4.1　概述
4.2　超级账本中的Gossip协议
4.3　成员认证及身份管理
4.4　节点启动及成员管理
4.5　主节点选举过程
4.6　基于反熵的状态同步
4.7　数据传播过程
4.8　多通道的支持
4.9　消息的验证策略
4.10　消息的多路分用及分区
4.11　和Gossip相关的配置参数
4.12　本章小结
第5章　分布式账本存储
5.1　概述
5.2　读写集
5.3　账本编号
5.4　账本数据
5.5　区块索引
5.6　状态数据
5.7　历史数据
5.8　数据恢复
5.9　本章小结
第6章　集成共识机制的排序服务
6.1　概述
6.2　实现数据隔离的多通道
6.3　可插拔的排序服务
6.4　本章小结
第7章　实现数据隔离的多链及多通道
7.1　数据存储对多链的支持
7.2　链码对多链的支持
7.3　多通道对多链的支持
7.4　命令行和SDK对多链的支持
7.5　关于系统链
7.6　本章小结
第8章　基于数字证书的成员管理服务
8.1　实现成员管理的MSP
8.2　颁发数字证书的Fabric　CA
8.3　本章小结
第9章　支持多种语言的智能合约
9.1　概述
9.2　链码的生命周期管理
9.3　内置的系统链码
9.4　链码的相互调用
9.5　背书节点和链码的有限状态机
9.6　本章小结
第三篇　应用篇
第10章　超级账本的应用开发模型
10.1　应用开发模型
10.2　应用程序开发的SDK
10.3　链码的开发和调试
10.4　本章小结
第11章　从零开始部署超级账本网络
11.1　准备超级账本运行环境
11.2　快速构建超级账本网络
11.3　逐步建立超级账本网络
11.4　本章小结
第12章　超级账本的应用开发实例
12.1　票据背书场景介绍
12.2　票据背书需求分析
12.3　票据背书架构设计
12.4　票据背书实现
12.5　票据背书快速部署
12.6　票据背书展示
12.7　本章小结
附录A　术语表
附录B　超级账本的实用工具
参考文献

序一

作为Linux基金会托管下增长最快的项目，超级账本（Hyperledger）在过去的一年成长十分迅速。这主要归功于Linux基金会开放、公平的治理模式，就是让各种规模的组织、开发者社区和技术专家可以达成最高水准的合作，以公开、公平和结构化的方式进行迭代。我们珍视每一个贡献，也鼓励更多的组织和开发者加入其中。

2017年7月，超级账本（Hyperledger）迎来了Fabric 1.0版本的正式发布，这是首个可用于生产环境部署的商业级应用，它历经了上百个概念验证。截至发布时间，共有27个组织、159位开发者参与并作出贡献。而在之后的每一天，这个数字一直都在上升。

我也很欣喜地看到，自Fabric 1.0版本发布以来，越来越多的企业、组织或个人对此产生了浓厚的兴趣，他们通过各种渠道，想要一探Fabric的究竟。因此，本书的面世可谓恰逢其时。

书中深入浅出地讲解了Fabric的内部运行原理，并且详细解答了Fabric 0.6和Fabric 1.0版本的区别。更难得的是，关于如何搭建Fabric系统，以及如何基于Fabric做区块链应用的开发，书中都有清楚、详尽的步骤演示，易于理解，很适合作为Fabric初学者教材，帮助他们快速上手。

本书集合了多方共同的心血，由Linux基金会会员、智链ChainNova的一线技术团队主笔撰写，他们不仅是超级账本中国社区的主要贡献者，还长期奋战在市场前线，对商业应用环境有相当的了解，相信从书中内容的翔实程度可见一斑。

——Brian Behlendorf，超级账本执行董事

序二

区块链技术是当今最具影响力的创新技术之一，得到产业界、学术界和投资领域的广泛关注。

Linux基金会超级账本（Hyperledger）开源项目创立于2015年12月，目前已经发展到160余个成员单位。超级账本项目的愿景是借助项目成员和开源社区的通力协作，共同制定并建立一个开放、跨产业、跨国界的区块链技术开源标准。它通过创建通用的分布式账本技术，协助组织扩展、建立行业专属应用程序以及平台和硬件系统来支持成员各自的交易业务。

董宁先生这本书深入浅出地描绘了HyperLedger技术与应用，为深度探索区块链技术和应用案例提供了很好的参考，是一本不可多得的技术参考书。

本书首先回顾了区块链发展的历史，展望了区块链的商用前景，对超级账本的基础环境、系统架构、交易流程、消息协议、策略管理和访问控制等进行了详细介绍。然后，进一步讨论了Gossip协议、分布式账本存储、共识机制、多链和多通道、基于数字证书的成员管理、智能合约等关键技术。最后，介绍了超级账本的应用开发模型、开发案例和应用部署方面的主题。这是我迄今为止所见关于超级账本技术和应用非常有参考意义的一本技术书籍，值得向广大区块链的研究者与开发同行推荐。

董宁先生长期以来致力于区块链技术的研究与推广，也是Hyperledger中国社区最有活力的推动者之一。相信本书的出版会对社区的发展和区块链技术的应用起到积极的推动作用。

陈钟

北京大学信息科学技术学院教授

北京大学金融信息化研究中心主任

2017年12月于燕园

序三

从2008年中本聪在论文中提到区块链开始到区块链结合各类产业应用场景落地，区块链以不可思议的速度发展起来，经常会有人在问区块链究竟是什么。可以说，区块链本质上是一种创建信任的技术机制，通过区块链可以跨机构执行可信的交易。

当下和未来，区块链的用武之地将远远超过加密货币，因此为了适应大多数商业应用的需求，设计与开发适合商用的区块链平台迫在眉睫，“超级账本”（Hyperledger）应运而生。作为一个由IBM等世界著名大企业领衔的商业化联盟链项目，Hyperledger是目前代码数量最大、社区参与度最高的区块链开源项目。更重要的是，该项目也标志着区块链从单纯的开源技术发展到了被主流机构和市场认可的阶段。这对于区块链相关产业的发展意义深远。

区块链数学上的可信，不等于工程实现上的可信。为此，中国信通院联合央行数字货币研究所以及30多家企业，共同讨论制定了可信区块链标准。2017年9月对包括智链ChainNova在内的9家企业的区块链进行了第一轮评测，并且于2017年10月正式在国际标准组织立项。“因为透明，所以可信”，可信区块链标准已经起到了引领和推动我国与全球区块链底层基础设施健康有序发展的目的。在这个过程中，通过与本书作者之一、前IBM大中华区IT经济学负责人和IBM区块链社区发起人、智链CEO董宁的接触，能感觉到他对企业级区块链和Hyperledger的未来充满信心。对于金融科技和互联网业内人士来说，不懂区块链可能冒着被潮流淹没的风险；对于有志于从事区块链技术的人士来说，不学习Hyperledger可能错失与极有可能占据市场领导地位的金融科技结缘的机会。

坦诚地说，本书并非市场上第一本区块链的书。事实上，我了解到在Hyperledger 0.6版本盛行之时，本书的作者就曾经完成了本书的初版。但是由于后来Hyperledger推出了1.0版本，本着对读者极其负责任的态度，他们又全面重写了本书，使读者能够完全跟上Hyperledger发展的最新状态。本书的目的也不是蜻蜓点水地介绍一些Hyperledger入门知识，而是通过阅读本书能让读者达到一定的水平，甚至可以加入区块链产业应用中来，为区块链的发展和实践落地添砖加瓦。同时也希望通过作者的努力，能够给有志于在Hyperledger平台进行开发，并有所进展的程序开发人员带来帮助。

何宝宏

中国信息通信研究院云计算与大数据研究所所长

前言

为什么要写这本书

区块链是在全球范围内受到极高关注的技术。简而言之，区块链就是防篡改并且由大家共同维护的账本，其中包含不断增长的数据记录列表。根据现在的发展趋势，区块链将在商用领域得到广泛应用。

超级账本（Hyperledger）是Linux基金会旗下的区块链开发平台项目，致力于发展跨行业的商用区块链平台技术。超级账本项目自创立伊始便吸引了众多行业的领头羊，包括金融、银行、互联网、运输、制造等行业。目前，超级账本项目在全球拥有超过100个成员，包括IBM、Cisco、Intel、J.P.Morgan、荷兰银行、SWIFT、R3等。基于区块链技术、智能合约及其他相关技术，超级账本项目致力于建立新一代的分布式账本交易应用平台，从而在简化与商业流程相关的事务的同时，建立起商业信任、透明、审查等能力。Hyperledger Fabric子项目是以IBM早期捐献出的Open Blockchain为主体搭建而成的，是一个带有可插入各种功能模块架构的区块链实施方案，其目标是建立一个更加标准化的开源区块链开发平台，类似OpenStack之于云计算。开源地址是：https://github.com/hyperledger/fabric 。Fabric主要框架的核心开发语言是Go语言，它非常适合联盟链，具有更高的商业应用前景。

从2015年开始，由于在IBM中国实验室工作，我开始接触区块链技术和IBM的Open Blockchain项目（即Hyperledger Fabric的前身），并开始为中国的金融用户推荐它，帮助这些用户借助区块链的技术价值来实现科技和业务的创新。到了2016年下半年，Hyperledger Fabric开发平台阶段性地稳定在0.6版本，无论是IBM还是云图智链（后来被智链ChainNova并购），都在很多行业应用场景中开始实践Fabric 0.6版本。那时在国内，绝大多数的金融企业都在尝试通过Hyperledger Fabric 0.6平台来开发属于自己的区块链应用，我在那个时候有机会参与了不少相关的区块链项目，涉及领域包括数字积分、资产托管和交易、保险、高价值商品溯源等。也正是从那时起，萌生了编写一本书来解释Hyperledger原理，介绍各项开发组件，并通过真实案例还原区块链开发全过程，让更多的人觉得区块链或者Hyperledger离自己并不遥远。于是，当时我们几个作者从社区、不同的开发项目，以及各个开发团队中开始收集和整理资料，完成了基于0.6版内容的大部分写作工作。

但恰逢此时，Hyperledger的第一个商用版本1.0准备推出，我们也第一时间从Linux基金会得到了这个消息。从当时1.0版本的计划来看，推出它的目的主要有两个方面：一是Hyperledger希望以这个版本为基调，作为企业级区块链平台；二是为了解决0.6版本中出现的一些问题，1.0版本进行了很大的改变和调整，可以说这个变化是翻天覆地的，以至于我们半开玩笑地说从0.6版本到1.0版本甚至没有可直接升级的路径。当时我还在IBM工作，第一时间找到了云图智链的张增骏老师等几位作者和出版社的高婧雅编辑，和他们商量是否需要调整写作方向和内容，因为在此之前实际上张增骏老师已经在上一个版本的写作中付出了很多的时间和精力。几乎是在十分钟之内我们就一致决定要基于Hyperledger Fabric 1.0版本重新组织材料，重新编写这本书，因为我们希望自己投入的时间和付出的精力为社区、众多开发者和广大对区块链技术感兴趣的读者带来一本真正实用的书，而不是仅仅为了把我们几个人的名字留在封面上。数月后的今天，我们把初审过的稿件提交给出版社。这个过程中由于Hyperledger开源社区和代码版本不断迭代内容也不断调整，经历了更多我们之前没涉及的新行业和应用场景，甚至经历了我个人工作角色的变化，我们几个作者最终坚持完成了本书的写作。当然必须承认，由于能力和经验不足，本书还有很多提升空间，内容本身也难免出现表达不准确的地方。本书希望抛砖引玉，欢迎读者多提宝贵意见，指出本书存在的技术错误，争取在下一版本中能纠正错误，不断完善，进一步提升质量。同时，我们还会一直基于Hyperledger这个平台不断地进行产业实践，后面还会以本书为起点策划系列丛书，把我们在工作学习过程中得到的启发和经验分享给读者。

本书作者大多来自智链ChainNova，均具有多年IT工作、实践经验。智链ChainNova与超级账本社区一直有非常紧密的合作。2017年智链ChainNova研究院联手Linux Foundation和IBM，共同主办了HyperLedger Fabric商用正式版本1.0发布后Hyperledger开源社区第一次线下会议（北京Meetup），Hyperledger全球副总裁、亚太区负责人Julian Gordan和多家国内知名金融企业、科技企业高层出席。在2018年，智链ChainNova正在计划携手Linux Foundation、北京大学和IBM共同主办超级账本黑客马拉松大赛（HyperLedger Hackathon）。同时为了进一步活跃Hyperledger社区，我们还即将承办多项开源社区活动，欢迎读者积极参与。相信读者将在学习区块链技术和Hyperledger的过程中获益匪浅。

本书特色

笔者是Hyperledger社区成员，参与超级账本社区的日常工作，了解超级账本发展和技术细节的第一手资料。本书以Fabric商用正式版本1.0作为底层平台，其中也凝结了我们在Hyperledger开发理论和实际操作方面的经验。

本书深入讨论Hyperledger的核心技术，帮读者分析原理、关键实现与使用，是为数不多深入探讨和研究区块链的书籍之一。

我们的目标是把本书作为高校、科研院所、职业培训、企业技术学习的教材，向社会普及Hyperledger，培养更多的Hyperledger开发人才。

读者对象

·区块链从业者

·区块链应用开发人员

·其他区块链技术爱好者（金融/Fintec从业者、产品经理、企业管理者等）

·计算机及相关专业师生

如何阅读本书

本书共有12章内容，大体可以分为三篇内容。

准备篇（第1～2章），介绍区块链的基本概念，感受区块链的魅力。

第1章　本章是区块链技术与生态的概览，涉及区块链的基本概念、演进、主流平台，并着重分析企业级区块链平台的应用场景，有助于读者对区块链和Hyperledger Fabric 1.0（以下简称Fabric）的设计理念有整体性的了解。

第2章　本章介绍Fabric的安装、部署与调试。抛开复杂的底层技术细节，简化复杂的部署过程，快速体验Fabric的强大功能，以便有直观的感受。

核心篇（第3～9章），从系统架构开始讲解内部实现机制。

第3章　本章基于Fabric 1.0讲解区块链的架构，这是后续章节的基础，高屋建瓴地看待各个部件之间的关系和运行逻辑。本章涉及系统逻辑架构、网络节点架构、典型的交易流程、消息协议结构、策略管理和访问控制等内容，后面章节会从技术角度逐一“拆解”。

第4章　本章介绍基于Gossip的P2P数据分发机制，包括节点启动与成员管理、主节点选举与基于反熵的状态同步、身份认证与管理、多路分用与分区处理过程、消息的多种验证策略等。

第5章　本章介绍最为基础的分布式账本技术，它涵盖账本数据、索引数据、状态数据、历史数据等的实现技术。

第6章　本章介绍如何在排序服务上实现多通道的数据隔离，包括创建通道、节点加入通道等。排序服务采用插件化设计，可以根据业务场景的需求采用不同的共识算法。本章后面的内容详细介绍了排序服务的接口，以及实现了排序服务接口的Solo和Kafka模式。

第7章　本章介绍Fabric 1.0支持的多链及其内部的实现，多个链同时运行是一个系统工程，本章从数据存储、链码、命令行工具和SDK实现等多个方面分析如何支持多链。

第8章　本章介绍成员管理机制。它分为两个部分，第一部分详细介绍了MSP机制，包括MSP成员的验证、目录结构和配置最佳实践等；第二部分介绍可选的Fabric CA，包括服务端的安装部署和客户端的使用，还介绍了服务端提供的RESTful接口。

第9章　本章介绍Fabric 1.0上智能合约的实现。包括的内容有链码的生命周期管理、内置的系统链码、链码的相互调用、背书节点和链码的有限状态机等。

应用篇（第10～12章），从安装部署和应用开发的角度，通过一个票据背书的案例讲解如何基于Hyperledger Fabric 1.0开发区块链应用。

第10章　本章介绍Fabric 1.0的应用开发模型。从应用开发的角度看，开发者需要关注两部分：一部分是基于不同语言的SDK开发和区块链网络交互的应用程序；另一部分是实现超级账本的智能合约。本章详细介绍HFC SDK各个模块及其主要功能，链码的主要接口及其功能。

第11章　本章介绍多种Fabric 1.0的部署方式，包括分别基于Vagrant、Virtualbox、Docker的运行环境，以及BYFN脚本的使用。详细说明如何手动构建Fabric 1.0网络等。

第12章　本章通过一个票据背书示例，讲解如何实际开发一个基于Fabric 1.0的区块链应用。通过本章的实践，读者能够掌握区块链应用开发的方方面面，然后就可以动手开发具体的项目了。

读者反馈与勘误

欢迎读者朋友反馈，请让我们知道你对本书的看法——你喜欢哪些地方，不喜欢哪些地方。读者反馈对于我们很重要，因为这将帮助我们继续写作使你获益的书籍。反馈意见请发送E-mail至jessie@chainnova.com，并在邮件主题中指明书名，我们将尽力解决问题。如果你有专长领域，并对写书或为书做出其他贡献感兴趣，请访问www.chainnova.com 参见作者指南。

特别致谢

首先感谢本书的其他作者——张增骏老师、朱轩彤老师和陈剑雄老师。他们在工作之余，挤出宝贵时间为本书贡献了他们对区块链技术和Hyperledger的理解和洞察。特别感谢张增骏老师在工作本身比较繁忙的前提下，为本书花费了很多精力，他不仅在内容上积极供稿，还在审定、修改和校正方面下了很多工夫。朱轩彤老师博闻强识，本身具有很强的行业背景，对科技产业的发展又格外关注，这些在本书第1章中得到了充分体现。智链首席科学家陈剑雄也对本书的内容给出了很多宝贵的意见和建议，同时对本书合作的达成给予了支持。

万分感谢超级账本执行董事Brian Behlendorf先生，北京大学陈钟教授和中国信息通信研究院云计算与大数据所何宝宏所长在百忙之中拨冗为本书做序，让我感觉特别荣幸。他们在各自领域都是最顶尖的专家，同时对区块链技术都有深刻且独到的见解。还有苏州同济金融科技研究院马小峰院长、中国电子学会区块链专委会孙贻滋秘书长和超级账本中国技术工作组杨保华主席为本书写来热情洋溢的推荐，令人备感温暖。

在成书的过程中，和我一起工作和合作的很多专家对本书都给予了不同程度的支持和帮助，像Linux基金会超级账本亚太区副总裁Julian Gordon和中国地区顾问龙文选先生，北京大学（天津滨海）新一代信息技术研究院马修军副院长，中国信息通信研究院云计算与大数据所魏凯主任和卿苏德博士，IBM的各位领导和专家，以及其他各个单位的领导和大咖，在此抱歉不能一一尽述。

非常感谢机械工业出版社华章公司的编辑高婧雅，她的敬业精神和编辑效率令我由衷敬佩，她的反馈、建议、鼓励和帮助引导我们克服诸多困难完成全部书稿。同时，本书的推广得到了CSDN及其副总裁孟岩先生、InfoQ及其总编辑郭蕾先生这些好朋友的大力支持。

最后，因为工作和写作，牺牲了很多本该陪伴家人的时间。我要特别感谢我的家人长期以来对我的默默支持和理解。

谨以本书献给我最亲爱的家人，多年以来帮助、支持我的师友，以及众多热爱区块链技术的朋友！

董宁

2017年12月

第一篇　准备篇

第1章　区块链概述

第2章　超级账本初体验

第1章　区块链概述

1.1　区块链的前世今生

区块链的发展历史比较短暂，最初仅仅作为支持数字货币比特币交易的技术。目前，区块链技术已经脱离比特币，在金融、贸易、征信、物联网、共享经济等诸多领域得到初步应用。由于区块链技术可以防止数据篡改，所以不仅可以用安全而透明的方式追踪比特币的活动，还能在区块链网络中追踪其他类别的数据，因此可以帮助私人公司或政府部门建立更值得信赖的网络。用户可在这个网络中分享信息和价值，未来它还将得到更广泛的应用。

1.1.1　区块链的历史起源——比特币

比特币起源于2008年全球金融危机期间中本聪（Satoshi Nakamoto）撰写的论文《Bitcoin：A peer-to-peer electroniccash system》（《比特币：一种点对点的电子现金系统》） [1] 。在这篇著名的论文中，重点讨论了比特币系统，区块链被描述为用于记录比特币交易的账目历史。事实上，中本聪关于比特币的白皮书虽然整体思想是开创性的，但其中使用的技术工具，如P2P（Peer-to-Peer）、分布式存储、非对称性加密等早已存在，他提出的是一个集成性的、系统性的、可供实践的解决方案。

实际上在这篇论文中并没有明确提出区块链的定义和概念，甚至还没有“区块链”（Blockchain）这个词，只有“区块”（Block）和“链”（Chain）。但在论文中涉及了几个对区块链技术影响深远的观点：

·点对点、去中心化的可靠交易；

·反欺诈；

·基于密码学原理的电子交易凭证管理；

·分布式的时间戳服务器；

·足够的安全能力支持系统。

在2009年他公开了最初的实现代码，第一个比特币于2009年1月3日18：15：05生成，但真正流行起来是2010年后的事情。设计和实现一种数字货币绝非易事，但是由于比特币设计精妙，诞生还不到十年，就在世界各地迅猛发展，从小众成功走向主流。

在日本和韩国比特币的交易特别火爆，比特币价格在震荡中不断刷新纪录。2009年10月最早有纪录的比特币价格仅为0.00076美元。不到8年时间，2017年上半年比特币这个全球最热门的加密货币的价格创历史新高，首次超过2000美元。收益远远超过黄金、地产，甚至有人预测其价格还将大幅攀升。此外，2017年5月勒索病毒WannaCry席卷全球，全球150个国家及地区的数十万台计算机遭到攻击，事件并没有给比特币市场造成直接的损失，反倒让比特币也跟着火了一把。

据报道，多国政府开始给予比特币合法身份。2017年4月，日本正式生效新规，比特币成为一种合法的支付方式；2017年7月，澳大利亚政府承认比特币为货币，并废除比特币的商品与服务税。

1.1.2　欢迎来到区块链的世界

在比特币系统成功运行多年后，2014年前后，部分金融机构开始意识到，作为比特币运行的底层支撑技术——区块链，实际上是一种极其巧妙的分布式共享账本技术，对金融乃至各行各业带来的潜在影响甚至可能不亚于复式记账法的发明。对区块链的初步认识来自2014年10月大英图书馆的一次技术讨论会。在这次会议中，人们对比特币的现状和未来以及区块链在金融等领域的应用前景进行了深入的探讨。

自此，区块链技术开始在全球崭露头角。可以把2015年称为世界区块链元年，因为在这一年经济领域关注到了它。划时代的标志是《华尔街日报》刊文称区块链是最近500年以来在金融领域最重要的突破，而《经济学人》杂志在封面《信任的机器》一文中介绍区块链为创造信任的机器。文章指出，区块链并非仅仅是一项加密技术或者数字货币，在信息不对称、不确定的环境下，它还可以建立满足经济活动赖以发生、发展的“信任”生态体系。作为比特币底层技术的“链”，其价值远大于比特币本身。区块链可以让人们在没有中央权威机构监督的情况下，对彼此协作建立起信心。区块链是一种共享账本技术，实现了在分布式商业网络里多方参与的双边交易中的去中介化。简单来说，它是一台创造信任的机器。

进入2016年，业界开始大规模认识到区块链技术的重要价值，并通过智能合约技术将其用于数字货币以外的分布式应用领域。世界经济论坛甚至预测，到2025年，世界GDP的10%都将存储在区块链上或者应用区块链技术。

政府部门也行动起来关注区块链。2016年1月，英国首席科学家建议英国政府把区块链技术列为英国国家战略，随后包括中国人民银行在内的多国政府和央行对区块链进行了研究。2017年，欧洲议会发布了一份新的报告——《区块链如何改变我们的生活》，总结了区块链技术的能力和挑战以及对社会价值可能带来的影响，其中特别指出智能合约的重要作用。

虽然直到今天，我们还不知道中本聪究竟是谁，但毫无疑问他开启了一个新的时代。许多商业组织和行业机构投入了大量资源来研究区块链。全球领先的信息技术研究和顾问公司Gartner公布了2016年新兴科技技术成熟度曲线（Hype Cycle for Emerging Technologies，2016） [2] ，如图1-1所示。2016年区块链正处于期望膨胀期，距离成熟期需要5～10年。在未来，全球区块链技术仍然会保持比较高的发展趋势。

图1-1　2016年新兴科技技术成熟度曲线

1.1.3　区块链演进趋势

本质上，因为区块链的链与链之间具有隐私、安全、共识、自治、价值共享的特性，所以在技术层面上解决了互联网上的价值传递问题。同时，区块链又具有底层开源和改变业务规则、创新业务多方共识等逻辑，因此区块链是未来整个IT架构和互联网转型的重要支撑。

Melanie Swan所著的《区块链：新经济蓝图及导读》 [3] 一书在业界引起了巨大的反响。她在书中提出了对区块链版本划分的方法，即按照区块链已经完成的以及将要完成的功能划分成区块链1.0、2.0和3.0三个阶段。这种版本划分方式基本上反映了区块链技术成熟发展的大脉络，目前也得到了业界广泛的认可。

1）我们可以把比特币理解为区块链技术的一个应用场景，也就是区块链1.0阶段。但是如果仅有比特币，区块链也只是一种数字货币，并不能达到今天的火爆程度，可以说比特币是当今区块链的“杀手级”应用，但是区块链可以做的事情远远超过比特币，很有可能产生其他“杀手级”应用。

2）区块链2.0的重要标志就是被金融领域所接受并得到广泛应用形成的金融互联网，让价值交换变得便捷、直接，节省时间、节省成本。目前区块链2.0在实际场景中的应用，有两个重要因素：资产数字化（上链过程）与智能合约。

区块链2.0更关注智能合约（Smart Contract）所体现的业务价值。在区块链的背景下，智能合约当作是一种运行在区块链之上的通用计算模式，这样智能合约的内涵就不一定必须要和传统的合同概念相关联，反而可以是任何的计算机程序。智能合约实际上是通过高级编程语言把现实世界的业务逻辑在区块链上加以实现。智能合约通过在区块链上增加应用功能拓展了其适用范围和生存空间，如此就可以通过区块链来描述众多实现当中的业务场景。

当前，技术和产业处于区块链2.0阶段。在摩根士丹利公司的一份报告中提到了他们对区块链技术在金融行业被采用的路线图展望，对区块链2.0，其展望大致持续到2025年：

·2014年—2016年是评估阶段。银行和其他金融基础设施中介机构对许可制的共享账本技术的效率、机会等进行评估；

·2016年—2018年对区块链进行概念原型验证测试。主要的测试目标是验证技术的可行性，将区块链技术和传统方式在性能、成本、速度、规模等方面进行对比；

·2017年—2020年预计基于区块链的共享架构开始出现；

·2021年—2025年在区块链技术证明有效的基础上，会有更多的金融资产转向区块链技术。

3）区块链3.0要把区块链的应用范围拓展到各行各业，支持广义的资产交互和登记，进入万物互联，设备民主的“区块链+”时代。

互联网使得全球之间的互动越来越紧密，伴随而来的是巨大的信任鸿沟，未来将进入到需要真正的强信任背书的大数据时代。通过使用区块链技术，任何人都没有能力也没有必要去质疑数据的质量和真实性。区块链技术具有全新的理念和逻辑结构，并且它每天还在发展变化过程中，因此，随着区块链能够为信任提供价值的场景改变，它自身也将进入不同的阶段。或许未来的某一天，区块链可能还将迈进更新的阶段。

1.2　区块链概念

区块链（Blockchain）技术自身仍然在飞速发展中，目前还缺乏统一的规范和标准。Wikipedia给出的定义为：

A blockchain，originally block chain，is a distributed database that maintains acontinuously-growing list of data records hardened against tampering andrevision.It consists of data structure blocks—which hold exclusively data ininitial blockchain implementations，and both data and programs in some of themore recent implementations—with each block holding batches of individualtransactions and the results of any blockchain executables.Each block containsa timestamp and information linking it to a previous block.

简而言之，区块链技术让参与的系统中任意多个节点，通过密码学算法把一段时间系统内的全部信息交流数据计算和记录到一个数据块（Block）中，并且生成该数据块的指纹用于链接（Chain）下个数据块和校验，系统中所有的参与节点共同认定记录是否为真。

1.2.1　区块链本质

区块链，实质是由多方参与共同维护的一个持续增长的分布式数据库，也称为分布式共享账本（Distributed Shared Ledger），其核心在于通过分布式网络、时序不可篡改的密码学账本及分布式共识机制建立彼此之间的信任关系，利用由自动化脚本组成的智能合约来编程和操作数据，最终实现由信息互联向价值互联的进化，如图1-2所示。

图1-2　从传统集中记账方式（左）到分布式总账（右）

区块链是一种与传统集中记账方式不同的记录技术。参与到区块链系统上的节点，可能不属于同一组织、彼此无须信任；区块链数据由所有节点共同维护，每个参与维护的节点都能获得一份完整记录的拷贝。与传统的记账技术相比，其特点包括：维护一条不断增长的链，只可能添加记录，而发生过的记录不可篡改；无须集中控制而能达成共识，实现上尽量采用分布式；通过密码学的机制来确保交易无法抵赖和破坏，并尽量保护用户信息和记录的隐私性。

1.2.2　区块链工作原理

所谓区块链，正是由多个区块组成的链状数据结构及存储方式。每个区块分为区块头和区块体，区块头主要用来实现区块链接的前一区块哈希值（Hash Value），而区块体主要包括交易账本，如图1-3所示。

图1-3　区块链的账本

以交易场景为例，区块链的工作原理如下：

1）客户端将发起一笔交易，经数字签名后广播给网络上的其他节点并等待确认；

2）网络中的节点对收到的数据记录信息进行校验，通过校验后，数据记录被记录到一个区块中；

3）全网所有接收节点对区块执行共识算法，区块通过共识算法过程后正式纳入区块链中存储，全网节点均表示接受该区块。表示接受的方法，是将该区块的随机哈希值视为最新的区块哈希值，新区块将提供永久和透明的交易记录并以该区块链为基础进行延长，实现资金转移。

1.2.3　区块链技术特点

具体来说，区块链技术作为创造信任的机器，主要有以下特点。

·分布式结构。区块链构建在分布式网络基础之上，账本并不是集中存放在某个服务器或数据中心，也不是由第三方权威机构来负责记录和管理，而是分散在网络中的每一个节点上，每一个节点都有一个该账本的副本，所有副本同步更新。

·信任机制。区块链技术通过数学原理和程序算法，使系统运作规则公开透明，实现交易双方在不需要借助第三方权威机构信用背书下通过达成共识，建立信任关系。

·公开透明。区块链对其上的节点可以做到开放、透明。任何人都可以加入区块链，也能查询区块链上的区块记录；同时所有用户看到的是同一个账本，能看到这个账本所发生和记录的每一笔交易。

·时序不可篡改。区块链采用带有时间戳的链式区块结构存储数据，具有极强的可追溯性和可验证性；同时由密码学算法和共识机制保证了区块链的不可篡改性。

1.2.4　区块链层次模型

区块链技术的模型包括由自下而上的数据层、网络层、共识层、激励层、合约层和应用层，共有6层。

数据层、网络层、共识层是区块链的必要元素。

1）数据层：最下层是“数据层”，它封装了底层数据区块的链式结构，以及相关的非对称公私钥数据加密技术和时间戳等技术，这是整个区块链技术中最底层的数据结构。

2）网络层：中间是网络层，包括P2P组网机制、数据传播机制和数据验证机制等。

3）共识层：第三层是共识层，封装了网络节点的各类共识机制算法。

而激励层、合约层和应用层不是区块链的必要元素，一些区块链应用并不完全包含上面3层结构。

第四层是激励层，它将经济因素集成到区块链技术体系中来，包括经济激励的发行机制和分配机制等，主要出现在公有链当中。

第五层是合约层，它封装各类脚本、算法和智能合约。

第六层是应用层，它封装了区块链的各种应用场景和案例，未来的可编程金融和可编程社会也将搭建在应用层中。

1.2.5　区块链共识算法

区块链并不是某种特定技术，而是一种类似于NoSQL（非关系型数据库）这样的技术解决方案的统称。共识机制算法是区块链的核心技术，因为这决定了到底由谁来记账，而记账方式将会影响整个系统的安全性和可靠性。目前已经出现了十余种共识机制算法，其中较为知名的有PoW（Proof of Work，工作量证明），PoS（Proof of Stake，权益证明），DPoS（Delegate Proof of Stake，股份授权证明）机制以及拜占庭将军容错共识等。当然，没有一种共识机制是完美无缺的，同时这也意味着没有一种共识机制是适合所有应用场景的。

1）PoW（工作量证明）：就是挖矿，主要应用包括比特币和以太坊前三个阶段等。依赖机器进行数学运算来获取记账权，即通过与或运算，计算出一个满足规则的随机数，获得本次记账权，发出本轮需要记录的数据，全网其他节点验证后一起存储。优点是完全去中心化，节点自由进出；缺点是资源消耗相比其他共识机制高、可监管性弱，同时每次达成共识需要全网共同参与运算，性能效率比较低，达成共识的周期较长，因此不适合商业应用。

2）PoS（权益证明）：由Quantum Mechanic 2011年在bitcointalk首先提出，它是PoW的一种升级共识机制，在Peercoin、NXT和以太坊第四个阶段等应用。根据每个节点所占代币的比例和时间，等比例地降低挖矿难度，从而加快寻找随机数的速度，因此节点记账权的获得难度与节点持有的权益成反比，但它依然是基于哈希运算竞争获取记账权的方式。其优点是相对于PoW在一定程度减少了数学运算带来的资源消耗，性能也得到了相应的提升；缺点是还需要挖矿，本质上没有解决商业应用的痛点，可监管性也比较弱。

3）DPoS（股份授权证明）：与PoW和PoS不同，DPoS不需要再挖矿了，而是类似于董事会投票，持币者投出一定数量的节点，代理他们进行验证和记账，持股人拥有所持股份对应的表决权。优点是大幅缩小参与验证和记账节点的数量，可以达到秒级的共识验证，降低运行网络的成本和维护网络安全的费用，增强网络效能；缺点是整个共识机制还是依赖于代币的，然而很多商业应用是不需要代币存在的。

在区块链加密技术出现之前，互联网上的信息拷贝是零成本的，数字资产具有无限可复制性，如果没有可信赖的第三方监督，我们根本无法确认一笔数字现金是否被花掉，因此可能出现重复支付的问题。

为了解决这个问题，区块链参照了“拜占庭将军问题”（Byzantine failures） [5] 的算法。该问题是一个协议问题，指拜占庭帝国军队的将军们必须全体一致决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的，并且将军中存在叛徒，而将军们只能依靠信使来传递信息。如何才能防止受到叛徒欺骗而做出错误决策呢？数学家设计的算法是让将军在接到上一位将军标有进攻时间的信件之后，写上同意或反对并盖上自己的图章，然后把信转发给其他所有的将军，在这样的信息周转之后，最后会出现一个盖有超过半数将军图章的信息链，以保证将军们在互不信任的情况下达成共识。

莱斯利·兰伯特把拜占庭将军问题引入到点对点通信中。拜占庭假设是对现实世界的模型化，由于硬件错误、网络拥塞或断开以及遭到恶意攻击，计算机和网络可能出现不可预料的行为。拜占庭容错协议必须处理这些失效，并且这些协议还要满足所要解决问题要求的规范。

区块链的技术原理参考了拜占庭将军问题的算法，通过盖戳的形式来进行公证。网络上的每一个参与者的计算机里都会有一份总账的备份，也都能在这本总帐里记上一笔，并且所有的备份都是在实时地、持续地进行更新、对账，以及同步着拷贝，即全网记账。每个节点都可以竞争盖戳，互相认证。这使得一个不可信网络变成了一个可信的网络，使得所有参与者可以在某些事情上达成一致。

1.2.6　区块链并不一定去中心化

理想化的区块链系统，是由许许多多节点组成的点与点的网络结构，似乎既不需要中心化的硬件设备，也不需要任何管理它的机构。在很多文献中都提出区块链是去中心化的（Decentralized），即整个网络没有中心化的硬件或者管理机构，任意节点之间的权利和义务都是均等的，且任一节点的损坏或者失去都会不影响整个系统的运作。

需要指出的是，区块链并不一定是去中心化的。实际上，软件系统的网络架构一般有3种模式：单中心、多中心、分布式，Decentralized只表明不是单中心模式的，它可能是多中心或弱中心，也可能是分布式的。把Decentralized翻译成“去中心化的”可能与最早由中国大陆“币圈”所做出的翻译偏差有关，实际上在中国台湾地区大多译为“分散式的”而不是“去中心化的”。因此，把Decentralized翻译成“分布式”或者“多中心化”，可能更切合今天技术和金融场景应用的实际。

在人类历史上，信息传播的延迟和谬误会导致信息不对称，这形成了各种中心化的强权组织和阶级分化。虽然信息社会特别是社交媒体的出现已经减弱了这种情况，但是目前仍无法达到绝对的信息对称。我们期待在区块链搭建的机器社会中进行深刻且迅速的社会关系变革，形成绝对信息对称，但是至少目前在机器社会还难以实现，即不能完全去中心化。

2016年The DAO受攻击事件也表明，完全去中心化至少在现阶段是不可行的。The DAO是一个基于以太坊公有链的众筹项目，成为史上最大的众筹项目。然而由于其智能合约的漏洞，导致The DAO被黑客攻击并转移走价值6000万美元的数字货币，最后不得不黯然落幕。这给以太坊生态系统带来了很多负面影响。此次事件之后，很多人对区块链的“去中心化”进行了反思：在挽回这个损失的过程中，原有的去中心化机制未能解决问题，最后还是通过“集中式”的方式，强制以太坊进行“硬分叉”完成交易回滚。

如果我们仔细研究中本聪的论文，就会发现其中只有Peer-to-Peer（P2P），而没有Decentralized一词。业界也正在逐渐对区块链并不一定去中心化形成共识。在2016年6月召开的W3C区块链标准会议上，以太坊的核心开发团队EthCore明确表示，不再使用Decentralized这个词，而是用P2P、Secure、Serverless这类纯技术性词语。

但是如果简单地宣称去中心化，会被误读成是在某种程度上存在着一种既想从事金融活动，又不愿意接受金融监管的倾向。

1.3　区块链技术平台

目前，全球有数个区块链技术平台，其中比特币（Bitcoin）、以太坊（Ethereum）、瑞波（Ripple）和Linux基金会的开源项目超级账本（Hyperledger Fabric）比较有代表性。

此外，还有比特股（Bitshare）、恒星（Stellar）、R3 Corda等国外区块链技术以及国内一些公司研发的区块链应用平台。

1.3.1　比特币

比特币（Bitcoin）是最早、全球使用最广泛的区块链技术，具有最去中心化、最多分布节点、最公平等特点。

比特币提出了一个不需要信用中介的数字货币系统，通过数字签名（Digital Signatures）使得在线支付能够直接由一方发起并支付给另外一方，中间不需要通过任何的金融机构。同时为了防止双重支付（Double-Spending），它提出了一种采用工作量证明机制的点对点网络来记录交易的公开信息，该网络通过随机散列（Hashing）对全部交易加上时间戳（Time），将它们合并入一个不断延伸的基于随机散列的工作量证明（Proof of Work）的链条作为交易记录，形成的交易记录将不可更改。只要诚实的节点能够控制绝大多数CPU的计算能力，就能使攻击者难以改变交易记录。节点之间的工作，大部分是彼此独立的，只需要很少的协同。每个节点都不需要明确自己的身份，可以随时离开网络，若想重新加入网络也非常容易。节点通过自己的计算力进行投票，表决它们对有效区块的确认，它们不断延长有效的区块链来表达自己的确认，并拒绝在无效区块之后延长区块以表示拒绝。可以说，比特币包含了一个点对点数字货币系统所需要的全部规则和激励措施。

在比特币这种去中心化的公有区块链系统中，在相互间没有信任基础的前提下需要有一种完成点对点交易的共识机制。比特币发行的共识机制基于工作量证明算法（挖矿），使用过程基于点对点支付和全局记账，货币有效性基于追溯验证算法。“挖矿”过程就是把系统中没有记录的现有交易打包到区块里，通过系统提供的计算“挖矿”难度的随机数不断遍历，最先达到条件的会获得记录区块的权利。随后节点将该区块记录通过网络发布广播，全网其他节点在验证该区块满足条件，同时区块记录的交易符合规定后，分别把该区块记录的信息更新到自己节点的区块链上，从而形成全网账本的共识。

比特币区块链核心技术的框架采用C++语言开发，共识算法采用PoW算法，工作量（挖矿）证明获得记账权，容错50%，实现全网记账，公网性能TPS小于7，开源地址为：https://github.com/bitcoin/bitcoin 。比特币发行并运行到现在，说明了区块链技术在数字货币领域的可行性，但其并不能完全代表区块链技术，它只有唯一的数字资产（比特币），而且没有图灵完备的编程语言平台，允许开发人员建立更广泛的分布式账本系统应用。

比特币区块链推出的时间比较早也不够强大（如不支持智能合约）。现在当人们提到“区块链”时，往往已经与比特币网络没有直接联系了，除非特别指出是承载比特币交易系统的“比特币区块链”。但比特币仍是区块链最早也是截至目前数字货币方面最大并且在全球各地经常提及的应用。

在比特币源代码基础上，照搬或进行较小改动之后，还出现了一些区块链技术体系，其中包括一些山寨币，例如彩色币（染色币）等，还有以锚定比特币为基础的比特币侧链等。

1.3.2　以太坊

以太坊是一个通用的数字代币平台 [3] ，它通过一套图灵完备的脚本语言（Ethereum Virtual MachineCode，EVM语言）建立应用，采用多种编程语言实现协议（编程并不需要直接使用EVM语言，而是使用类似C语言、Python、Lisp等高级语言，再通过编译器转成EVM语言），采用Go语言编写的客户端作为默认客户端（即与以太坊网络交互的方法，支持其他多种语言的客户端）。以太坊ETH的开源地址：https://github.com/ethereum 。

以太坊的核心目标是智能合约，它可以看作是一个以太坊系统里的自动代理人。它有一个自己的以太币地址，当用户向合约地址发送一笔交易后，该合约就会被激活，然后根据交易中的额外信息，合约运行自身的代码，最后返回一个结果，这个结果可能是从合约地址发出的另外一笔交易。需要指出的是，以太坊中的交易不只是发送以太币而已，它还可以嵌入相当多的额外信息。如果一笔交易是发送给合约的，那么这些信息就非常重要，因为合约将根据这些信息来完成自身的业务逻辑。智能合约的引入对区块链2.0有着极大的推动作用，而作为早期推动智能合约的区块链平台，以太坊一度为广大区块链社区所看好。智能合约配合友好的界面和一些额外的小支持，可以让用户基于合约搭建各种千变万化的DApp应用，这样使得开发人员开发区块链应用的门槛大大降低。以太坊架构图如图1-4所示。

图1-4　以太坊架构图

以太坊合并了很多对比特币用户来说十分熟悉的特征和技术，同时自己也进行了很多修正和创新。比特币区块链纯粹是一个关于交易的列表，而以太坊的基础单元是账户。以太坊区块链跟踪每个账户的状态，所有以太坊区块链上的状态转换都是账户之间价值和信息的转移。账户分为两类：

·外部账户，由私人密码控制；

·合约账户，由它们的合约编码控制，只能由外部账户“激活”。

对于大部分用户来说，两者的基本区别在于外部账户是由人类用户掌控——因为他们能够控制私钥，进而控制外部账户。而合约账户则是由内部编码管控。如果它们是被人类用户“控制”的，那也是因为程序设定了它们被具有特定地址的外部账户所控制，进而被持有私钥控制外部账户的人控制着。“智能合约”这个流行的术语指的是在合约账户中编码——交易发送给该账户时所运行的程序。用户可以在区块链中部署编码来创建新的合约。

以太坊迭代周期比较快，使得依赖于以太坊特别是以太坊公网的商业应用比较容易受到攻击。The DAO（The DistributedAutonomous Organization，去中心化自治组织）是区块链业界最大的众筹项目，它在短时间内就募集了价值1.3亿美元的数字货币。2016年6月17日，由于其编写的智能合约存在重大缺陷，运行在以太坊公有链上的The DAO智能合约遭遇攻击 [1] 。导致300多万以太币资产被分离出The DAO资产池。The DAO官方交流社区DAOhub称，在黑客风波结束及所有的以太币被解锁后，返还所有的以太币，并关闭The DAO。

1.3.3　瑞波

瑞波（Ripple）是开放源码的点到点支付网络，它可以轻松、廉价、安全、跨国界地进行转账。转账对象可以是互联网上的任何一个人，无论他在世界的哪个地方；被转账的可以是清算货币、虚拟货币、数字资产或任意一种有价值的资产。

瑞波使用的共识机制是RPCA，可以通过特殊节点的投票，在很短的时间内对交易进行验证和确认。瑞波客户端不需要下载区块链，它在普通节点上舍弃掉已经验证过的总账本链，只保留最近已验证的总账本和一个指向历史总账本的链接，因而同步和下载总账本的工作量很小。

作为世界上第一个开放的支付网络，瑞波于2015年下半年对外公布了其InterLedger协议项目，该项目的目标就是打造全球统一的支付标准，创建统一的网络金融传输协议。通过瑞波支付网络可以转账任意一种货币，包括美元、欧元、人民币、日元或者比特币，简便、易行、快捷，交易确认在几秒内完成，交易费用几乎是零，没有所谓的跨行异地以及跨国支付费用。而且由于是P2P软件，所以没有任何个人、公司或政府操控瑞波，任何人都可以创建一个账户。

目前，瑞波币和以太坊之间正在争夺世界第二大加密货币的地位，胜负尚未决出。2017年早些时候，瑞波网络已实现在3.7s内能够完成7万笔交易。瑞波公司为其瑞波共识账本（RCL）和互联账本协议（ILP）引入了新的功能（托管和支付通道），这提高了瑞波币（XRP）的交易吞吐量，这些改进使得瑞波网络的可扩展性达到了Visa的级别，也就是其交易吞吐量将可与Visa抗衡。

1.3.4　区块链商用平台：超级账本

超级账本（Hyperledger）是Linux基金会的区块链项目，致力于发展跨行业的商用区块链平台技术 [4] 。超级账本项目自创立伊始便吸引了众多行业的领头羊，包括金融业、银行、互联网行业、运输业、制作业等。目前，超级账本项目在全球拥有超过100个成员，包括Cisco、IBM、Intel、J.P.Morgan、荷兰银行、SWIFT等。基于区块链技术、智能合约及其他相关技术，超级账本项目致力于建立新一代的分布式账本交易应用平台，从而在简化商业流程和法律事务的同时，建立起商业信任、透明、审查能力。旗下的Hyperledger Fabric子项目是以IBM早期捐献出的Open Blockchain为主体搭建而成，当时共向HyperLedger贡献了44000行开源代码。

HyperLedger Fabric是一个带有可插入各种功能模块架构的区块链实施方案，目标是打造成一个由全社会共同维护的开源超级账本。开源地址：https://github.com/hyperledger/fabric 。Fabric的主要框架核心开发语言是Go语言，其更适合于联盟链。早在2016年IBM宣布，计划提供开源代码并持续向超级账本项目（Hyperledger Project）贡献区块链代码。IBM将提供经过IBM测试与认证的超级账本区块链代码，以及在多种技术平台上进行安装的方法，以便开发者可以在容器（Container）内执行超级账本的代码，并开始快速构建商品溯源、贸易融资、信用证、供应链以及企业贷款等区块链网络。

可以说，Hyperledger是对传统区块链模型的革新，在某种程度上是允许创建授权和非授权的区块链。Hyperledger还通过提供一个针对身份识别、可审计、隐私安全和健壮的模型，使得缩短计算周期、提高规模效率和响应各个行业的应用需求成为可能。

利用超级账本平台，用户可以轻松地搭建起企业级的区块链网络。在这个网络中，每名成员都可以访问实时更新、加密过的账本，并能查询及发起交易。一旦交易经过共识流程的验证，它就会立即加入到网络中所有的账本中，并且不能更改。交易结果迅速、私有、保密且易于审计。另外，早期的超级账本还定义了协议规范：Open Blockchain Protocol Specification，并以此建立了区块链平台Hyperledger Fabric，并可以用于一系列B2B和B2C交易相关的行业案例中。为了满足这些功能及要求，Hyperledger Fabric的实现利用了下述概念：

·智能合约（smart contracts）

·数字资产（digital assets）

·记录存储系统（system of record repositories/stores）

·基于共识的去中心化网络（decentralized consensus-based network）

·可插拔的共识算法及共识模型（pluggable consensus algorithms/models）

·加密安全机制（cryptographic security）

这些概念和功能让Hyperledger Fabric架构结合了3个范畴：成员管理、区块链和智能合约（Chaincode）。这3个范畴是按逻辑划分的，而不是在物理上对独立过程、地址空间或（虚拟）机器的组件分割。

（1）成员管理服务

成员管理提供了诸多服务，包括身份管理、网络隐私、保密及审查。对于非准入型区块链，参与者不需要提供认证许可，所有的节点都平等地发起交易、验证交易及累积账本。也就是说，在非准入型区块链中没有身份的区别。成员管理服务结合PKI技术和去中心化/共识，将非准入型区块链转变为准入型区块链。在准入型区块链中，参与者通过注册获取身份认证许可（注册证书），并且通过参与类型区分类别。通过使用身份认证许可，用户可以向交易认证中心（TCA）申请伪匿名认证许可。只有使用这样的许可信息（即交易证书）用户才可以发起交易。此外，交易证书在区块链上永久存在，审查人员可以以此追溯交易。

（2）区块链服务

通过建立在HTTP/2上的P2P协议，区块链服务管理分布式账本。账本上的数据结构被高度优化，从而支持对世界状态复制的高效哈希算法。此外，在部署智能合约时，还可以指定不同的共识算法，如PBFT、RAFT、PoW和PoS等。

（3）智能合约

智能合约在Fabric中称为“链码”。链码服务为链码在验证节点上的执行提供了安全轻量级的沙箱。执行环境是一个“锁定”且安全的容器及一组签名镜像，包含安全操作系统和链码语言、运行时、SDK层。链码语言包括Go、Java和Node.js。此外，可以根据需求来启用其他语言。在网络中，验证节点与链码可以发出事件，应用程序可以监控并响应这些事件。目前已经预置了一些事件类型，链码还可以发出用户自定义事件。

1.3.5　区块链技术平台比较

各个区块链技术平台各有千秋，前面提到的共识机制、是否有智能合约功能、适用场景等都是进行比较的主要内容。

1）智能合约： 1995年，跨领域法律学者和密码学家尼克·萨博（Nick Szabo）首次提出了“智能合约”（Smart contract）这一术语。当一个预先编好的条件被触发时，智能合约执行相应的合同条款。从本质上讲，这些智能合约的工作原理类似于其他计算机程序的if-then语句，智能合约只是以这种方式与真实世界的资产进行交互。以太坊和HyperLedger Fabric等以智能合约为核心的区块链越来越受到重视。

2）适用场景：通常把区块链分为“公有链”（Public blockchain）、“私有链”（Private blockchain）和“联盟链”（Consortium blockchain）3种。公有链对所有人开放，任何人都可以参与比特币，这是最典型的公有链；联盟链仅对特定的组织团体开放；私有链仅对单独的个人或实体开放。现在业内普遍认为联盟链介于公有链和私有链之间，可视为“部分去中心化”，公众可以查阅和交易，对于验证交易或发布智能合约等功能需要获得联盟许可。在非数字货币之外的场景中引入区块链技术时，使用哪种区块链，需要对诸多因素进行权衡决策。短期内，主流金融机构仍难以接纳公有链。共识算法的对比如表1-1所示。

表1-1　共识算法比对

[1] The DAO编写的智能合约中有一个splitDAO函数，攻击者通过此函数中的漏洞重复利用自己的资产不断从The DAO项目的资产池中分离资产给自己。

1.4　区块链的商用之道

区块链之所以称为一种“颠覆性”的新兴技术，因为尽管其成名于比特币，但未来区块链的用武之地将远远超过加密货币。区块链的分布式共享账本这一技术本质能够在商业网络中使更多的参与方得到更加广泛的参与，并为商业网络或行业业务带来更低的沟通或整合成本，以及更高的业务效率。可以预见，区块链作为一个独立的技术板块，会在商业领域中得到广泛应用。

1.4.1　区块链的2.0时代：商用区块链

自2009年比特币在交易领域迅速崛起以来，这种加密币受到了广泛关注，但也颇受争议。不过比特币的底层技术——区块链，由于能够快速改进银行、供应链以及其他交易网络，在降低与业务运营相关的成本和风险的同时，带来创新和增长机会，所以是比较无争议的新兴技术模式，得到了商业界的鼎力支持。

传统的商业业务模式存在的问题是很难在一个互信的网络中监视跨机构的交易执行：每个参与方都有自己的账本，在交易发生时各自更改；协同各方会导致额外的工作及中介等附加成本；由于业务条件的不同，“合同”重复分散在各个参与方，造成整体业务流程的低有效性；整个业务网络依赖于一个或几个中心系统，整个商业网络十分脆弱。

而区块链提供了共享、复制、授权的账本这样一个解决方案。区块链架构带来了以下改变：

1）区块链架构使每一个商业网络的参与方都具有一个共享账本，当交易发生时，通过点对点的复制更改所有账本；

2）使用密码算法确保网络上的参与者仅仅可以看到和他们相关的账本内容，交易是安全的、授权的和验证的；

3）区块链也将与资产转移交易相关的合同条款嵌入交易数据库中以做到在满足商务条件下交易才发生；

4）网络参与者基于共识机制或类似的机制来保证交易是共同验证的，商业网络满足政府监管、合规及审计。

总体而言，区块链在提高业务效率和简化流程上确实具有优势。

当前国内外区块链产业生态发展迅猛，产业链层次逐渐清晰，无论是底层基础架构和平台，还是细分产业板块的区块链应用，以及风险资本投资都已初具规模。综合来看，全球区块链在商业行业发展具有三大趋势。

1）从比特币向更丰富的应用场景发展。区块链2.0把之前“区块链就是比特币”的意义向前推进了一大步，区块链也不再是比特币的专有技术和代名词，而是在更加广泛的应用场景中，成为资产流转的价值表述。区块链当下不再依赖数字货币或资产这一类单一场景，而是发展到支付汇兑、电子商务、移动社交、众筹、慈善、互助保险等面向终端用户的应用，以及数字资产、IP版权和交易、金融清算和结算、商品溯源等企业级应用领域。

2）全球区块链生态日益丰富，参与方开始出现明显的产业分工。从全球视角来看，随着参与者越来越多，区块链形成了不同的技术平台、行业以及发展路径的产业生态。对全球的区块链从业者来说，更看重区块链作为未来金融科技（FinTech）的一个领域从而提前布局，大胆尝试实践区块链在金融及其他行业中的各类业务场景。同时，高科技龙头企业也希望在区块链技术框架的建立上尽早发力，通过支持全球开源社区建立更扎实的底层区块链平台和更广泛的应用场景。区块链的热潮带动了更多创业者的热情，众多初创公司如雨后春笋般应运而生。从行业角度来看，区块链初创公司覆盖了银行和保险服务、供应链、医疗、物联网、外贸等众多行业，可谓是百花齐放。

3）全球投资正在快速注入，重点关注企业级应用落地。区块链项目融资正呈现井喷式增长，从2012年到2015年，区块链领域吸引的风险投资从200万美元增长到4.69亿美元，增长超过了200倍，累计投资已达10亿美元左右。2016年仅在金融领域，区块链技术投资额就占整体投资的七成以上。从全球的投资情况来看，由于越来越多的行业已经开始实践区块链，所以使得更多投资人开始关注行业内的区块链应用场景，投资趋于理性，但更注重利用投资者自身资源帮助投资标的进行深度的行业孵化。

总之，市场、行业、投资等多方对于商用区块链的发展诉求十分强烈，作为“颠覆性创新”技术的区块链前景光明。

1.4.2　超级账本：商用区块链的“第五元素”

企业级区块链四大平台要素包括：共享账本，共识、隐私和保密、智能合约。此外，还有第五要素，即商业网络。企业级的区块链一定是围绕业务场景展开的，因此在第五元素商业网络当中需要包含市场参与者的对等架构以及伙伴间的一个共识协议。

目前，以比特币为代表的公有链有一些加密货币之外的新型应用，但是却无法克服自身固有的一些问题，例如交易效率低，区块没有最终确定性（finality）等，而且是由极客主导的，不符合商业主流趋势。为了克服上述不足，满足大多数商业应用的要求，设计开发适合商用的区块链平台迫在眉睫。

企业级商用区块链网络比较适合使用联盟链和许可制。这样在一个限定的范围内，只有授权的节点和用户才能参与到交易和智能合约的执行中来，而任何匿名节点或非授权用户均被拒绝服务。从团体联盟的角度来看，这增加了区块链网络的安全可靠。当前，在欧美主流的区块链应用大部分是行业链或者是联盟链，也就是某一个行业的上下游，或者核心企业联合起来，一起构建的半公开化的区块链。从这个角度讲，超级账本具备成为未来最主要商用区块链技术平台的潜力，值得技术开发人员花时间和精力进行学习和研究。

由于超级账本有个重要的设计原则是按照“用例驱动”（use case driven）的方式来实现的，所有功能都应该有对应的用例需求，因此学习研究的过程并不一定十分辛苦。此外，鉴于超级账本是个通用型框架，无法预先确定将来所有的应用场景，因此，定义出部分典型的用例，可使超级账本先满足这部分有代表性的区块链应用需求，然后再用可替换模块满足其他需求。

1.4.3　区块链的商业应用场景

区块链的商业应用才刚刚起步，一般都将金融业应用作为切入口，很多其他领域的应用还在探索或试水阶段。最重要的是，不能为了技术而技术，为了区块链而区块链。商用区块链技术要解决企业的痛点，为客户创造新的价值。可喜的是，在金融和金融以外的各个细分领域，区块链都在加速落地。以下为一些应用实例和构想。

1）金融领域。21世纪是金融的“大航海时代”，对银行、保险、清算、股权登记交易、信用评级、公证等领域，既需要绝对的可信任，也需要隐私保密，所以特别适合区块链应用。举例来说，金融行业关心的资产分布式管存，可以把资产（如证券等）数据存放在区块链网络中，资产利益相关人可以直接访问资产数据，而无须经过传统的中间人，可大幅提高效率和节约成本。区块链股权登记和交易平台脱胎于加密币交易所，也是比较合适及容易实现的应用。

2）产业互联网领域。供应链溯源和共享经济可以应用区块链。在供应链中，所有的参与者都通过区块链记录、追踪和共享各种数据，这些数据记录存储在区块链里面并贯穿货物的生产、运输和销售等环节，从而提供深度回溯、查询等核心功能，实现信息公开透明，出了问题可以依此来追责。附加值较高的食品、药品和疫苗、零部件生产检测结果等都可以使用区块链。

例如，现在市场上号称是北大荒地区生产的大米，特别是五常大米，是当地实际产量的很多倍，造成良莠不齐。消费者希望花比较高的价钱购买真正的北大荒大米，却苦于无法分辨哪些大米是真的。此处可以体现区块链在供应链溯源上的价值，就是利用区块链中数据记录的真实性或者有效性。如果在原产地和各个流通环节中设置的传感设备在区块链上签名盖戳，一旦进入到区块链里面，每个人的签名就不能抵赖了。含有被区块链标记的时间戳、地理戳、品质戳的放心粮从源头上杜绝了各个环节作弊的动机，这样市场上才能销售与当地产量相匹配的大米。

3）传统行业的转型创新。区块链的应用绝不仅局限于金融和互联网等前沿领域，还可以与能源、零售、电商、房地产等传统领域接轨，因此区块链不是个摆设。例如，高盛公司就提出，对于资产所有权需经过谨慎识别的房地产交易，如果能够利用区块链技术建立安全、共享的所有权数据库，那么房产交易纠纷和交易成本将大大缩小。

4）FinTech2.0的三驾马车：区块链、认知物联网和人工智能。未来，世界将进入人工智能、认知物联网和区块链三足鼎立的时期，如果能将三者有机结合将创造巨大的价值。例如，如果将闲置或未充分利用的资产（如汽车、仓库、医疗设备等）接入物联网，那么区块链技术可以帮助互不相识的这些资产的所有者进行资产使用的交易谈判。在共享经济的模式下，最需要解决的就是陌生人之间的信任问题，即资源的提供方和资源的租用者，如何在缺乏信任的基础上安全地完成交易。分布式区块链将是一种全新的去信任方式，不使用任何中间平台，达到各方参与者可靠交易的目的。这有点类似于分时用车和分时用房，这将引爆以前隐藏在深处的过剩资产容量。

1.5　本章小结

区块链作为当下最流行的技术之一，从提出概念原型到造就价格惊人的比特币只用了不到十年的时间，而且它的用途还可以扩展到商业的多个领域。本章介绍了区块链的概念，包括其本质、工作原理、技术特点、层次模型、共识算法等，还特别纠正了“区块链一定是去中心化的”错误观念。我们还对比特币、以太坊、瑞波、超级账本等技术进行了简要介绍和比较。现在我们大致了解了区块链的世界，下面可以具体学习超级账本了。

第2章　超级账本初体验

本章先简单介绍一下Hyperledger Fabric 1.0的环境搭建，快速地体验一下超级账本的功能。本书所有的内容都是基于Hyperledger Fabric 1.0的，在后面章节中我们偶尔也会用到“超级账本”这个词，指的也是超级账本的Hyperledger Fabric 1.0项目。

2.1　基础环境安装

Hyperledger Fabric 1.0依赖Docker执行智能合约，需要先安装Docker和Docker Compose的运行环境。

2.1.1　Docker的安装和使用

Docker支持Linux、Mac、Windows等多个平台，安装文档参考：https://docs.docker.com/engine/installation 。

1.在Linux环境下Docker的安装

Ubuntu、Debian、CentOS等Linux系统，可以通过Docker官方提供的脚本进行安装：

curl -sSL https://get.docker.com | sh

然后把用户加入到docker组，非root用户USER可以执行docker命令（可能需要重新登录生效）：

sudo usermod -aG docker $USER

如果是Ubuntu或者Debian操作系统，修改Docker的配置文件/etc/default/docker，增加Docker的socket绑定，运行在Docker中的进程才能通过映射的socket调用Docker的API执行镜像编译和创建容器等操作。

DOCKER_OPTS="-s=aufs -r=true --api-cors-header='*' -H tcp://0.0.0.0:2375 -H
unix:///var/run/docker.sock "

接着，重启Docker服务让配置生效：

sudo service docker start

CentOS系统采用Systemd进行系统和服务管理，配置文件的修改方法是不一样的。CentOS系统下Docker的配置文件是/etc/sysconfig/docker，同样要修改DOCKER_OPTS选项。还需要修改/usr/lib/systemd/system/docker.service文件，在[Service]的ExexStart=下面增加一行$DOCKER_OPTS，如下所示：

[Service]
Type=notify
NotifyAccess=all
EnvironmentFile=-/etc/sysconfig/docker
EnvironmentFile=-/etc/sysconfig/docker-storage
EnvironmentFile=-/etc/sysconfig/docker-network
Environment=GOTRACEBACK=crash
Environment=DOCKER_HTTP_HOST_COMPAT=1
Environment=PATH=/usr/libexec/docker:/usr/bin:/usr/sbin
ExecStart=/usr/bin/dockerd-current \
--add-runtime docker-runc=/usr/libexec/docker/docker-runc-current \
--default-runtime=docker-runc \
--exec-opt native.cgroupdriver=systemd \
--userland-proxy-path=/usr/libexec/docker/docker-proxy-current \
$DOCKER_OPTS \
$OPTIONS \
$DOCKER_STORAGE_OPTIONS \
$DOCKER_NETWORK_OPTIONS \
$ADD_REGISTRY \
$BLOCK_REGISTRY \
$INSECURE_REGISTRY

重启服务让配置生效：

systemctl daemon-reload
systemctl restart docker.service

2.其他环境下Docker的安装

Windows和Mac都提供了安装包，直接下载即可安装：

·Docker for Mac：https://download.docker.com/mac/stable/Docker.dmg

·Docker for Windows：https://download.docker.com/win/stable/InstallDocker.msi

3.Docker国内镜像仓库

国外的镜像下载较慢，可以设置国内的镜像，阿里云和DaoCloud都提供镜像加速的服务，需要登录注册才能使用。

·阿里云：登录容器Hub服务https://cr.console.aliyun.com 的控制台，左侧的加速器帮助页面会显示为你独立分配的加速地址。

·DaoCloud：在https://www.daocloud.io 进行注册登录，然后点击加速器，就可以获取加速器的相关配置。

修改Docker镜像仓库的办法是在DOCKER_OPTS里增加registry-mirror参数，比如：

DOCKER_OPTS="-s=aufs -r=true --api-cors-header='*' -H tcp://0.0.0.0:2375
-H unix:///var/run/docker.sock
--registry-mirror=http://069f616f.m.daocloud.io”

重启Docker服务就可以使用镜像加速了。

Docker在Windows和Mac中的版本可以在图形界面添加镜像仓库。

4.Docker常用命令

Docker常用命令如表2-1所示。

表2-1　Docker常用命令

更多的命令请查看帮助文档和在线文档：https://docs.docker.com/engine/reference/commandline/docker 。

2.1.2　Docker Compose的安装和使用

Docker Compose能够在一个主机上创建出相互隔离的网络，通过命令行管理多个Docker容器，快速启动、停止和更新容器。

1.Docker Compose的安装

Docker在Windows和Mac中都已经集成了Docker Compose工具，不需要单独安装。在Linux系统下有多种安装方法，如下所示：

（1）通过pip进行安装

sudo apt install python-pip
sudo pip install docker-compose

（2）直接下载文件

curl -L
https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-composechmod +x /usr/local/bin/docker-compose

2.Docker Compose的配置文件

Compose采用YAML文件定义Docker容器之间的依赖，设置环境变量和文件的持久化。我们看一个配置文件examples/e2e_cli/base/docker-compose-base.yaml的节选：

version: '2'

services:

orderer.example.com:
container_name: orderer.example.com
image: hyperledger/fabric-orderer
environment:
- ORDERER_GENERAL_LOGLEVEL=debug
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_GENESISMETHOD=file

- ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
# enabled TLS
- ORDERER_GENERAL_TLS_ENABLED=true
- ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt
- ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: orderer
volumes:

- ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/
msp:/var/hyperledger/orderer/msp
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/
tls/:/var/hyperledger/orderer/tls
ports:
- 7050:7050

peer0.org1.example.com:
container_name: peer0.org1.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer0.org1.example.com
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
- CORE_PEER_CHAINCODELISTENADDRESS=peer0.org1.example.com:7052
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
volumes:
- /var/run/:/host/var/run/
- ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.
com/msp:/etc/hyperledger/fabric/msp

- ../crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.
com/tls:/etc/hyperledger/fabric/tls
ports:
- 7051:7051
- 7052:7052
- 7053:7053

在这个节选的配置文件中，一共定义了1个排序服务节点orderer.example.com和1个Peer节点peer0.org1.example.com。Docker Compose目前有3个版本，这个配置文件采用的version 2的语法，配置文件的解释如表2-2所示。

表2-2　配置文件的解释

更多不同版本的配置文件说明请参考在线帮助文档：https://docs.docker.com/compose/compose-file 。

3.Docker Compose的常用命令

Docker Compose的常用命令如表2-3所示。

表2-3　Docker Compose的常用命令

更多的命令查看帮助文档和在线文档：https://docs.docker.com/compose/reference 。

2.1.3　下载超级账本源代码

超级账本的源代码都托管在https://gerrit.hyperledger.org/r/#/admin/projects/ 下面，并在https://github.com/hyperledger 上提供只读代码。最好的方式是直接通过git下载：

git clone https://github.com/hyperledger/fabric.git

也可以打包下载文件后解压：https://github.com/hyperledger/fabric/archive/release.zip 。

2.2　超级账本部署调用

先最小化地体验一下超级账本的环境，更详细的部署流程参考第11章。

2.2.1　下载Docker镜像文件

超级账本源码scripts目录下有多个下载镜像的脚本，我们可以修改权限以后直接运行：

# 进入fabric/scripts目录
chmod +x bootstrap-1.0.0.sh
# MacOS系统执行如下命令（不下载二进制文件）
sed -i '' 's/curl/#curl/g' bootstrap-1.0.0.sh
#其他系统执行如下命令（不下载二进制文件）
sed -i 's/curl/#curl/g' bootstrap-1.0.0.sh
# 直接下载Docker镜像文件
./bootstrap-1.0.0.sh

根据网络情况，可能需要等待一段时间。下面是下载的Docker镜像文件：

localhost:dive-into-fabric clarity$ docker images
REPOSITORY TAG IMAGE ID SIZE
hyperledger/fabric-tools latest 0403fd1c72c7 1.32GB
hyperledger/fabric-tools x86_64-1.0.0 0403fd1c72c7 1.32GB
hyperledger/fabric-couchdb latest 2fbdbf3ab945 1.48GB
hyperledger/fabric-couchdb x86_64-1.0.0 2fbdbf3ab945 1.48GB
hyperledger/fabric-kafka latest dbd3f94de4b5 1.3GB
hyperledger/fabric-kafka x86_64-1.0.0 dbd3f94de4b5 1.3GB
hyperledger/fabric-zookeeper latest e545dbf1c6af 1.31GB
hyperledger/fabric-zookeeper x86_64-1.0.0 e545dbf1c6af 1.31GB
hyperledger/fabric-orderer latest e317ca5638ba 179MB
hyperledger/fabric-orderer x86_64-1.0.0 e317ca5638ba 179MB
hyperledger/fabric-peer latest 6830dcd7b9b5 182MB
hyperledger/fabric-peer x86_64-1.0.0 6830dcd7b9b5 182MB
hyperledger/fabric-javaenv latest 8948126f0935 1.42GB
hyperledger/fabric-javaenv x86_64-1.0.0 8948126f0935 1.42GB
hyperledger/fabric-ccenv latest 7182c260a5ca 1.29GB
hyperledger/fabric-ccenv x86_64-1.0.0 7182c260a5ca 1.29GB
hyperledger/fabric-ca latest a15c59ecda5b 238MB
hyperledger/fabric-ca x86_64-1.0.0 a15c59ecda5b 238MB

REPOSITORY代表的是镜像的仓库名称，每个仓库下面都有打了不同TAG的标签名称，代表不同的版本。通常最少有两个标签，一个是latest；另外一个的命名规则是“主机CPU类型–超级账本主版本号–snapshot–代码库版本号”，其中主机CPU类型为x86_64，说明是Intel的64位CPU，超级账本的主版本为1.0.0，snapshot是固定名称，代码库版本号为58cde93，它是git代码库最近一次提交版本号的前7位。snapshot和代码库版本号只有通过本地编译的时候才会出现。每次make docker的时候都会检查是否有文件改动，如果有变化的文件，则会重新构建，生成新的镜像再标记成latest。镜像文件详细的解释请参考第11章的相关内容。

2.2.2　部署超级账本网络

运行超级账本需要设置较多的初始化配置，我们先绕开初始化过程，用fabric-samples工程中已经生成的配置文件来体验部署安装的过程：

git clone https://github.com/hyperledger/fabric-samples.git

进入basic-network目录，利用docker-compose启动容器：

cd fabric-samples/basic-network
docker-compose -f docker-compose.yml up -d

查看已经启动的容器（输出进行了删减）：

localhost:basic-network clarity$ docker ps
CONTAINER ID IMAGE NAMES
efddfbf4fc0a hyperledger/fabric-peer:x86_64-1.0.0 peer0.org1.example.com
606d13c1e7a2 hyperledger/fabric-couchdb:x86_64-1.0.0 couchdb
d8c870db8634 hyperledger/fabric-ca:x86_64-1.0.0 ca.example.com
c6f25a5e6fd6 hyperledger/fabric-tools:x86_64-1.0.0 cli
a5f6331c5bc5 hyperledger/fabric-orderer:x86_64-1.0.0 orderer.example.com

切换到管理员用户再创建通道和加入通道：

# 切换环境到管理员用户的MSP,进入Peer节点容器peer0.org1.example.com
docker exec -it -e "CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp/users/Admin@
org1.example.com/msp" peer0.org1.example.com bash
# 创建通道
peer channel create -o orderer.example.com:7050 -c mychannel -f /etc/hyperledger/
configtx/channel.tx
# 加入通道
peer channel join -b mychannel.block
# 退出Peer节点容器peer0.org1.example.com
exit
#退出Peer节点容器peer0.org1.example.com，进入cli容器安装链码和实例化：
# 进入cli容器
docker exec -it cli /bin/bash
# 给Peer节点peer0.org1.example.com安装链码
peer chaincode install -n mycc -v v0 -p github.com/chaincode_example02
# 实例化链码
peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n mycc -v
v0 -c '{"Args":["init","a","100","b","200"]}'

2.2.3　链码调用和查询

链码实例化以后，可以查询初始值，同样是在cli容器里执行下面的操作：

peer chaincode query -C mychannel -n mycc -v v0 -c '{"Args":["query","a"]}'

查询结果显示为Query Result：100，详细信息如下：

2017-08-09 14:47:05.853 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing
local MSP
2017-08-09 14:47:05.853 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 14:47:05.853 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003
Using default escc
2017-08-09 14:47:05.854 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004
Using default vscc
2017-08-09 14:47:05.854 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A9
1070A6708031A0C08A9A694D00510...6D7963631A0A0A0571756572790A0161
2017-08-09 14:47:05.854 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: E18FC9
7C13D550C5E3349AAD49523A6D7C71B4E51C219CD9A8799DEF54FFFE66
Query Result: 100
2017-08-09 14:47:05.886 UTC [main] main -> INFO 007 Exiting.....

调用链码，从“a”转移10到“b”：

peer chaincode invoke -C mychannel -n mycc -v v0 -c '{"Args":["invoke","a",
"b","10"]}'

显示调用成功的结果：

2017-08-09 14:49:46.018 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0cb
Chaincode invoke successful. result: status:200

再次查询“a”和“b”的值：

peer chaincode query -C mychannel -n mycc -v v0 -c '{"Args":["query","a"]}'
peer chaincode query -C mychannel -n mycc -v v0 -c '{"Args":["query","b"]}'

查询结果显示“a”的值为Query Result：90，“b”的值为Query Result：210。

2.2.4　常见错误

1.请求调用者权限不足

调用的时候设置了错误的MSP，比如需要管理员才能执行创建通道的操作，但是设置了普通的成员MSP，会出现Error：Got unexpected status：BAD_REQUEST的错误：

2017-08-09 14:49:04.652 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing
local MSP
2017-08-09 14:49:04.652 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 14:49:04.654 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and
orderer connections initialized
2017-08-09 14:49:04.656 UTC [msp] GetLocalMSP -> DEBU 004 Returning existing
local MSP
2017-08-09 14:49:04.656 UTC [msp] GetDefaultSigningIdentity -> DEBU 005 Obtaining
default signing identity
2017-08-09 14:49:04.657 UTC [msp] GetLocalMSP -> DEBU 006 Returning existing
local MSP
2017-08-09 14:49:04.657 UTC [msp] GetDefaultSigningIdentity -> DEBU 007 Obtaining
default signing identity
2017-08-09 14:49:04.657 UTC [msp/identity] Sign -> DEBU 008 Sign: plaintext: 0A8
8060A074F7267314D535012FC052D...53616D706C65436F6E736F727469756D
2017-08-09 14:49:04.657 UTC [msp/identity] Sign -> DEBU 009 Sign: digest: F77320
AE89B131CE75A858A4A450CF0F35301DA62FE1DE465CAEF4439F6FC520
2017-08-09 14:49:04.657 UTC [msp] GetLocalMSP -> DEBU 00a Returning existing
local MSP
2017-08-09 14:49:04.657 UTC [msp] GetDefaultSigningIdentity -> DEBU 00b Obtaining
default signing identity
2017-08-09 14:49:04.657 UTC [msp] GetLocalMSP -> DEBU 00c Returning existing
local MSP
2017-08-09 14:49:04.657 UTC [msp] GetDefaultSigningIdentity -> DEBU 00d Obtaining
default signing identity
2017-08-09 14:49:04.657 UTC [msp/identity] Sign -> DEBU 00e Sign: plaintext: 0AB
F060A1508021A0608E0D591D00522...A38A58EED7B94AC4CB800B86F0A5EF03
2017-08-09 14:49:04.658 UTC [msp/identity] Sign -> DEBU 00f Sign: digest: 475A33
426FA36D50F090AAF7C3AAAB2BF34339191BA74D4A60BF13460B241329
Error: Got unexpected status: BAD_REQUEST
Usage:
peer channel create [flags]

Flags:
-c, --channelID string In case of a newChain command, the channel ID to
create.
-f, --file string Configuration transaction file generated by a tool
such as configtxgen for submitting to orderer
-t, --timeout int Channel creation timeout (default 5)

Global Flags:
--cafile string Path to file containing PEM-encoded trusted
certificate(s) for the ordering endpoint
--logging-level string Default logging level and overrides, see
core.yaml for full syntax
-o, --orderer string Ordering service endpoint
--test.coverprofile string Done (default "coverage.cov")
--tls Use TLS when communicating with the orderer endpoint
-v, --version Display current version of fabric peer server

2.传递错误的通道名称

比如通道名称是mychannel，传递参数的时候写成了错误的myc：

peer chaincode query -C myc -n mycc -v v0 -c '{"Args":["query","a"]}'

会出现如下错误：

2017-08-09 14:40:36.703 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing
local MSP
2017-08-09 14:40:36.703 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 14:40:36.706 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003
Using default escc
2017-08-09 14:40:36.706 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004
Using default vscc
2017-08-09 14:40:36.707 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A9
1070A6708031A0C08A4A394D00510...30300A000A04657363630A0476736363
2017-08-09 14:40:36.707 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: F085E1
89A0765713209DD8802DDF57B054EBCD294305A500F56BC34CB3D2E577
Error: Error endorsing chaincode: rpc error: code = Unknown desc = chaincode
error (status: 500, message: chaincode exists mycc)
Usage:
peer chaincode instantiate [flags]

Flags:
-C, --channelID string The channel on which this command should be executed
(default "testchainid")
-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
-l, --lang string Language the chaincode is written in (default "golang")
-n, --name string Name of the chaincode
-P, --policy string The endorsement policy associated to this chaincode
-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
--logging-level string Default logging level and overrides, see core.
yaml for full syntax
-o, --orderer string Ordering service endpoint
--test.coverprofile string Done (default "coverage.cov")
--tls Use TLS when communicating with the orderer endpoint

2.3　节点的配置参数传递规则

在docker-compose.yml文件中，我们可以看到有ORDERER_GENERAL_LEDGERTYPE=ram的设置，这是传递给节点的参数。给节点传递参数的方法有多种方式：环境变量、配置文件、动态环境变量、默认值。

程序在启动的时候会读取配置文件和环境变量的值，分别保存到不同变量缓存起来，在程序需要获取某个变量值的时候，不同传递方法的参数读取流程图如图2-1所示。

从图2-1中可以看到，如果配置了自动从环境变量获取参数的值，那么每次都实时地从环境变量中获取，否则依次读取程序启动时从环境变量、配置文件中读取后缓存到内存中的值，优先获取到的值作为返回值。如果都没有获取到，则返回空，交给程序进行处理，程序可能会以默认值运行，也可能会报错停止运行，这跟业务逻辑有关系。所以，只有在设置了自动从环境变量中获取参数的情况下，才能在运行时通过修改环境变量改变参数的值。

每个环境变量的名称都有一个前缀，每个模块都是单独设置的，比如ORDERER_GENERAL_LEDGERTYPE的前缀是ORDERER，通常每个模块的前缀是不一样的。比如这里的ORDERER代表的是排序服务节点，Peer节点的变量名称前缀是CORE。环境变量名称是以“_”作为分隔符的，代表一种层级，是和配置文件一一对应的，比如ORDERER_GENERAL_LEDGERTYPE对应orderer.yaml配置文件的General.LedgerType。环境变量和配置文件的变量名称都是不区分大小写的，内部会统一转换成小写的变量名称进行处理。

配置文件路径优先读取环境变量设置的路径，排序服务节点和Peer节点都是相同的环境变量。如果没有设置环境变量，则默认是应用程序所在的目录，然后环境变量GOPATH路径对应到模块代码工程下的目录，比如排序服务节点的目录是$GOPATH/src/github.com/hyperledger/fabric/orderer，Peer节点的目录是$GOPATH/src/github.com/hyperledger/fabric/peer。

图2-1　节点配置参数的传递规则

一般情况下，配置文件的名称设置成模块前缀的小写，比如排序服务节点的配置文件名称是orderer，Peer节点的配置文件名称是core。

配置文件名称的后缀支持：json、toml、yaml、properties、props、prop，它们分别对应JSON文件、TOML文件、YAML文件和Properties文件，程序设置配置文件的时候如果不指定后缀，则按支持的后缀顺序在配置文件路径下进行搜索，找到第一个匹配的文件作为最终的配置文件。文件路径和文件后缀按照广度优先的搜索顺序，即在同一路径下匹配完所有文件后缀再进入下一个文件路径。假设Peer节点没有设置配置文件路径，$GOPATH的路径是/opt/gopath，则有两个目录下的文件如下所示：

vagrant@hyperledger-devenv:v0.2.2-58cde93: /opt/gopath/bin$ tree .
.
├── core.yaml
└── peer
vagrant@hyperledger-devenv:v0.2.2-58cde93: /opt/gopath/bin$ tree /opt/gopath/
src/github.com/hyperledger/fabric/peer
.
├── core.json
└── peer

搜索路径顺序是：

./core.json
./core.toml
./core.yaml
./core.properties
./core.props
./core.prop
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.json
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.toml
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.yaml
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.properties
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.props
/opt/gopath/src/github.com/hyperledger/fabric/peer/core.prop

所以最终获取到的配置文件是：./core.yaml，而不是/opt/gopath/src/github.com/hyperledger/fabric/peer/core.json。

2.4　本章小结

本章零基础地介绍了如何快速体验超级账本搭建的区块链网络，我们先绕过了比较复杂的初始化配置，用官方提供的fabric-samples提供的配置和链码示例，展示了如何调用和查询链码，对Hyperledger Fabric实现的功能有一个初步的认识。这部分更为详细的初始化配置和网络部署等内容将在第11章会详细介绍。后面的章节我们会带你一起领略一下区块链的奥秘。

第二篇　核心篇

第3章　超级账本的系统架构

第4章　基于Gossip的P2P数据分发

第5章　分布式账本存储

第6章　集成共识机制的排序服务

第7章　实现数据隔离的多链及多通道

第8章　基于数字证书的成员管理服务

第9章　支持多种语言的智能合约

第3章　超级账本的系统架构

区块链的业务需求多种多样，一些要求在快速达成网络共识及快速确认区块后，才可以将区块加入区块链中。有一些可以接受相对缓慢的处理时间，以换取较低级别的信任。各行各业在扩展性、可信度、合法性、工作流复杂度以及安全性等方面的需求和用途都不尽相同。我们先来看一下在企业级区块链系统中常见的模块构成，如图3-1所示。

图3-1　企业级区块链系统的常用功能

从图3-1中可以看到一些常用的功能模块有：应用程序、成员管理、智能合约、账本、共识机制、事件机制、系统管理等。纵轴代表用户或者开发者更关心的内容，越往上代表用户更关注，比如应用程序和钱包等，越靠下是开发者更关注的模块，比如事件机制。而横轴则是从时间的维度来看的，左边是一开始关注的功能，直到完成所有的功能。

Hyperledger Fabric 1.0是一种通用的区块链技术，其设计目标是利用一些成熟的技术实现分布式账本技术（Distributed Ledger Technology，DLT）。超级账本采用模块化架构设计，复用通用的功能模块和接口。模块化的方法带来了可扩展性、灵活性等优势，会减少模块修改、升级带来的影响，能很好地利用微服务实现区块链应用系统的开发和部署。Hyperledger Fabric 1.0设计有几个特点：

1）模块插件化：很多的功能模块（如CA模块、共识算法、状态数据库存储、ESCC、VSCC、BCCSP等）都是可插拔的，系统提供了通用的接口和默认的实现，这满足了大多数的业务需求。这些模块也可以根据需求进行扩展，集成到系统中。

2）充分利用容器技术：不仅节点使用容器作为运行环境，链码也默认运行在安全的容器中。应用程序或者外部系统不能直接操作链码，必须通过背书节点提供的接口转发给链码来执行。容器给链码运行提供的是安全沙箱环境，把链码的环境和背书节点的环境隔离开，链码存在安全问题也不会影响到背书节点。

3）可扩展性： Hyperledger Fabric 1.0在0.6版本的基础上，对Peer节点的角色进行了拆分，有背书节点（Endorser）、排序服务节点（Orderer）、记账节点（Committer）等，不同角色的节点有不同的功能。节点可以加入到不同的通道（Channel）中，链码可以运行在不同的节点上，这样可以更好地提升并行执行的效率和吞吐量。

4）安全性： Hyperledger Fabric 1.0提供的是授权访问的区块链网络，节点共同维护成员信息，MSP（Membership Service Provider）模块验证、授权了最终用户后才能使用区块链网络的功能。多链和多通道的设计容易实现数据隔离，也提供了应用程序和链码之间的安全通道，实现了隐私保护。

3.1　系统逻辑架构

图3-2所示为Hyperledger Fabric 1.0设计的系统逻辑架构图。

图3-2　Hyperledger Fabric 1.0的系统逻辑架构图

图3-2所示的系统逻辑架构图是从不同角度来划分的，上层从应用程序的角度，提供了标准的gRPC接口，在API的基础之上封装了不同语言的SDK，包括Golang、Node.js、Java、Python等，开发人员可以利用SDK开发基于区块链的应用。区块链强一致性要求，各个节点之间达成共识需要较长的执行时间，也是采用异步通信的模式进行开发的，事件模块可以在触发区块事件或者链码事件的时候执行预先定义的回调函数。下面分别从应用程序和底层的角度分析应该关注的几个要素。

1.应用程序角度

（1）身份管理

用户注册和登录系统后，获取到用户注册证书（ECert），其他所有的操作都需要与用户证书关联的私钥进行签名，消息接收方首先会进行签名验证，才进行后续的消息处理。网络节点同样会用到颁发的证书，比如系统启动和网络节点管理等都会对用户身份进行认证和授权。

（2）账本管理

授权的用户是可以查询账本数据（ledger）的，这可以通过多种方式查询，包括根据区块号查询区块、根据区块哈希查询区块、根据交易号查询区块、根据交易号查询交易，还可以根据通道名称获取查询到的区块链信息。

（3）交易管理

账本数据只能通过交易执行才能更新，应用程序通过交易管理提交交易提案（Proposal）并获取到交易背书（Endorsement）以后，再给排序服务节点提交交易，然后打包生成区块。SDK提供接口，利用用户证书本地生成交易号，背书节点和记账节点都会校验是否存在重复交易。

（4）智能合约

实现“可编程的账本”（Programmable Ledger），通过链码执行提交的交易，实现基于区块链的智能合约业务逻辑。只有智能合约才能更新账本数据，其他模块是不能直接修改状态数据（World State）的。

2.底层角度

下面的内容是从Hyperledger Fabric 1.0底层的角度来看，如何实现分布式账本技术，给应用程序提供区块链服务。

（1）成员管理

MSP（Membership Service Provider）对成员管理进行了抽象，每个MSP都会建立一套根信任证书（Root of Trust Certificate）体系，利用PKI（Public Key Infrastructure）对成员身份进行认证，验证成员用户提交请求的签名。结合Fabric-CA或者第三方CA系统，提供成员注册功能，并对成员身份证书进行管理，例如证书新增和撤销。注册的证书分为注册证书（ECert）、交易证书（TCert）和TLS证书（TLS Cert），它们分别用于用户身份、交易签名和TLS传输。

（2）共识服务

在分布式节点环境下，要实现同一个链上不同节点区块的一致性，同时要确保区块里的交易有效和有序。共识机制由3个阶段完成：客户端向背书节点提交提案进行签名背书，客户端将背书后的交易提交给排序服务节点进行交易排序，生成区块和排序服务，之后广播给记账节点验证交易后写入本地账本。网络节点的P2P协议采用的是基于Gossip的数据分发，以同一组织为传播范围来同步数据，提升网络传输的效率。

（3）链码服务

智能合约的实现依赖于安全的执行环境，确保安全的执行过程和用户数据的隔离。Hyperledger Fabric采用Docker管理普通的链码，提供安全的沙箱环境和镜像文件仓库。其好处是容易支持多种语言的链码，扩展性很好。Docker的方案也有自身的问题，比如对环境要求较高，占用资源较多，性能不高等，实现过程中也存在与Kubernetes、Rancher等平台的兼容性问题。

（4）安全和密码服务

安全问题是企业级区块链关心的问题，尤其在关注国家安全的项目中。其中底层的密码学支持尤其重要，Hyperledger Fabric 1.0专门定义了一个BCCSP（BlockChain Cryptographic Service Provider），使其实现密钥生成、哈希运算、签名验签、加密解密等基础功能。BCCSP是一个抽象的接口，默认是软实现的国标算法，目前社区和较多的厂家都在实现国密的算法和HSM（Hardware Security Module）。

Hyperledger Fabric 1.0在架构上的设计具有很好的可扩展性，目前是众多可见的区块链技术中最为活跃的，值得区块链技术爱好者深入研究。

3.2　网络节点架构

节点是区块链的通信主体，是一个逻辑概念。多个不同类型的节点可以运行在同一物理服务器上。有多种类型的节点：客户端、Peer节点、排序服务节点和CA节点。图3-3所示为网络节点架构图。

接下来详细地解释图3-3所示的不同节点的类型。

1.客户端节点

客户端或者应用程序代表由最终用户操作的实体，它必须连接到某一个Peer节点或者排序服务节点上与区块链网络进行通信。客户端向背书节点（Endorser）提交交易提案（Transaction Proposal），当收集到足够背书后，向排序服务广播交易，进行排序，生成区块。

2.Peer节点

所有的Peer节点都是记账节点（Committer），负责验证从排序服务节点区块里的交易，维护状态数据和账本的副本。部分节点会执行交易并对结果进行签名背书，充当背书节点的角色。背书节点是动态的角色，是与具体链码绑定的。每个链码在实例化的时候都会设置背书策略，指定哪些节点对交易背书后才是有效的。也只有在应用程序向它发起交易背书请求的时候才是背书节点，其他时候就是普通的记账节点，只负责验证交易并记账。

图3-3　网络节点架构图

图3-2所示的Peer节点还有一种角色是主节点（Leader Peer），代表的是和排序服务节点通信的节点，负责从排序服务节点处获取最新的区块并在组织内部同步。可以强制设置为主节点，也可以动态选举产生。

在图3-2中还可以看到，有的节点同时是背书节点和记账节点，也可以同时是背书节点、主节点和记账节点，也可以只是记账节点。在后面的章节中，有的地方会用记账节点代表普通的Peer节点。

3.排序服务节点

排序服务节点（Ordering Service Node或者Orderer）接收包含背书签名的交易，对未打包的交易进行排序生成区块，广播给Peer节点。排序服务提供的是原子广播（Atomic Broadcast），保证同一个链上的节点接收到相同的消息，并且有相同的逻辑顺序。

排序服务的多通道（MultiChannel）实现了多链的数据隔离，保证只有同一个链的Peer节点才能访问链上的数据，保护用户数据的隐私。

排序服务可以采用集中式服务，也可以采用分布式协议。可以实现不同级别的容错处理，目前正式发布的版本只支持Apache Kafka集群，提供交易排序的功能，只实现CFT（Crash Fault Tolerence，崩溃故障容错），不支持BFT（Byzantine Fault Tolerance，拜占庭容错）。

4.CA节点

CA节点是Hyperledger Fabric 1.0的证书颁发机构（Certificate Authority），由服务器和客户端组件组成。CA节点接收客户端的注册申请，返回注册密码用于用户登录，以便获取身份证书。在区块链网络上所有的操作都会验证用户的身份。CA节点是可选的，可以用其他成熟的第三方CA颁发证书。

3.3　典型交易流程

图3-4所示为Hyperledger Fabric 1.0典型的交易流程图。

图3-4　交易流程总图

从上一节的网络节点架构中，我们已经了解到基于Hyperledger Fabric 1.0的区块链应用中涉及几个节点角色：应用程序、背书节点、排序服务节点和主节点。在图3-4中，假定各节点已经提前颁发好证书，且已正常启动，并加入已经创建好的通道。后面的步骤介绍在已经实例化了的链码通道上从发起一个调用交易到最终记账的全过程。

3.3.1　创建交易提案并发送给背书节点

使用应用程序构造交易提案，SignedProposal的结构如下所示：

SignedProposal: {
ProposalBytes(Proposal): {
Header: {
ChannelHeader: {
Type: "HeaderType_ENDORSER_TRANSACTION",
TxId: TxId,
Timestamp: Timestamp,
ChannelId: ChannelId,
Extension(ChaincodeHeaderExtension): {
PayloadVisibility: PayloadVisibility,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
}
},
Epoch: Epoch
},
SignatureHeader: {
Creator: Creator,
Nonce: Nonce
}
},
Payload: {
ChaincodeProposalPayload: {
Input(ChaincodeInvocationSpec): {
ChaincodeSpec: {
Type: Type,
ChaincodeId: {
Name: Name
},
Input(ChaincodeInput): {
Args: []
}
}
},
TransientMap: TransientMap
}
}
},
Signature: Signature
}

我们来看看上面的结构，SignedProposal是封装了Proposal的结构，添加了调用者的签名信息。背书节点会根据签名信息验证其是否是一个有效的消息。Proposal由两个部分组成：消息头和消息结构。消息结构详细的解释参考后面的章节。这里简单讲一下消息头。

消息头（Header）也包含两项内容。

1）通道头（ChannelHeader）：通道头包含了与通道和链码调用相关的信息，比如在哪个通道上调用哪个版本的链码。TxId是应用程序本地生成的交易号，跟调用者的身份证书相关，可以避免交易号的冲突，背书节点和记账节点都会校验是否存在重复交易。

2）签名头（SignatureHeader）：签名头包含了调用者的身份证书和一个随机数，用于消息的有效性校验。

应用程序构造好交易提案请求后，选择背书节点执行并进行背书签名。背书节点是链码背书策略里指定的节点。有一些背书节点是离线的，其他的背书节点可以拒绝对交易进行背书，也可以不背书。应用程序可以尝试使用其他可用的背书节点来满足策略。应用程序以何种顺序给背书节点发送背书请求是没有关系的，正常情况下背书节点执行后的结果是一致的，只有背书节点对结果的签名不一样。

3.3.2　背书节点模拟交易并生成背书签名

背书节点在收到交易提案后会进行一些验证，包括：

·交易提案的格式是否正确；

·交易是否提交过（重复攻击保护）；

·交易签名有效（通过MSP）；

·交易提案的提交者在当前通道上是否已授权有写权限。

验证通过后，背书节点会根据当前账本数据模拟执行链码中的业务逻辑并生成读写集（RwSet），其中包含响应值、读写集等。在模拟执行时账本数据不会更新。而后背书节点对这些读写集进行签名成为提案响应（Proposal Response），然后返回给应用程序。ProposalResponse的结构如下：

ProposalResponse: {
Version: Version,
Timestamp: Timestamp,
Response: {
Status: Status,
Message: Message,
Payload: Payload
},
Payload(ProposalResponsePayload): {
ProposalHash: ProposalHash,
Extension(ChaincodeAction): {
Results(TxRwSet): {
NsRwSets(NsRwSet): [
NameSpace: NameSpace,
KvRwSet: {
Reads(KVRead): [
Key: Key,
Version: {
BlockNum: BlockNum,
TxNum: TxNum
}
],
RangeQueriesInfo(RangeQueryInfo): [
StartKey: StartKey,
EndKey: EndKey,
ItrExhausted: ItrExhausted,
ReadsInfo: ReadsInfo
],
Writes(KVWrite): [
Key: Key,
IsDelete: IsDelete,
Value: Value
]
}
]
},
Events(ChaincodeEvent): {
ChaincodeId: ChaincodeId,
TxId: TxId,
EventName: EventName,
Payload: Payload
}
Response: {
Status: Status,
Message: Message,
Payload: Payload
},
ChaincodeId: ChaincodeId
}
},
Endorsement: {
Endorser: Endorser,
Signature: Signature
}
}

返回的ProposalResponse中包含了读写集、背书节点签名以及通道名称等信息，更多字段的详细解释参考3.4节。

背书节点接收消息后执行的详细过程请参考第9章的相关内容。

3.3.3　收集交易的背书

应用程序收到ProposalResponse后会对背书节点签名进行验证，所有节点接收到任何消息后都是需要先验证消息合法性的。如果链码只进行账本查询，应用程序会检查查询响应，但不会将交易提交给排序服务节点。如果链码对账本进行Invoke操作，则须提交交易给排序服务进行账本更新，应用程序会在提交交易前判断背书策略是否满足。如果应用程序没有收集到足够的背书就提交交易了，记账节点在提交验证阶段会发现交易不能满足背书策略，标记为无效交易。

如何选择背书节点呢？目前fabric-sdk-go默认的实现是把配置文件选项channels.mychannel.peers（其中的mychannel需要替换成实际的通道名称）里的节点全部添加为背书节点，需要等待所有背书节点的背书签名。应用程序等待每个背书节点执行的超时时间是通过配置文件选项client.peer.timeout.connection设置的，配置文件的示例给出的是3秒，根据实际情况调整，如果没有设置就是5秒的默认值。

3.3.4　构造交易请求并发送给排序服务节点

应用程序接收到所有的背书节点签名后，根据背书签名调用SDK生成交易，广播给排序服务节点。生成交易的过程比较简单，确认所有的背书节点的执行结果完全一致，再将交易提案、提案响应和背书签名打包生成交易。交易的结构如下：

Envelope: {
Payload: {
Header: {
ChannelHeader: {
Type: "HeaderType_ENDORSER_TRANSACTION",
TxId: TxId,
Timestamp: Timestamp,
ChannelId: ChannelId,
Extension(ChaincodeHeaderExtension): {
PayloadVisibility: PayloadVisibility,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
}
},
Epoch: Epoch
},
SignatureHeader: {
Creator: Creator,
Nonce: Nonce
}
},
Data(Transaction): {
TransactionAction: [
Header(SignatureHeader): {
Creator: Creator,
Nonce: Nonce
},
Payload(ChaincodeActionPayload): {
ChaincodeProposalPayload: {
Input(ChaincodeInvocationSpec): {
ChaincodeSpec: {
Type: Type,
ChaincodeId: {
Name: Name
},
Input(ChaincodeInput): {
Args: []
}
}
},
TransientMap: nil
},
Action(ChaincodeEndorsedAction): {
Payload(ProposalResponsePayload): {
ProposalHash: ProposalHash,
Extension(ChaincodeAction): {
Results(TxRwSet): {
NsRwSets(NsRwSet): [
NameSpace: NameSpace,
KvRwSet: {
Reads(KVRead): [
Key: Key,
Version: {
BlockNum: BlockNum,
TxNum: TxNum
}
],
RangeQueriesInfo(RangeQueryInfo): [
StartKey: StartKey,
EndKey: EndKey,
ItrExhausted: ItrExhausted,
ReadsInfo: ReadsInfo
],
Writes(KVWrite): [
Key: Key,
IsDelete: IsDelete,
Value: Value
]
}
]
},
Events(ChaincodeEvent): {
ChaincodeId: ChaincodeId,
TxId: TxId,
EventName: EventName,
Payload: Payload
}
Response: {
Status: Status,
Message: Message,
Payload: Payload
},
ChaincodeId: ChaincodeId
}
},
Endorsement: [
Endorser: Endorser,
Signature: Signature
]
}
}
]
}
},
Signature: Signature
}

我们来看交易信封的几个对应关系：

·Envelope.Payload.Header同交易提案SignedProposal.Proposal.Header；

·Envelope.Payload.Data.TransactionAction.Header是交易提案的提交者的身份信息，同SignedProposal.Proposal.Header.SignatureHeader和Envelope.Payload.Header.SignatureHeader是冗余的；

·Envelope.Payload.Data.TransactionAction.Payload.ChaincodeProposalPayload同交易提案的SignedProposal.Proposal.Payload.ChaincodeProposalPayload，唯一不同的是，TransientMap强制设置为nil，目的是避免在区块中出现一些敏感信息；

·Envelope.Payload.Data.TransactionAction.Payload.Action.Payload结构，其实和Proposal Response.Payload结构完全一样；

·Envelope.Payload.Data.TransactionAction.Payload.Action.Endorsement变成了数组，代表多个背书节点的背书签名。

整个信封Envelope的Signature是交易提交者对整个Envelope.Payload的签名。应用程序可以把生成的交易信封内容发送给任意选择的几个排序服务节点。

3.3.5　排序服务节点以对交易进行排序并生成区块

排序服务不读取交易的内容，如果在生成交易信封内容的时候伪造了交易模拟执行的结果，排序服务节点也不会发现，但会在最终的交易验证阶段校验出来并标记为无效交易。排序服务要做得很简单，先是接收网络中所有通道发出的交易信息，读取交易信封的Envelope.Payload.Header.ChannelHeader.ChannelId以获取通道名称，按各个通道上交易的接收时间顺序对交易信息进行排序，生成区块，详细的流程请参考第6章。

3.3.6　排序服务节点以广播给组织的主节点

排序服务节点生成区块以后会广播给通道上不同组织的主节点，详细的流程请参考第4章。

3.3.7　记账节点验证区块内容并写入区块

背书节点是动态角色，只要参与交易的背书就是背书节点，哪些交易选择哪些节点作为背书节点是由应用程序选择的，这需要满足背书策略才能生效。所有的背书节点都属于记账节点。所有的Peer节点都是记账节点，记录的是节点已加入通道的账本数据。记账节点接收到的是排序服务节点生成的区块，验证区块交易的有效性，提交到本地账本后再产生一个生成区块的事件，监听区块事件的应用程序可以进行后续的处理。如果接收到的区块是配置区块，则会更新缓存的配置信息。记账节点的处理流程如图3-5所示。

图3-5　记账节点的流程图

1.交易数据的验证

区块数据的验证是以交易验证为单位的，每次对区块进行验证时都会生成一个交易号的位图TxValidationFlags，它记录每个交易号的交易验证状态，只有状态为TxValidationCode_VALID才是有效的。位图也会写入到区块的元数据BlockMetadataIndex_TRANSACTIONS_FILTER中。交易验证的时候会检查以下内容：

·是否为合法的交易：交易格式是否正确，是否有合法的签名，交易内容是否被篡改；

·记账节点是否加入了这个通道。

基本的验证通过以后会提交给VSCC进行背书策略的验证。

2.记账节点与VSCC

链码的交易是隔离的，每个交易的模拟执行结果读写集TxRwSet都包含了交易所属的链码。为了避免错误地更新链码交易数据，在交易提交给系统链码VSCC验证交易内容之前，还会对链码进行校验。下面这些交易都是非法的：

·链码的名称或者版本为空；

·交易消息头里的链码名称Envelope.Payload.Header.ChannelHeader.Extension.ChaincodeId.Name和交易数据里的链码名称Envelope.Payload.Data.TransactionAction.Payload.ChaincodeProposalPayload.Input.ChaincodeSpec.ChaincodeId.Name不一致；

·链码更新当前链码数据时，生成读写集的链码版本不是LSCC记录的最新版本；

·应用程序链码更新了LSCC（生命周期管理系统链码）的数据；

·应用程序链码更新了不可被外部调用的系统链码的数据；

·应用程序链码更新了不可被其他链码调用的系统链码的数据；

·调用了不可被外部调用的系统链码。

更多系统链码的介绍，请参考9.3节。

3.基于状态数据的验证和MVCC检查

交易通过VSCC检查以后，就进入记账流程。kvledger还会对读写集TxRwSet进行MVCC（Multi-Version Concurrency Control）检查。

kvledger实现的是基于键值对（key-value）的状态数据模型。对状态数据的键有3种操作：

·读状态数据；

·写状态数据；

·删除状态数据。

对状态数据的读操作有两种形式：

·基于单一键的读取；

·基于键范围的读取。

MVCC检查只对读数据进行校验，基本逻辑是对模拟执行时状态数据的版本和提交交易时状态数据的版本进行比较。如果数据版本发生变化或者某个键的范围数据发生变化，就说明这段时间之内有别的交易改变了状态数据，当前交易基于原有状态的处理就是有问题的。由于交易提交是并行的，所以在交易未打包生成区块之前，并不能确定最终的执行顺序。如果交易执行的顺序存在依赖，在MVCC检查的时候就会出现依赖的状态发生了变化，实际上是数据出现了冲突。图3-6所示为基于状态的数据验证的流程图。

写集合本身包含了写和删除的数据，有一个状态位标识是否删除数据。为了提升效率，状态数据库的提交是批处理的，整个区块交易的状态数据同时提交，这也保证了整个区块的状态数据要么都提交成功，要么都提交失败。这时只会出现记录的账本数据和状态数据库不一致，不会出现区块的状态数据不一致的情况。当账本数据和状态数据库不一致时，可以通过状态数据库的检查点来标记。

图3-6　基于状态的数据验证的流程图

4.无效交易的处理

伪造的交易会导致无效交易，正常的交易也可能出现无效交易。MVCC检查的是背书节点在模拟执行的时候，环境是否和记账节点提交交易时的环境一致，这里的环境是指状态数据库里数据的三元组（key、value、version）是否完全一致。如果正常提交的交易在这个过程中涉及的数据发生了变化，那么也会出现检查失败从而导致无效交易。在这种情况下，需要在上层的应用程序有一些补偿措施，比如调整交易打包的配置，重新提交失败的交易等。

在目前版本的实现中，无效交易也会保留在区块中，可以通过区块记录的元数据确定哪些是无效交易。无效交易的读写集不会提交到状态数据库中，不会导致状态数据库发生变化，只是会占用区块的大小，占用记账节点的硬盘空间。后续的版本会实现账本的精简，过滤掉无效交易。

3.3.8　在组织内部同步最新的区块

主节点在组织内部同步区块的过程详见第4章的相关内容。

3.4　消息协议结构

3.4.1　信封消息结构

信封消息是认证内容中最基本的单元。它由一个消息负载（Payload）和一个签名（Signature）组成。

// 信封包含一个带有签名的负载，以便认证该消息
message Envelope {
// 编组的负载
bytes payload = 1;

// 负载头中指定创建者签名
bytes signature = 2;
}
// 负载是消息内容（允许签名）
message Payload {
// 负载头部，提供身份验证并防止重放
Header header = 1;

// 数据，其编码由头的类型定义
bytes data = 2;
}

负载包含：

1）消息头部。头部带有类型，描述负载的性质以及如何解组数据字段。此外，头部还包含创建者的信息和随机数，以及用来标识时间逻辑窗口的时期信息。只有在两个条件都成立的情况下，Peer节点才能接受一个信封。

①消息中指定的时期信息是当前窗口期；

②该负载在该周期内只看到一次（即没有重放）。

2）数据字段的类型由头部指定。头部消息的组织方式如下所示：

message Header {
bytes channel_header = 1;
bytes signature_header = 2;
}
// 通道头是一个通用的预防重放和身份标识的消息，它包含在一个被签名的负载之中
message ChannelHeader {
int32 type = 1; // 头类型0-10000由HeaderType保留和定义

// 版本指示消息协议版本
int32 version = 2;

// 时间戳是发件人创建消息的本地时间
google.protobuf.Timestamp timestamp = 3;

// 该消息绑定通道的标识符
string channel_id = 4;

// 端到端使用唯一的标识符
// - 由较高层设置，如最终用户或SDK
// - 传递给背书节点（将检查唯一性）
// - 当消息正确传递时，它将被记账节点检索（也会检查唯一性）
// - 存储于账本中
string tx_id = 5;

// 时期信息基于区块高度而定义，此字段标识时间的逻辑窗口
// 只有在两个条件都成立的情况下，对方才接受提案响应
// 1. 消息中指定的时期信息是当前时期
// 2. 该消息在该时期内只看到一次（即没有重放）
uint64 epoch = 6;

// 根据消息头类型附加的扩展
bytes extension = 7;
}
enum HeaderType {
MESSAGE = 0; // 非透明消息
CONFIG = 1; // 通道配置
CONFIG_UPDATE = 2; // 通道配置更新
ENDORSER_TRANSACTION = 3; // SDK提交背书
ORDERER_TRANSACTION = 4; // 排序管理内部使用
DELIVER_SEEK_INFO = 5; // 指示Deliver API查找信息
CHAINCODE_PACKAGE = 6; // 链码打包安装
}
message SignatureHeader {
// 消息的创建者，链的证书
bytes creator = 1;

// 只能使用一次的任意数字，可用于检测重放攻击
bytes nonce = 2;
}

信封消息结构对于验证负载的签名是必要的。否则，对于大载荷消息，就必须连接所有的载荷再进行签名验证，这往往成本很高。

经过排序后，批量的信封消息交付给记账节点进行验证，通过验证后的数据被记录到账本之中。

3.4.2　配置管理结构

区块链有与之相关的配置，配置设置在创世区块之中，但可能在后续被修改。该配置信息在类型为CONFIGURATION_TRANSACTION的信封消息中编码。配置信息本身就是区块的一个单独交易。配置信息交易没有任何依赖，所以每个配置信息交易必须包含对于链的全量数据，而不是增量数据。使用全量数据更容易引导新的peer或排序节点，也便于未来进行裁剪工作。

CONFIGURATION_TRANSACTION类型的信封消息具有ConfigurationEnvelope类型的负载数据。它定义为：

message ConfigurationEnvelope {
repeated SignedConfigurationItem Items = 3;
}

配置信息的信封消息有与之关联的序列号和链ID。每次修改配置序列号必须递增，这可以作为防止重放攻击的一个简单机制。配置信息的信封中会嵌入一系列的SignedConfigurationItems，定义如下：

message SignedConfigurationItem {
bytes ConfigurationItem = 1;
repeated Envelope Signatures = 2;
}

因为SignedConfigurationItem必须支持多个签名，所以它包含一组重复的信封消息。这些消息中每个都有一个类型为CONFIGURATION_ITEM的头部。负载的数据部分在ConfigurationItem中保存，定义为：

message ConfigurationItem {
enum ConfigurationType {
Policy = 0;
Chain = 1;
Orderer = 2;
Fabric = 3;
}
bytes ChainID = 1;
uint64 LastModified = 2;
ConfigurationType Type = 3;
string ModificationPolicy = 4;
string Key = 5;
bytes Value = 6;
}

Type提供了配置项的范围和编码信息。LastModified字段设置为上一次配置项被修改时配置信息信封中的序列号。ModificationPolicy指向一个已经命名的策略，用来对将来的签名进行评估，以确定修改是否被授权。Key和Value字段分别用作引用配置项及其内容。

修改配置包含以下内容：

·检索现有配置；

·递增配置信息信封消息中的序列号；

·修改所需的配置项，将每个配置项的LastModified字段设置为递增后的序列号；

·更新SignedConfigurationItem中的签名信息；

·将签名后的信封信息提交给排序节点。

配置管理员将验证：

·所有配置项和信封都指向正确的链；

·添加或修改了哪些配置项；

·有没有现有的配置项被忽略（即提交的数据是全集）；

·所有配置更改的LastModification都等于信封消息中的序列号；

·所有配置更改均符合相应的修改策略。

任何有权更改配置项的角色都可以构建新的配置信息交易。修改配置项将更新序列号并产生新的创始区块，这将引导新加入网络的各种节点。

3.4.3　背书流程结构

当Envelope.data中携带与链码相关的消息时，使用ENDORSER_TRANSACTION类型。

获得批准的ENDORSER_TRANSACTION负载流程如下。

首先，客户端向所有相关的背书节点发送提案消息（提案基本上是要进行一些影响账本的动作）。

然后，每个背书节点向客户端发送一个提案响应消息。提案响应包含背书结果的成功/错误码、应答负载和签名。应答负载之中包含提案的哈希值信息，用此信息可以将提案和针对该提案的应答安全地连接起来。

最后，客户端将背书结果写入交易中，签名并发送到排序服务。

在接下来的章节中，我们将详细介绍消息及其流程。

1.交易提案结构

一个提案消息包含头部（包含描述它的一些元数据，例如类型、调用者的身份、时间、链的ID、加密的随机数）和不透明的负载：

message SignedProposal {
// 提案
bytes proposal_bytes = 1;
// 对提案进行签名，该签名将和头部的创建者标识进行验证
bytes signature = 2;
}
message Proposal {
// 提案头部
bytes header = 1;
// 提案负载，具体结构由头部的类型决定
bytes payload = 2;
// 提案的可选扩展。对于CHAINCODE类型的消息，其内容可能是ChaincodeAction消息
bytes extension = 3;
}

一个提案发送给背书节点进行背书。该提案包含：

·头部，可以解组为头部信息；

·负载，由头部的类型决定；

·扩展，由头部的类型决定。

和信封消息结构类似，这种SignedProposal消息结构也是重要的。否则，对于大载荷消息，我们必须连接所有的载荷再进行签名验证，这往往成本很高。

当背书节点收到签名后的提案消息后，它将验证消息中的签名。验证签名需要以下步骤。

1）预验证用户生成签名证书的有效性。一旦SignedProposal.proposal_bytes和Proposal.header都解组成功，就可以认为证书基本是可用的。虽然这种在证书验证前的解组操作可能并不太理想，但是在这个阶段可以过滤掉证书过期的情况。

2）验证证书是否可信（证书是否由受信任的CA签名），并允许交易（能否通过ACL检查）。

3）验证SignedProposal.proposal_bytes上的签名是否有效。

4）检测重放攻击。

以下是当ChainHeader的类型为ENDORSER_TRANSACTION时的消息：

message ChaincodeHeaderExtension {
// 控制提案的负载在最终交易和账本中的可见程度
bytes payload_visibility = 1;
// 要定位的链代码ID
ChaincodeID chaincode_id = 2;
}

ChaincodeHeaderExtension消息用于指定要调用的链码以及应在账本中呈现的内容。理想情况下，payload_visibility是可配置的，支持至少3种主要可见性模式：

·负载所有字节都可见；

·只有负载的哈希值可见；

·任何东西都不可见。

注意，可见性功能可能也会由ESCC设置，此时本字段将会被覆盖。另外本字段也将影响ProposalResponsePayload.proposalHash的内容。

message ChaincodeProposalPayload {
// 包含调用链码的参数，
bytes input = 1;
// 用于实现某些应用程序级的加密数据
map<string, bytes> TransientMap = 2;
}

ChaincodeProposalPayload消息包含调用链码的参数。TransientMap字段的内容应始终从信封消息中省略掉，并不记录到账本之中。

message ChaincodeAction {
// 调用链码产生的读/写集
bytes results = 1;
// 调用链码产生的事件
bytes events = 2;
// 调用链码的结果
Response response = 3;
// 含链ID、背书节点在模拟执行提案时设置
// 账本节点将验证版本号是否与链码最新版本匹配，含有链ID信息将支持单个交易打开多个链码
ChaincodeID chaincode_id = 4;
}

ChaincodeAction消息包含执行链码所产生的动作和事件。results字段包含读取集合，events字段包含由链码执行生成的事件。

2.提案响应结构

提案响应消息从背书节点返回给提案客户端。背书节点使用该消息表达对于交易提案的处理结果。应答结果可能是成功也可能是失败，另外还会包含动作描述和背书节点的签名。如果足够数量的背书节点同意相同的动作并进行签名，则可以生成负载消息，并发送给排序节点。

message ProposalResponse {
// 消息协议版本
int32 version = 1;
// 消息创建时间，由消息发送者定义
google.protobuf.Timestamp timestamp = 2;
// 某个动作的背书是否成功
Response response = 4;
// 负载，ProposalResponsePayload字节序列
bytes payload = 5;
// 提案的具体背书内容，基本上就是背书节点的签名
Endorsement endorsement = 6;
}
message ProposalResponsePayload {
// 触发此应答交易提案的哈希值
bytes proposal_hash = 1;
// 扩展内容，应该解组为特定类型的消息
bytes extension = 2;
}
message Endorsement {
// 背书节点身份(例如，证书信息)
bytes endorser = 1;
// 签名，对提案应答负载和背书节点证书这两个内容进行签名
bytes signature = 2;
}

ProposalResponsePayload消息是提案响应的负载部分。这个消息是客户端请求和背书节点动作之间的“桥梁”。对于链码来说，它包含一个表示提议的哈希值proposal_hash，以及表示链码状态变化和事件extension字段。

proposal_hash字段将交易提案和提案响应两者对应起来，即为了实现异步系统的记账功能也为了追责和抗抵赖的安全诉求。哈希值通常会覆盖整个提案消息的所有字节中。但是，这样实现就意味着只有获得完整的提案消息才能验证哈希值的正确性。

出于保密原因，使用链码不太可能将提案的负载直接存储在账本中。例如，类型为ENDORSER_TRANSACTION的消息，需要将提案的头部和负载分开进行处理：头部总是进行完整散列的，而负载则可能进行完整散列或对哈希值再进行散列，或者根本不进行散列。

3.背书交易结构

客户端获得足够的背书后，可以将这些背书组合成一个交易信息。这个交易信息可以设置为负载信息的数据字段。以下是在这种情况下要使用的具体消息：

message Transaction {
// 负载是一个TransactionAction数组，每个交易需要一个数组来适应多个动作
repeated TransactionAction actions = 1;
}

message TransactionAction {
// 提案头部
bytes header = 1;
// 负载由头部类型决定，它是ChaincodeActionPayload字节序列
bytes payload = 2;
}

TransactionAction消息将提案绑定到其动作。它的头部是SignatureHeader消息，它的负载是ChaincodeActionPayload消息。

message ChaincodeActionPayload {
// ChaincodeProposalPayload消息的字节序列，内容来自链码原始调用的参数
bytes chaincode_proposal_payload = 1;
// 应用于账本的动作列表
ChaincodeEndorsedAction action = 2;
}

ChaincodeActionPayload消息携带chaincodeProposalPayload和已经通过背书的动作以应用于账本。主要的可见性模式是“full”（整个ChaincodeProposalPayload消息包含在这里）、“hash”（仅包含ChaincodeProposalPayload消息的哈希值）或“nothing”。该字段将用于检查ProposalResponsePayload.proposalHash的一致性。此外，action字段包含应用于账本的动作列表。

message ChaincodeEndorsedAction {
// 由背书节点签名的ProposalResponsePayload消息字节序列
bytes proposal_response_payload = 1;
// 提案背书，基本上是背书节点的签名
repeated Endorsement endorsements = 2;
}

ChaincodeEndorsedAction消息承载有关具体提案的背书信息。proposalResponsePayload是由背书节点签名的，对于ENDORSER_TRANSACTION类型，ProposalResponsePayload的extenstion字段会带有一个ChaincodeAction。此外，endorsements字段包含提案已经收到的背书信息。

3.5　策略管理和访问控制

在Hyperledger Fabric 1.0中，较多的地方都使用策略进行管理，它是一种权限管理的方法，包括交易背书策略、链码的实例化策略、通道管理策略等。

3.5.1　策略定义及其类型

策略定义了一些规则，验证签名数据是否符合定义的条件，结果为TRUE或者FALSE。策略的定义如下：

type Policy struct {
Type int32 // 策略的类型
Value []byte // 策略的内容
}

策略的类型有两种。

1）SignaturePolicy：在基于验证签名策略的基础上，支持条件AND、OR、NOutOf的任意组合，其中的NOutOf指的是满足m个条件中的n个就表示满足策略（m≥n）。比如OR（Org1.Admin，NOutOf（2，Org2.Member））表示Org1的管理员或者两个Org2的成员签名都满足策略。

2）ImplicitMetaPolicy：隐含的元策略，是在SignaturePolicy之上的策略，支持大多数的组织管理员这种策略，只适用于通道管理策略。

SignaturePolicy实际只有两种类型，SignedBy和NOutOf，其他的，比如AND和OR都会转换成NOutOf类型。其定义如下：

type SignaturePolicy struct {
// 支持的类型有：
// *SignaturePolicy_SignedBy,验证单个签名是否正确
// *SignaturePolicy_NOutOf_,验证是否有n个签名都正确
Type isSignaturePolicy_Type `protobuf_oneof:"Type"`
}

ImplicitMetaPolicy是递归策略的定义方法，名称中的Implicit说明规则是由子策略生成的，Meta说明策略依赖其他策略的验证结果。

type ImplicitMetaPolicy struct {
SubPolicy string // 子策略的名称
Rule ImplicitMetaPolicy_Rule // 策略的规则
}

策略的规则支持3种形式：

·ImplicitMetaPolicy_ANY：任意一个子规则成立就满足策略；

·ImplicitMetaPolicy_ALL：全部子规则都成立才满足策略；

·ImplicitMetaPolicy_MAJORITY：大多数的子规则成立就满足策略。

特别说明ImplicitMetaPolicy_MAJORITY需要满足子规则数的计算方法：

threshold = len(subPolicies)/2 + 1

比如一共有3个子策略，需要至少2个子策略成立才能满足策略。如果总共有4个子策略，需要至少3个子策略成立才能满足策略。如果没有子策略，默认是满足的。

策略的内容可以有多种，下面分别来看几种策略：交易背书策略、链码实例化策略和通道管理策略。

3.5.2　交易背书策略

交易背书策略是对交易进行背书的规则，是跟通道和链码相关的，在链码实例化的时候指定。在链码调用的时候，需要从背书节点收集足够的签名背书，只有通过背书策略的交易才是有效的。这是通过应用程序和背书节点之间的交互来完成的，这在前面的交易流程里已经介绍过了。

1.交易背书策略的验证

背书是由一组签名组成的，每个Peer节点接收到区块时，都能根据交易的内容本地验证背书是否符合背书策略，不需要和其他节点交互。验证交易背书的基本原则是：

1）所有的背书都是有效的，验证消息用有效的证书进行正确的签名；

2）满足背书策略的有效背书数量，转化为NOutOf格式进行比较；

3）背书是期望的背书节点签名的，在背书策略中指定了哪些组织和角色是有效的背书节点。

如何来实现这几个原则的呢？我们先从背书签名的命令行语法开始，背书签名的语法AND和OR都可以转为NOutOf：

·AND（A，B）可以转换为NOutOf（1，A，B）；

·OR（A，B）可以转换为NOutOf（2，A，B）。

我们主要来看下NOutOf如何实现，背书策略的定义如下：

type SignaturePolicyEnvelope struct {
Version int32 // 背书策略版本，默认都是0
Rule *SignaturePolicy // 背书策略规则：签名策略
Identities []*common1.MSPPrincipal // 背书策略主体：MSP主体签名
}

其中，MSP主体（Principal）是基于MSP的身份标识的，有如下几种类型。

1）MSPPrincipal_ROLE：基于MSP角色的验证方法，目前只有admin和member两种。

2）MSPPrincipal_ORGANIZATION_UNIT：基于部门的验证方法，同一个MSP中的不同部门。

3）MSPPrincipal_IDENTITY：基于某个具体身份证书的验证方法，验证签名是否有效。

MSPPrincipal的定义如下：

type MSPPrincipal struct {
PrincipalClassification MSPPrincipal_Classification // MSP的类型
Principal []byte // 根据MSP的类型不同，实体有不同的内容
}

根据不同的MSP类型，主体是不同的。

（1）基于MSP角色的验证

当PrincipalClassification是MSPPrincipal_ROLE时，主体存储的内容如下：

type MSPRole struct {
// MSP标识符
MspIdentifier string
// MSP角色：可选值是MSPRole_MEMBER和MSPRole_ADMIN
Role MSPRole_MSPRoleType
}

不同角色的验证方法如下：

1）MSPRole_MEMBER：验证是否为同一个MSP的有效签名；

2）MSPRole_ADMIN：验证签名者是否是MSP设置好的admin成员。

（2）基于部门的验证

当PrincipalClassification是MSPPrincipal_ORGANIZATION_UNIT时，主体存储的内容如下：

type OrganizationUnit struct {
// MSP标识符
MspIdentifier string
// 组织部门标识符
OrganizationalUnitIdentifier string
// 证书标识符：信任证书链和组织部门信息的哈希
CertifiersIdentifier []byte
}

验证过程的步骤是：

·验证是否为相同的MSP；

·验证是否是有效的证书；

·验证组织部门信息是否匹配。

（3）基于身份证书的验证

当PrincipalClassification是MSPPrincipal_IDENTITY时，主体存储的内容如下：

type identity struct {
// 身份标识符，包含MSP标识符和身份编号
id *IdentityIdentifier
// 身份的数字证书，包含了对公钥的签名
cert *x509.Certificate
// 身份的公钥
pk bccsp.Key
// 身份的MSP信息
msp *bccspmsp
}

这样验证MSP是否是有效证书就可以了。

2.命令行的背书策略语法

在命令行里，可以用一种简单的语言，根据主体的布尔表达式来表示策略。主体是用MSP来表示的，用来验证签名者的标识和签名者在MSP里的角色。目前支持两种角色：member和admin。主体的表示方法是MSP.ROLE，其中MSP是MSP的标识，ROLE可以是memeber也可以是admin。这都是有效的主体：Org0.admin表示由MSP标识Org0的任何一个管理员，Org1.memeber表示由MSP标识Org1的任何一个成员。

其语法是：EXPR（E[，E...]），其中EXPR可以是AND也可以是OR，E可以为一个主体，也可以为嵌套的EXPR。比如：

1）AND（'Org1.member'，'Org2.member'，'Org3.member'）要求3个MSP标识Org1、Org2和Org3，其中每个MSP都有1个成员有1个签名；

2）OR（'Org1.member'，'Org2.member'）要求2个MSP标识Org1、Org2，其中任何1个成员有1个签名；

3）OR（'Org1.member'，AND（'Org2.member'，'Org3.member'））要求MSP标识Org1的成员有1个签名，或者MSP标识Org2和Org3的成员都有1个签名。

目前在命令行的语法中，背书策略只支持AND和OR两种，并不支持更为复杂的NOutOf。这部分的设计在后续内容中也会有调整。SDK对背书策略都会转换成NOutOf语法，不过不是所有的SDK都支持。比如目前fabric-sdk-go提供的默认接口不支持NOutOf语法，但其内部是支持的，稍加改动很容易就能支持。详细可以参考cauthdsl_builder.go文件。

3.给链码指定背书策略

背书策略可以在部署的时候用-P参数指定，后面是具体的背书策略。比如：

peer chaincode deploy -C testchainid -n mycc -p github.com/hyperledger/fabric/
examples/chaincode/go/chaincode_example02 -c '{"Args":["init","a","100","b","200"]}’
-P "AND('Org1.member', 'Org2.member')"

这个命令在链testchainid上部署链码mycc，背书策略是AND（'Org1.member'，'Org2.member'）。如果命令行里没有指定策略，那么默认的背书策略要求MSP标识DEFAULT成员的一个签名。

3.5.3　链码实例化策略

链码实例化策略是用来验证是否有权限进行链码实例化和链码升级的。链码实例化策略是在对链码打包和签名的时候指定的，如果没有指定实例化策略，默认是通道的管理员才能实例化。

type SignedChaincodeDeploymentSpec struct {
// 链码部署规范
ChaincodeDeploymentSpec []byte
// 链码的实例化策略，结构同背书策略，在实例化的时候验证
InstantiationPolicy []byte
// 链码所有者的签名背书列表
OwnerEndorsements []*Endorsement
}

链码实例化策略的定义和背书策略完全一样，验证方法也相同，只是用途和用法不一样。链码实例化策略是直接从链码打包中获取的，实例化完成后会将策略存放在链上。在链码实例化和升级的时候会先验证是否符合当前的实例化策略，验证通过才可以更新链码实例化策略。存储在链上的链码信息结构如下所示：

type ChaincodeData struct {
// 链码名称
Name string
// 链码版本
Version string
// 链码的ESCC
Escc string
// 链码的VSCC
Vscc string
// 链码的背书策略
Policy []byte
// 链码的内容：包含链码的名称、版本、链码源码哈希、链码名称和版本的元数据哈希等内容
// 不包含链码源码
Data []byte
// 链码指纹标识，目前没有使用
Id []byte
// 链码实例化策略
InstantiationPolicy []byte
}

链码信息结构ChaincodeData在链上是按链码的名称索引的。

3.5.4　通道管理策略

通道配置是递归定义的：

type ConfigGroup struct {
Version uint64 // 配置版本
Groups map[string]*ConfigGroup // 子配置
Values map[string]*ConfigValue // 配置值
Policies map[string]*ConfigPolicy // 配置策略定义
ModPolicy string // 配置修改策略的名称
}

其中，配置值ConfigValue定义的是一些配置数据，定义如下：

type ConfigValue struct {
Version uint64 // 配置版本
Value []byte // 配置数据，可以是JSON结构的
ModPolicy string // 配置修改策略名称
}

比如在通道配置中区块生成间隔BatchTimeout设置的值是“2s”，局部的格式如下：

"BatchTimeout": {
"mod_policy": "Admins",
"value": {
"timeout": "2s"
}
}

我们再来看最重要的配置策略的定义：

type ConfigPolicy struct {
Version uint64 // 配置策略版本
Policy *Policy // 配置策略的内容，这在前面已经介绍过
ModPolicy string // 配置策略中修改策略的名称
}

从上面的定义中我们可以看到，配置策略是基于SignaturePolicy和ImplicitMetaPolicy的，ModPolicy代表的是修改同级策略用到的策略名称。通道定义了3种配置策略，如表3-1所示。

表3-1　3种配置策略

1.通道配置的递归定义

我们来看一个简化的通道配置是如何递归定义的。

Channel:
Policies:
Readers
Writers
Admins
Groups:
Orderer:
Policies:
Readers
Writers
Admins
Groups:
OrdereringOrganization1:
Policies:
Readers
Writers
Admins
Application:
Policies:
Readers
-----------> Writers
Admins
Groups:
ApplicationOrganization1:
Policies:
Readers
Writers
Admins
ApplicationOrganization2:
Policies:
Readers
Writers
Admins

在上面的配置中，最外层是Channel，它定义了通道的子配置和策略定义。Channel的子配置里面定义了Orderer和Application配置，它们分别是相同的递归定义结构。其中"------->"显示的一行按照层级展开，代表的是/Channel/Application/Writers。

怎么来使用这些配置策略呢？比如在排序服务节点调用Deliver接口的时候会检查这个节点是否满足/Channel/Readers策略。Peer节点同步区块的时候也会检查是否满足/Channel/Application/Readers策略。

更详细的例子参考第6章的相关内容。

2.通道配置的默认策略

在使用configtxgen工具生成创世区块或者通道配置时，使用的默认策略如表3-2所示。

表3-2　通道配置的默认策略

3.6　本章小结

本章从逻辑结构、节点结构、典型交易流程、消息协议结构、策略管理等几个方面介绍了Hyperledger Fabric 1.0的架构。通过这些内容的介绍，能够基本了解Hyperledger Fabric 1.0的设计原则和思路。

第4章　基于Gossip的P2P数据分发

4.1　概述

背书节点模拟执行签名的结果会经过排序服务（Ordering Service）广播给所有的节点，它提供的是一种原子广播服务（Atomic Broadcast），即在逻辑上所有节点接收到消息的顺序是相同的，相同序号都是相同的内容，排序服务的详细原理和实现请参考第6章。排序服务广播的信息包括更新的状态信息和账本信息等，这些信息需要广播给所有节点。如果排序服务和所有节点都保持直接连接，在节点较多、数据量较大的情况下，容易形成单点故障或成为性能瓶颈。

由超级账本节点组成的区块链网络本身就是一种去中心化的网络，利用P2P实现数据广播是显而易见的做法，最常见的实现方法是洪泛（Flooding）。洪泛是节点在接收到数据包以后直接转发给所有的邻居节点，直到所有的节点都接收到了数据包或者数据包的跳数（Hop Count）超过一定的限制。洪泛有很多优点，比如节点覆盖度高。如果在一个源节点和目标节点之间存在一条路径，洪泛就能通过广播找到这条路径，并且能以最快的速度找到这条路径。洪泛还有很好的冗余度，这在不稳定的网络中能提高网络的健壮性（Robustness）。

洪泛的一个缺点是非常低效，可能会出现广播风暴。超级账本采用基于Gossip的协议实现P2P的数据分发，与洪泛（Flooding）的广播策略不同，节点在接收到数据包以后，不是直接转发给邻居节点，而是会计算一下概率，根据计算结果来判断是否需要进行转发。转发概率设置为固定值的纯Gossip（Pure Gossip）、盲Gossip（Blind Gossiping）或者固定概率Gossip（Fixed Probability Gossip）。转发概率还可以根据其他一些信息动态计算，比如节点的度（Degree）、全局的拓扑结构等。在超级账本的实现中，采用的是随机的选择k（默认值是3）个节点进行转发，如果邻居节点的数量还没有需要转发的节点数量多，就全部转发。

采用基于Gossip的广播策略，除了能提高转发效率外，还有以下一些好处。

1）扩展性（Scalability）：在网络节点数增加的情况下，整个系统的性能不会快速恶化。每个节点选择转发给邻居节点的数量和网络中的总节点数量是没有直接关系的。转发概率是基于本地信息进行计算的，这些信息的变化若较小，整个系统的性能就会比较稳定，系统就是可扩展的。

2）适应性（Adaptability）：在超级账本中，节点定期会跟其他节点交换信息。如果在这个过程中有节点发生故障，则会从存活节点中删除这个节点的信息。对于故障节点，还会定时检查是否已经恢复，恢复存活的节点会更新到存活节点列表中。如果有新加入的节点，也能通过节点信息的交换获取到，添加到存活节点列表中，广播给其他节点。这些都能通过Gossip协议学习到，自动调整网络的拓扑结构，适应网络节点的变化，保证整个网络的正常运行。

3）优雅降级（Graceful Degradation）：在可靠的广播协议中，存在一个值f，如果错误数量没有超过这个值，则整个协议能正常运行；如果超过这个值，就会出现一些异常，或者完全不能工作。协议的可靠性就等于不超过f个错误的概率。若想计算这个概率可能是非常困难的，计算概率所需要的数据也可能非常难以测量。优雅降级（Graceful Degradation）就是指协议能正确工作的概率不会因为错误数超过f时就快速地降低。

4.2　超级账本中的Gossip协议

超级账本的Peer节点组成了一个P2P的网络，客户端SDK（Go、Java、Python、Node.js等不同语言）会提交请求给Peer节点，Peer节点处理后会提交交易提案（Transaction Proposal）给背书节点（Endorser），然后进行背书签名（Endorsement），最后经过排序服务达成共识后广播给Peer节点，如图4-1所示。Gossip模块负责连接排序服务和Peer节点上，实现从单个源节点到所有节点高效的数据分发，在后台实现不同节点间的状态同步，并且可以处理拜占庭问题、动态的节点增加和网络分区。账本信息、状态信息、成员信息等都会通过Gossip协议进行分发。概括起来，Gossip协议主要完成的功能和目标有以下几个。

1）在不需要所有节点都连接到排序服务获取账本区块数据的情况下，超级账本网络中所有节点还能有相同的账本数据、状态信息。

2）系统已经正常运行后，对于新加入到超级账本网络中的节点，可以不直接连接到排序服务就能从网络中其他节点处获取到账本数据、状态信息。

3）那些错过了批量更新的节点（比如由于网络中断或者临时的超负荷运行没有接收到数据），能够保证落后的节点获取到缺失的区块。

4）维护和管理成员信息，跟踪哪些成员是存活的，哪些是有故障的。

5）数据能快速地从单个源节点同步到所有的其他节点上，能够保证大量的数据可以在节点之间进行传输。

图4-1　基于Gossip的通信路径

超级账本网络会通过主节点选举（Leader election）选择一些节点，通过调用Deliver（）接口连接到排序服务，这些节点负责把接收到的批量区块（batch）广播给其他节点。每个组织都会根据自己的需要选择一个节点来连接排序服务，再把批量区块分发给组织内的其他节点。后面会专门阐述主节点选举的过程。

基于Gossip的广播实现的过程是，一个Peer节点接收到消息以后，随机地选择k个节点，把消息发送给它们。如果有节点没有响应，则同步更新发现模块的数据。另外，还有一个基于反熵（Anti-entropy）的状态同步过程，它会在不同节点间同步状态。它们周期性地和其他节点比较信息，保持状态的同步。每个节点都会维护完整的成员信息，这样它们可以在发送信息的时候自行随机地选择节点。在这种模式下，不需要维护固定的连接，因为它是非常健壮的，相对来说这也是非常容易实现的，同时还能处理节点故障和拜占庭问题。

4.3　成员认证及身份管理

在Gossip网络中，每个节点都有超级账本网络认可的MSP（Membership Service Provider）颁发的证书（身份，Identity），从证书计算哈希值导出（目前的实现是直接用Endpoint转换而来的）的一个标识符称为节点的PKI-ID。

身份管理模块管理节点标识符和证书之间的映射，在内部维护了以PKI-ID为键、证书为值的映射表pkiID2Cert，可以通过PKI-ID获取节点的证书，也可以更新节点的证书。同时还内置了MCS（MessageCryptoService）模块，它可以对消息进行签名和验证。更新节点证书的时候也会通过MCS模块验证证书是否有效，检查从证书导出的标识符是否和PKI-ID匹配。

在生产环境中，假设所有节点都是双向TLS部署的，TLS连接的双方都有一个有效的TLS证书。当节点和对端节点第一次连接时，会有一个握手协议，这个协议验证它是否拥有TLS证书的私钥，因而在TLS会话中绑定成员身份。握手是相互的，过程比较简单，握手节点主动发送给对端节点一条ConnEstablish消息，包含的内容有：

·节点TLS证书的哈希值；

·节点的PKI-ID；

·MSP身份证书。

然后，对端节点的处理过程是：

·接收发送过来的ConnEstablish消息；

·提取节点的TLS证书并计算哈希值；

·通过哈希值验证ConnEstablish的签名是否正确。

如果签名验证通过，还会检查PKI-ID是否在黑名单列表中，如果不在黑名单中，就信任这个节点。用PKI-ID作为键值，保存证书到映射表pkiID2Cert中，后续可以验证这个节点发送消息的签名。同时还会用PKI-ID作为键值，创建一个连接并进行关联，并将其保存在内存中。需要和这个节点通信的时候通过PKI-ID快速地找到连接并发送消息。发送的过程是把消息放到一个发送缓冲区中，由一个线程负责把发送缓冲区中的数据发送出去。如果签名验证失败，节点拒绝这次连接。

4.4　节点启动及成员管理

超级账本网络维护着所有节点的信息，包括存活节点和故障节点，包括最近一次检测到它们存活或者掉线的时间，所有节点信息都是以PKI-ID为标识符的。

要想加入到超级账本网络，节点必须至少要知道网络中一个存活节点的地址信息。节点启动的时候会读取配置文件core.yaml，读取bootpeer.gossip.bootstrap字段的值，这个字段可以设置为一个列表，它包含了它可以连接的一些节点，这个列表名为启动集合（Bootstrap Set）。节点会给启动集合中的所有节点发送MembershipRequest消息，其包含的内容是：

1）AliveMessage消息；

2）本地节点已知的存活节点列表。

其中，AliveMessage消息包含的内容有以下几项。

1）PKI-ID；

2）Endpoint（host+“：”+port）；

3）Metadata（字节数组，以后备用）；

4）PeerTime，它又包含以下内容：

·节点转世时间（即启动时间）；

·单调递增的计数器，每次在AliveMessage传播时加1。

5）上面所有字段的签名；

6）节点的证书（可选）。

当一个节点接收到其他节点发送过来的AliveMessage后，首先会调用MCS（MessageCryptoService）模块验证消息的证书和签名，若验证通过，会添加到存活节点列表中，并更新检测到存活的时间。

节点证书只在节点启动后的一段时间内随消息发送，过了这段时间后消息就不再包含证书信息。当一个节点接收到其他节点的消息时，它会用之前收到的这个节点发送的证书进行验证。这就是为什么在节点启动后的一段时间内在AliveMessage中需要包含证书信息，就是为了在没有证书的情况下能够验证这个节点签名的信息。没有接收到其他节点证书的节点，可以通过周期性的数据交换机制来获取缺失的证书。

节点启动以后，每个节点还会周期性地在网络中广播AliveMessage消息，接收到消息的节点会更新本地的存活节点列表。每个节点根据接收到的存活消息判断故障节点。这是最简单的处理拜占庭问题的办法，这里考虑到存活消息是由节点签名的，因而不能被攻击者伪造，其他节点也不能通过发送消息就改变成员的状态。同时每个节点还会周期性地检查本地的存活节点列表，查看是否有节点在相当长的一段时间内没有更新存活状态，如果有节点掉线了，就从存活列表中删除它，添加到掉线节点列表中。节点会周期性地尝试重新连接掉列表中的节点。

4.5　主节点选举过程

主节点选举（Leader election）的用处是，判断在相同组织（Organization）中哪个节点可以作为代表连接排序服务。主节点选举过程是在Gossip层实现的，而且假设在某个时间段内，同时存在多个主节点，以避免拜占庭行为发生。

主节点选择过程是基于组织范围（Scope）内广播的LeadershipMessage消息的，主节点选举的消息有两种类型。

1）一种是参与主节点选举的消息proposal，它在竞争主节点的过程中广播给所有节点的消息。

2）一种声明成为主节点的消息declaration，它通过比较，可申请为主节点的消息。

两种消息的结构是一样的，包含的内容如下所示：

1）节点的PKI-ID。

2）是否声明为主节点。

3）PeerTime，它又包含以下的内容：

·节点转世时间（即启动时间）；

·单调递增的计数器，每次发送一个LeadershipMessage消息时加1。

当一个节点启动的时候，它先等待网络稳定再开始参与主节点选举。网络稳定的等待时间是15秒，如果在等待时间内节点数不再变化或者已经出现了主节点，这就是稳定了。如果在等待过程中没有产生主节点，则参与主节点的选举。参与主节点选举前设置自己的“isLeader”属性为false，广播一个proposal消息给组织内其他的节点。然后节点会设置一个超时时间（5秒），等待proposal消息在组内扩散，其他节点接收到proposal消息会暂存起来。若超时时间到了，还没有出现主节点，则按PKI-ID的字母顺序排序，最靠前的节点（也可以采用其他的算法）标识“isLeader”为true，声明自己为主节点，在组织内广播一个declaration消息。

如果在时间超时（5秒）之前有其他节点声明为主节点，则接受它为主节点。如果同时有多个节点声明为主节点，名称最靠前的节点会成为主节点，其他节点主动放弃，它们标记自己的“isLeader”属性为false。所有的节点都认定在声明为主节点的Peer节点中名字最靠前的节点为主节点。如果主节点掉线，剩余节点里名字最靠前的声明为主节点。如果这时发现有另外一个名称更靠前的节点声明为主节点，则当前节点就会主动放弃。目标是最终只有一个节点设置“isLeader”为true。

一次主节点选举的有效时间leaderAliveThreshold是10s，没有成为主节点的节点清除收到的proposal消息，然后等待有效时间结束，进入下一轮主节点选举的过程。

4.6　基于反熵的状态同步

反熵（Anti-entropy）是指每个节点周期性地和邻居节点交换保存的数据，然后对比本地数据和邻居节点所保存的数据，检查是否有缺失或者过期的数据，然后更新本地节点的数据为最新的数据。

超级账本的反熵实现比较简单，每个节点定期（10s）检查本地账本的区块序列号和其他节点账本的序列号。若发现本地的序列号比网络中其他节点账本的序列号小就在网络中广播一个GossipMessage_StateRequest消息，请求缺失序列号的区块。收到请求的节点如果有对应序列号的区块，会在网络中广播一个GossipMessage_StateResponse消息，使其包含本地账本中请求序列号的区块。这种方式是通过直接消息（directMessage）渠道进行的，节点接收到消息后会缓存起来，放到一个PayloadsBuffer的数据结构里保存起来。

由于TCP/IP网络传输的特点，数据可能不是按序到达的，PayloadsBuffer会在内部保存一个索引，记录等待提交账本的下一个区块的序列号next。如果接收到的区块序列号小于next，说明是过期的区块，直接丢弃。如果序列号大于next，PayloadsBuffer把收到的数据放到序列号对应的缓冲区数组里。只要收到的数据和已提交区块序列号连续了，就会把连续的数据区块提交到账本里，然后删除缓冲区中已提交的数据区块，同时更新索引next。

另外一个更新状态数据的过程是在主节点从排序服务中获取到区块以后，会创建一个类型为GossipMessage_DataMsg的数据消息广播给其他节点，其他节点接收到区块后同样也会和PayloadsBuffer中的next进行比较，进行同样的处理。和直接消息方式不同的地方是，从排序服务获取到的GossipMessage_DataMsg消息只包含一个区块，直接消息里可能会包含多个缺失的区块。

4.7　数据传播过程

数据传播机制（Data dissemination）的底层是由基于Gossip的节点通信支撑的。在这种模式下，不会构建每个节点之间的分离路径（disjoint path）信息，而是每个节点每次都随机性地选择k个节点来扩散消息。节点在某个时间点随机性地选择节点交换信息，信息流就在整个系统中流动起来了。这种交互方式比固定结构的方式更健壮，而且在遇到节点变动或者拜占庭问题的时候更容易维护。而且，固定结构的模式保留了每个节点之间的分离路径信息，消息复杂度会更大。注意，一个节点在随机选择其他节点时，它可以参考当前的成员视图、前一段时间的历史存活节点、启动集合，以及所有节点的列表等信息。

每个节点有更高的出度（Outgoing Degree），不同节点独立选择不同的传播路径，这能够保证当路由中有拜占庭节点的时候不会阻止整个网络接收特定的消息。允许一个节点转发消息给更多的节点，系统自然能在遇到拜占庭节点的时候有更好的健壮性，这样网络中传输的消息会更多。处理拜占庭错误的健壮性与每一轮选择消息的发送数量及节点数量的通信开销之间是需要权衡的。理论上的推导和结果模拟说明，每个节点以一个相对小的出度能够在高比例的拜占庭节点的情况下保证一个合适的扩散成功率。

4.8　多通道的支持

创建通道是为了限制信息传播的范围，是和某一个账本关联的。每个交易都是和唯一的通道关联的。这会明确地定义哪些实体（组织及其成员）会关注这个交易。

客户端SDK通过发送一个CONFIGURATION的交易背书（Endorsement）请求来创建一个通道，然后通过排序服务广播给其他节点。创建通道的请求包含组成通道的组织（Organization）列表，即哪些组织可以加入到这个通道中。

一旦创建好了通道，客户端SDK就可以通知组织内的节点加入到新创建的通道中。节点的Gossip模块就会给这通道内的组织广播一个消息：它属于这个通道了。

Gossip要在多通道环境下还能正常工作，成员管理需要对通道内的成员节点进行维护，也就是说通道内的所有节点都需要知道其他节点的存在。代表一个组织和排序服务进行连接的节点会接收到哪些节点属于这个通道，会根据需要调用Deliver（）接口。排序服务能够保证代表一个组织参与到这个通道的节点能够接收到区块数据。连接节点会分发新收到的区块数据给加入到这个通道的节点。当成员管理建立起来后，Gossip模块就会在隧道内部分发数据。不属于这个通道的数据项会在通道外传输，包括交易信息和与Gossip相关的会员管理信息。通道信息不会在通道外的节点间传播。

一旦一个节点加入到一个通道中，它需要获取最新的通道配置信息，用来识别参与的组织。节点加入通道的时候，客户端SDK就会提供通道最新的配置交易信息，里面会包含参与的组织。对于新创建的通道，这会是一个创世区块（Genesis Block）；对于老的通道来说，这是一个最新的重新配置的块。

节点获取到这些信息后，Gossip模块就会在通道允许的组织范围内分享这些信息。这样，通道的成员管理信息是在Gossip服务允许的范围内进行维护的。这些信息都建立起来以后，Gossip就会基于这些信息进行工作，就像只有一个通道一样。

通道内Gossip的工作方式和标准的Gossip是一样的。需要注意的是，成员管理是每个通道独立的，因而所有的操作都在这个通道的成员管理范围内。特别地，一个节点可以选择通道内的一些节点来进行区块转发，再选通道内的其他一些节点来进行状态同步。注意，状态可能会在不同的组织之间同步，只要它们是同一个通道的成员。

从通道中删除一个组织的过程和创建一个通道是类似的。客户端SDK会发送一个CONFIGURATION的交易背书请求。通道内的Peer节点背书完成后，删除组织和成员的交易就会发送给排序服务节点。然后，其他节点会收到一个更正过组织列表的通道消息。每个收到消息的Gossip模块都会移除被删掉的节点，因而，和通道相关的信息只会在剩下的通道成员之间传输。

被通道删掉的Peer节点最终会发现自己被删除了。因为Peer节点的Gossip成员管理模块会继续发送心跳信息，但是这些信息不会发送给已删除的节点。所以，过了一段时间以后，节点就知道自己不再属于这个通道了。

4.9　消息的验证策略

每个通过Gossip协议转发给其他节点的消息都会声明节点的一些信息，其包括以下内容：

·必须包含节点的PKI-ID；

·必须由节点进行签名；

·能够通过节点的证书进行验证。

节点间点对点传播的消息没有签名，不会通过Gossip转发。假设在生产环境下，节点的TLS默认是开启的，并且有安全方面的考虑（防止流量劫持、重放攻击等）。唯一一个不用节点签名而且不通过点对点方式传播的消息是账本数据区块，它是由排序服务进行签名的。

Peer节点接收到排序服务广播的数据区块以后，可以验证附加在区块里的签名信息，这个签名信息可以用来作为k/n的多签名（n个节点中至少有k个签名验证通过）验证策略。比如，SOLO和Kafka的排序服务只要求节点验证单个数字签名这样就可以验证分发的区块了。SBFT则是n个里f+1的策略，要求每个区块都要验证f+1个排序节点的数字签名。其他的排序服务可以指定其他的验证策略或者不同的k值。

Gossip模块初始化以后，它就会设置通道的验证策略，以判断节点所属的组织，并且它只给通道内的节点发送区块。这个过程可以通过关联每个节点的PKI-ID和其组织的根证书来实现，本地账本都有通道里组织的最新配置。

由排序服务广播的批块（Batch）包含了通道中最新配置区块（Configuration block）的序列号。当一个节点接收到其他节点发送过来的区块时，会检查区块的序列号和自己本地账本的序列号，然后就可以知道是不是可以安全地转发这个区块了。检查方法是查看最新配置区块的序列号，如果收到的数据区块序列号比提交到账本的最新序列号高，则数据区块就会缓存到内存中，不会转发给其他节点。否则，账本的最新配置就是最新的，区块就可以安全地转发给通过策略验证的节点了。

超级账本声明了一个内部消息的存储接口MessageStore：

type MessageStore interface {
// 添加消息msg到内存中，返回是否添加成功
Add(msg interface{}) bool

// 返回存储的消息数量
Size() int

// 返回存储的所有消息
Get() []interface{}
}

具体的实现如下所示：

type messageStoreImpl struct {
pol common.MessageReplacingPolicy
lock *sync.RWMutex
messages []*msg
invTrigger invalidationTrigger
}

包含一个通用的消息验证策略函数：

type MessageReplacingPolicy func(this interface{}, that interface{})
InvalidationResult

其中，this和that指代的是当前消息和原有消息的比较，并判断当前消息的有效性。比较的结果InvalidationResult有3种可能。

1）MESSAGE_INVALIDATES：当前消息是有效的，原有消息是无效的，用当前消息替换原有消息；

2）MESSAGE_INVALIDATED：当前消息是无效的，原有消息是有效的，丢弃当前的消息；

3）MESSAGE_NO_ACTION：两个消息可能是不同类型的，两个消息不进行比较，两个消息都是有效的。

MessageReplacingPolicy可以根据实际存储的消息定义不同的比较策略。

invalidationTrigger是一个回调函数，当替换原有消息时，可以对替换掉的消息进行一些处理。

我们来看几种具体消息类型实现的验证策略。目前用到的内部消息类型有：存活消息（Alive）、区块数据（Data）、状态消息（State）、身份消息（Identity）、主节点选举（Leader）消息。每个消息都有一个时间戳信息PeerTime，它包含两个字段incNumber和seqNum。判断两个消息的有效性和消息类型有关，验证方法可以分为3类。

1）基于时间戳的比较。总的原则是incNumber大的消息有效，有相同incNumber的消息，seqNum大的有效，incNumber和seqNum都相同的话以原有消息为准，当前消息若是无效消息，直接丢弃。

2）基于消息序列号的比较。这种比较策略只对数据区块进行比较，相同seqNum的还会比较数据哈希值，用相同哈希值的替换原有消息，否则两个消息都是有效的。若seqNum不同则要检查缓冲区大小能否存储两个消息seqNum之间的所有消息。如果缓冲区足够存放，则两个消息都是有效的，否则seqNum大的消息有效。

3）基于节点PKI-ID的比较。检查发送两个消息的节点PKI-ID是否相同，如果相同就以当前消息为准，已有的消息就过期无效了。这种验证方法只用在身份消息中，每个节点定期都会广播身份信息，其他节点就会以最后收到的信息为准，作为相同PKI-ID的节点身份信息。

不同消息类型的验证策略如表4-1所示。

表4-1　不同消息类型的验证策略

4.10　消息的多路分用及分区

通过Gossip协议广播的消息种类较多，不同种类的消息有不同的处理逻辑。Gossip模块利用GO语言的通道，实现一个消息的多路分用接口ChannelDeMultiplexer：

type ChannelDeMultiplexer struct {
channels []*channel
lock *sync.RWMutex
closed int32
}

其中lock是一个读写锁，用来同步对channels的处理；closed是通道是否关闭的标识，通道关闭就不能再从通道中读取数据；channels是一个channel数组，如下所示：

type channel struct {
pred common.MessageAcceptor
ch chan interface{}
}

ch是缓存消息的通道，默认只能保存10个消息。节点接收到Gossip消息的时候，会调用DeMultiplex接口。pred用来判断订阅者对某个消息是否感兴趣，感兴趣的消息会过滤出来存放在ch里，等待下一步的处理。pre的策略可以根据业务逻辑来实现，函数定义如下：

type MessageAcceptor func(interface{}) bool

我们来看看Gossip模块实现的几种MessageAcceptor。

1）Gossip消息过滤器。Gossip模块接收到的消息类型有：GossipMessage和Signed GossipMessage，如果不是这两种类型的消息，会过滤出来丢弃。

2）存活消息过滤器。过滤出类型为GossipMessage_AliveMsg、GossipMessage_MemReq和GossipMessage_MemRes的消息。

3）状态消息过滤器。过滤出状态同步消息。

4）远程状态消息过滤器。过滤出GossipMessage_StateRequest和GossipMessage_StateResponse的消息，这两种消息是为了状态同步而主动发送和接收的消息。如果它包含了连接验证信息，则会调用MCS验证是否是指定通道成员发送的消息。

5）主节点选举消息过滤器。过滤出某个通道上的GossipMessage_LeadershipMsg消息。

我们来看看Gossip模块中的多路分用消息过滤器漏斗，如图4-2所示。

节点的Gossip模块会绑定gRPC端口（默认是7051端口），从其他节点或者排序服务节点上获取到的消息，通过handler调用DeMultiplex实现消息的多路分用，消息会通过消息过滤器过滤后进行业务逻辑的处理。这里的过滤器都虚线来表示，以说明同一层的过滤器不是互斥的，就是说同一层的过滤器输入都是相同的，经过过滤器的输出会不同。如果经过多层过滤器的过滤，消息是在上一层过滤的基础上筛选出来的。比如“状态数据过滤器”和“状态同步消息过滤器”的输入都是“状态消息过滤器”的输出，而“状态消息过滤器”的输入是gRPC消息过滤出的Gossip消息。

图4-2　多路分用消息过滤器漏斗

ChannelDeMultiplexer中的channels可以定义多个，同一个消息只要满足pre定义的过滤条件，就会放到对应的ch里，所以可能不同的channel里有相同的消息。对于需要广播的消息，Gossip模块还实现了消息分区，同一个消息经过过滤器过滤以后，只会出现在一个列表中。函数实现比较简单，代码如下：

func partitionMessages(pred common.MessageAcceptor, a
*proto. SignedGossipMessage) ([]*proto.SignedGossipMessage,
[]*proto.SignedGossipMessage) {
s1 := []*proto.SignedGossipMessage{}
s2 := []*proto.SignedGossipMessage{}
for _, m := range a {
if pred(m) {
s1 = append(s1, m)
} else {
s2 = append(s2, m)
}
}
return s1, s2
}

经过partitionMessages处理过的消息a，会分成两个切片，满足过滤器pre的放到切片s1中，不满足的放到切片s2中。

广播消息过滤器有以下几种：

·区块数据过滤器：过滤出指定通道chainID的区块数据。

·状态消息过滤器：过滤出状态同步消息。

·通道内部的消息过滤器：过滤出只在通道内部广播的消息。

·组织内部的消息过滤器：过滤出只在组织内部广播的消息。

·主节点选举消息过滤器：过滤出某个通道上的GossipMessage_LeadershipMsg消息。

图4-3所示为分区消息过滤器漏斗。

图4-3　分区消息过滤器漏斗

和消息多路分用不同，经过分区以后消息只能在一个结果集中，同一层的过滤器是互斥的。比如经过“区块数据过滤器”过滤以后，“状态消息过滤器”等就只能在其他消息中进行筛选。

4.11　和Gossip相关的配置参数

和Gossip相关的配置参数如下所示：

# 启动连接节点，可以是多个
bootstrap: 127.0.0.1:7051
# 自动选择主节点还是指定主节点
useLeaderElection: false
# 只在useLeaderElection设置为false的时候生效
orgLeader: true
# 本地节点的ID：ip:port
endpoint:
# 区块缓冲区大小
maxBlockCountToStore: 100
# 消息推送间隔（毫秒）
maxPropagationBurstLatency: 10ms
# 消息推送缓冲区大小
maxPropagationBurstSize: 10
# 消息推送次数
propagateIterations: 1
# 消息推送节点数
propagatePeerNum: 3
# 消息获取间隔（秒）
pullInterval: 4s
# 消息获取节点数
pullPeerNum: 3
# 状态消息获取间隔（秒）
requestStateInfoInterval: 4s
# 状态消息推送间隔（秒）
publishStateInfoInterval: 4s
# 状态消息存活周期（秒）
stateInfoRetentionInterval:
# 存活消息中推送证书的周期（秒）
publishCertPeriod: 10s
# 是否验证区块消息
skipBlockVerification: false
# 是否忽略安全检查
ignoreSecurity: false
# 建立连接超时时间（秒）
dialTimeout: 3s
# 连接超时时间（秒）
connTimeout: 2s
# 接收消息缓冲区大小
recvBuffSize: 20
# 发送消息缓冲区大小
sendBuffSize: 20
# 摘要消息处理超时时间（秒）
digestWaitTime: 1s
# 请求消息处理超时时间（秒）
requestWaitTime: 1s
# 消息响应超时时间（秒）
responseWaitTime: 2s
# 存活消息间隔时间（秒）
aliveTimeInterval: 5s
# 存活消息超时时间（秒）
aliveExpirationTimeout: 25s
# 重新连接间隔时间（秒）
reconnectInterval: 25s
# 外部标识
externalEndpoint:

4.12　本章小结

本章主要介绍由Peer节点组成的P2P网络如何实现数据的同步。区块链网络可能由较多的节点组成，Peer节点和排序服务节点之间建立过多的连接容易造成网络异常和单点问题，基于Gossip的P2P数据分发可以减轻排序服务节点的压力。

第5章　分布式账本存储

分布式账本技术（DLT，Distributed ledger Technology）还有一个名称叫共享账本（Shared Ledger），通过在不同节点之间达成共识，记录相同的账本数据，这是区块链技术的基础。本章讨论在Hyperledger Fabric 1.0中分布式账本技术的实现。

5.1　概述

超级账本采用背书/共识（Endorsement/Consensus）模型，模拟执行和区块验证是在不同角色的节点中分开执行的。模拟执行是并发的，这可以提高扩展性和吞吐量：

·在背书节点（Endorsing Peer）处模拟执行链码（Chaincode）；

·在所有的Peer节点上验证交易并提交。

每个Peer节点会维护多个账本，如图5-1所示。

超级账本包含以下元素。

·账本编号：快速查询存在哪些账本；

·账本数据：实际的区块数据存储；

·区块索引：快速查询区块/交易；

·状态数据：最新的世界状态数据；

·历史数据：跟踪键的历史。

每个Peer节点会维护4个DB，它们分别是：

·idStore，存储chainID；

·stateDB，存储world state；

·versioned DB，存储key的版本变化；

·blockdb，存储block。

图5-1　分布式账本存储

5.2　读写集

5.2.1　交易模拟和读写集

在背书节点（Endorser）模拟执行交易的过程中，会生成读写集（Read-Write Set）。读集（Read Set）包含了唯一键的列表，还有在模拟执行过程中交易读取的已提交键值。写集（Write Set）也包含了一个唯一键的列表，还有在模拟执行过程中交易写的键值。交易过程中有删除键，它会记录删除标记。如果在一个交易中对同一个键进行了多次更新，则会以最后一个为准。交易只会读取已提交的数据，即使在交易中更新了某个键的值。就是说，不支持读取本次交易更新后的结果。

读集会包含键的版本，写集只会包含键的最新值。

版本号是使每个键不重复的唯一标识，实现的方法有很多，比如可以采用单调递增的数值来表示。在目前的实现中，采用的是区块链的高度来表示，一个交易中所有键值修改的版本号都是相同的。高度是一个二元组Heigh（blockNumber，txNuber），其中，blockNumber是区块号，txNumber是区块内的交易编号。采用区块链高度来表示版本号是有好处的，其他模块，比如状态数据statedb、交易模拟和验证都能更高效地判断版本号的有效性。

下面是一个读写集的示例，为了描述简单，版本号就用数字来表示：

<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来表示。

5.2.2　交易验证和世界状态更新

提交节点根据读写集中的读集来验证交易，根据写集来更新键的版本和值。

在验证阶段，怎么判断交易的合法性呢？首先，检查读集的版本号，比较在交易中读集里每个键的版本号是否和世界状态（World State）键的版本号一致。然后，如果读写集包含了query-info，则会检查query-info包含的键是否有变化，比如新增键、更新或者删除键。要比较在模拟阶段交易进行范围查询的结果是否和验证阶段范围查询的结果是一致的。这能够保证在提交过程中如果出现了幻影项（Phantom Item），交易就会标记为无效。注意幻影保护只实现了范围查询（即链码调用GetStateByRange），没有实现其他查询（比如链码调用GetQueryResult）。其他查询是有幻影风险的，只能用在不提交给排序服务的只读交易中，除非应用能够保证在模拟阶段和提交阶段结果的稳定性。

如果交易通过了上面的检查，那么提交节点会根据写集来更新世界状态。遍历写集中的每个键，更新世界状态里对应的键值和版本号。

5.2.3　模拟和验证示例

为了便于理解，来看一个例子。假设一个键值对在世界状态里用一个三元组来表示：（k，ver，val），其中，键k最新版本ver的值是val。

有5个交易T1、T2、T3、T4、T5，它们都基于同一个世界状态的快照进行模拟。下面的片段说明的是每个交易的读写集的操作：

世界状态: (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)

假设交易按照T1～T5进行排序，检查结果如下。

1）T1检查通过。因为在这个交易里没有任何读操作。交易会更新键k1和k2，更新后世界状态里的三元组为：（k1，2，v1'）和（k2，2，v2'）。

2）T2检查失败。因为交易需要读取的键k1在前面一个交易T1中被修改了。

3）T3检查通过。因为在这个交易里没有任何读操作。更新后世界状态的三元组为：（k2，3，v2''）。

4）T4检查失败。因为交易需要读取的键k2在前面一个交易T1中被修改了。

5）T5检查通过。因为交易需要读取的键k5在前面的交易中没有被修改。

5.3　账本编号

超级账本支持多账本（详细内容可以参考第7章的相关内容），每个账本的数据是分开存储的。

账本编号（LedgerID）的数据存储在LevelDB数据库中，只是记录了有哪些账本，创建新的账本会检查是否有相同的账本编号存在，这保证了全局唯一性。账本编号库并不存储与区块相关的数据，这和后面的区块索引不一样。

5.4　账本数据

账本数据（Ledger）是以二进制文件的形式存储的，每个账本数据存储在不同的目录下。后面的内容都是在已经区分了账本的情况下再对数据进行查询的。基于文件系统的区块存储实现了如下功能接口。

1）账本存储管理。

·提交区块到账本（AddBlock）

·获取区块链信息（GetBlockchainInfo）

·获取区块数据（RetrieveBlocks）

·关闭区块存储（Shutdown）

2）索引管理：跟踪区块和交易保存在哪个文件。

·根据哈希值获取区块（RetrieveBlockByHash）

·根据区块编号获取区块（RetrieveBlockByNumber）

·根据交易编号获取交易（RetrieveTxByID）

·根据区块编号和交易编号获取交易（RetrieveTxByBlockNumTranNum）

·根据交易编号获取区块（RetrieveBlockByTxID）

·根据交易编号获取交易验证码（RetrieveTxValidationCodeByTxID）

账本数据的所有操作都是通过区块文件管理器（blockfileMgr）实现的，定义如下：

type blockfileMgr struct {
rootDir string // 区块链中区块存储的根目录
conf *Conf // 配置信息
db *leveldbhelper.DBHandle // 数据库指针
index index // 区块索引接口
cpInfo *checkpointInfo // 区块检查点信息
cpInfoCond *sync.Cond // 条件变量
currentFileWriter *blockfileWriter // 当前写入区块文件的指针
bcInfo atomic.Value // 区块链信息
}

区块文件管理器实现的功能分为几类。

1）账本数据存储管理。

·确定文件存储在哪个目录；

·确定区块存储在哪个文件。

2）检查点管理：跟踪最新持久化存储的文件。

3）索引管理：跟踪区块和交易保存在哪个文件。

5.4.1　账本数据存储

区块文件管理器创建的文件名以"blockfile_"为前缀，6位数字为后缀，后缀必须是从小到大连续的数字，中间不能有缺失，比如blockfile_000000、blockfile_000001、blockfile_000002等。默认的区块文件大小上限为64MB（目前这个大小在代码中是固定的，以后可能会动态调整）。一个账本能保存的最大数据量大概有61TB，这应该能满足目前绝大多数的应用了。

记账节点（Committer）负责维护节点本地的账本，通过Gossip模块从排序服务接收到区块以后，区块添加到账本的过程如图5-2所示。

区块文件管理器维护一个当前写入区块文件的指针currentFileWriter，写入区块文件的数据包括两个部分，一个是区块大小，另一个是区块数据。写入区块文件中的区块大小和区块数据都是经过序列化处理的，序列化过程见技术实现部分。如果写入区块大小和序列化后，当前区块文件的大小超过设定值，则会写入到下一个区块文件中。创建一个新的区块检查点信息（见区块索引部分），保存到数据库中。

图5-2　添加区块

若写入区块文件和保存区块检查点信息的过程出现任何异常，就会根据区块检查点记录的最新区块文件偏移latestFileChunksize（见区块索引部分），恢复区块文件到写入前的状态。多个区块数据保存到区块文件后，区块文件形成了一个非结构化的二进制文件，它需要在区块文件外记录索引信息，才能快速地定位到区块。保存区块数据后会建立这个区块的索引，并保存到数据库中，详细的构建区块索引过程在后面的章节会有介绍。

最后是更新区块链文件管理器维护的区块检查点信息和区块链信息。

5.4.2　账本数据读取

区块文件流（blockfileStream）和区块流（blockStream）是以数据流的方式从区块文件系统中读取区块的，区块文件流是从单个文件中读取的，区块流可以跨不同的文件，实际的文件读取通过区块文件流来实现。区块流维护了当前的区块文件流指针，还有当前区块文件的编号等，详细的定义如下：

type blockStream struct {
rootDir string // 区块链中区块存储的根目录
currentFileNum int // 当前读取文件区块的编号
endFileNum int // 结束读取文件区块的编号
currentFileStream *blockfileStream // 当前区块文件流指针
}

可以有多个区块流同时读取区块文件以获取区块数据，读取从区块文件currentFileNum的某个偏移开始，到区块文件endFileNum（包含整个区块文件）之间的所有区块。结束读取文件区块的endFileNum可以设置为小于0的数值，这表示读取后续所有的区块文件，直到账本数据目录下某个区块文件不存在为止。

跟区块文件存储过程类似，读取区块数据需要先读取区块数据的大小，再读取区块数据本身。区块数据的大小是可变长的64位数字，最大长度为8个字节。由于可变长数字序列化后最后一个字节的最高位是0，所以能从字节流中区分出区块数据大小和区块数据本身。详细的编码规则见序列化部分。

区块迭代器（blocksItr）基于区块流，实现了获取区块数据（RetrieveBlocks）的接口。增加的一个业务逻辑是如果需要获取的区块编号还未生成，迭代器会一直等待，直到获取到指定区块或者关闭迭代器。

5.4.3　交易模拟执行

链码是可以并行执行的，执行的过程并不影响当前的状态数据库。实现的方法是在最新账本上生成一个账本数据的模拟器，模拟执行过程生成的数据会写入模拟器的writeMap中，读取的数据写入到readMap中，最后再根据writeMap和readMap生成TxRwSet结果。每次链码执行的时候都会生成一个新的模拟器，所以多个链码并行执行并不会相互影响，模拟执行的结果也不会直接影响当前的状态数据库，生成的TxRwSet在提交交易的时候，只有验证通过以后才会记录到账本中。

5.5　区块索引

超级账本提供多种区块索引（Block Index）方式，以便能够快速找到区块。这些方式包括：

·区块编号；

·区块哈希；

·交易编号；

·同时按区块编号和交易编号；

·区块交易编号；

·交易验证码。

5.5.1　文件位置指针

索引的内容是文件位置指针（File Location Pointer），位置指针的结构如下所示：

type fileLocPointer struct {
fileSuffixNum int
locPointer
}

type locPointer struct {
offset int
bytesLength int
}

文件位置指针由3个部分组成：

·所在文件的编号fileSuffixNum；

·文件内的偏移量offset；

·区块占用的字节数bytesLength。

区块查找是一个三级索引过程，查找一个区块就是先确定是哪个链，然后根据文件编号找到对应的文件，再根据文件的偏移量和占用的字节数确定区块的内容，如图5-3所示。

图5-3　多链下区块的三级索引

文件位置指针序列化后保存到LevelDB数据库中，不同的索引方式对应的键如表5-1所示（“+”是逻辑符号，不是真实的键组成部分）：

构建哪些索引类型是可配置的，默认是所有类型都建立索引（排序服务也会建立索引，但只有区块编号这种索引类型）。有两种途径可以构建索引：

·提交区块（Commit Block）；

·同步索引（Sync Index）。

表5-1　区块索引类型

提交区块到账本的时候会自动按照表5-1所示的区块类型构建索引，下面来看一下索引的同步过程。

5.5.2　索引的同步过程

索引同步只在创建区块文件管理器（blockfileMgr）的时候执行，验证数据库里保存的索引是否和区块文件系统里的一致。

数据库记录了两个检查点的信息：

·索引检查点信息（indexCheckpointKey）；

·区块检查点信息（blkMgrInfo）。

索引检查点信息记录的是最后建立索引的区块编号。通过索引检查点信息的区块编号，可以查询到区块编号对应的文件位置指针，它代表了数据库中记录的最新状态。这个状态由于系统异常等原因可能和区块文件系统存储的不一致。

区块检查点信息记录的是已提交到账本的区块信息，数据结构如下：

type checkpointInfo struct {
latestFileChunkSuffixNum int // 最新区块的文件编号
latestFileChunksize int // 最新区块的文件偏移
isChainEmpty bool // 是否为空链
lastBlockNumber uint64 // 最新的区块编号
}

区块检查点信息记录的是区块文件系统的一个状态，可能和索引检查点信息记录的区块编号不一致，也可能和区块文件系统不一致。由于记录过程是先提交区块到区块文件系统，再记录区块检查点信息，最后是索引检查点信息，所以区块文件系统的信息是最完整的。可以通过区块文件系统同步区块检查点信息和索引检查点信息。区块检查点信息和索引检查点信息记录的状态可能不是最新的，代表的是过去某个时刻正确的状态，索引同步可以从检查点记录的状态开始，更新为区块文件系统的最新状态，减少同步时间。

区块文件管理器在创建的时候会从数据库中获取区块检查点信息，并且需要和区块文件系统对比检查其是否为最新的状态。检查过程是基于区块检查点的文件编号和文件内由偏移构建的一个区块文件流（blockfileStream），从这个偏移开始验证该文件是否还有区块数据。如果确实还有区块，找到该文件实际存储的最大区块编号，更新区块检查点信息。这里用的是区块文件流，它只会检查单个文件。这可能会存在一个特殊情况，就是上一个区块刚好写满这个文件，达到了上限maxBlockfileSize，这个区块从下一个文件开始写，写完区块数据以后，更新区块检查点信息的时候出现异常，这时会导致区块检查点信息记录和区块文件系统的永久性不一致。

经过上面的检查后，区块检查点信息就和区块文件系统完全一致了。根据区块检查点信息，构建区块流，从索引检查点信息记录的区块文件和偏移开始，到区块检查点记录的最新区块的文件编号为止，全部重新构建索引，并更新索引检查点信息。

5.6　状态数据

状态数据（State Database）记录的是交易执行的结果，最新的状态代表了通道（Channel）上所有键的最新值，所以又称为“世界状态”。链码调用根据当前状态数据执行交易。为了提高链码执行的效率，所有键的最新值都存储在状态数据库中。状态数据库只是区块链交易日志中的索引视图，因此可以随时根据区块链重新生成。状态数据库在Peer节点启动时自动恢复，重新构建完成后才接受新的交易。

对于状态数据库本身插件化的设计，目前支持LevelDB和CouchDB。LevelDB和CouchDB都支持基本的链码操作，比如获取和设置键值，基于键进行查询等。

·LevelDB（默认的KV数据库）：支持键的查询、组合键的查询、键范围查询。

·CouchDB（可选）：支持键的查询、组合键的查询，还有复杂的查询。

不同账本的状态数据库存放在不同的目录下，同一个账本的数据是存放在一起的，不同链码的数据是按链码编号（chaincodeID）作为命名空间（Namespace）来划分数据的。命名空间在生成组合键（compositeKey）的时候作为组合键的前缀，分隔符可以自定义（修改源码），默认的分隔符是"0x00"。

状态数据库的基本操作是基于键值对的管理的。某个组合键k，在指定版本ver的值可以用一个三元组：（k，ver，val）表示，存储和读取数据都是版本化的。读取状态数据的时候不能指定版本，读取到的状态数据是某个时刻最新的版本，返回的数据包含版本和数据两个部分，版本的实现用交易的高度来表示的，它是由区块编号和交易编号组成的二元组。数据结构定义如下：

type VersionedValue struct {
Value []byte // 数据
Version *version.Height // 版本
}

type Height struct {
BlockNum uint64 // 区块编号
TxNum uint64 // 交易编号
}

基本区块数据的读取也是通过键来查询的，有3种方式：查询单个键的数据、查询多个键的数据、查询一个范围内的数据。如果采用的是CouchDB方式，还可支持某些字段的条件查询，详细的内容下面的章节会有介绍。

节点验证完数据后会披露并进行更新。这可以同时包含不同链码上的数据，同时用账本编号（chaincodeID）作为命名空间分割。相同链码的数据是由不同的键值对组成的字典（map）。批量更新的数据各自有不同的版本，在更新同一批数据的同时会更新世界状态交易的高度。

状态数据库支持如下的功能。

1）根据命名空间和键获取状态数据。

·获取单个键的数据：GetState

·获取多个键的数据：GetStateMultipleKeys

·获取一个范围内的查询数据：GetStateRangeScanIterator

2）根据条件查询获取数据：ExecuteQuery。

3）更新状态数据：ApplyUpdates，可批量更新。

4）获取最新交易高度：GetLatestSavePoint。

5）数据库操作。

·打开数据库：Open

·关闭数据库：Close

5.6.1　LevelDB

LevelDB是默认的状态数据库。LevelDB是采用C++编写的一种高性能嵌入式数据库，没有独立的数据库进程，占用资源少，速度快。它有如下一些特点。

1）键和值可以是任意的字节数组。

2）数据是按键排序后存储的。

3）可以自定义排序方法。

4）基本的操作是基于键的：

·Put（key，value）；

·Get（key）；

·Delete（key）。

5）支持批量修改的原子操作。

6）支持创建快照。

7）支持对数据前向和后向的迭代操作。

8）数据采用Snappy压缩。

超级账本基于https://github.com/syndtr/goleveldb 实现对LevelDB数据库的操作。状态值的存储和获取都是基本的键值操作，键是包含了链码编号（chaincodeID）的组合键，值是包含了版本信息和状态值的序列化结果。由于LevelDB不支持复杂的查询，所以使用LevelDB作为状态数据库也不支持条件查询接口：ExecuteQuery。交易高度是一个区块编号和交易编号组成的二元组，存储也是按照键值对的方式进行的，键为“0x00”；值是二元组序列化后的结果，GetLatestSavePoint是反序列化后得到的交易高度。具体的序列化和反序列化方法见后面的实现部分。

5.6.2　CouchDB

另外一个可选的数据库是CouchDB。CouchDB是一种文档型数据库，提供RESTful的API操作数据库文档。CouchDB中的文档是无模式的（Schemaless），并不要求文档具有某种特定的结构。CouchDB支持原生的JSON和字节数组的操作，基于JSON的操作，可以支持复杂的查询。如果存储的数据是字节数组，也支持基本的键值对操作。存储在CouchDB中的数据CouchDoc包含JSONValue和附件两个部分，如下所示。

type CouchDoc struct {
JSONValue []byte
Attachments []Attachment
}

type Attachment struct {
Name string
ContentType string
Length uint64
AttachmentBytes []byte
}

其中JSONValue：也和存储的类型有关系，它最终会转换成一个JSON的结构。JSONValue结构序列化后的内容如下。

{
"version": "$BlockNum:$TxNum",
"chaincodeid": "$chaincodeID",
"data": "$rawJSON"
}

在超级账本中，如果存储的类型是JSON，且JSONValue的data字段是状态值经过JSON序列化后的内容，则CouchDoc中的Attachments为空；如果存储的类型是字节数组，则JSONValue只保存版本信息，data字段为空，状态值放在Attachments的AttachmentBytes中，Attachments的Name为"valueBytes"，ContentType为"application/octet-stream"。在获取时根据data字段是否为空可以判断出存储的状态值类型，最后得到存储的版本和状态值。

条件查询是基于LevelDB的状态数据库所没有的功能进行的。查询前会对查询条件进行转换，增加"data."前缀和查询记录限制等，查询结果还会默认增加"_id""version""chaincodeid"。比如原始的查询条件为：

{
"selector": {
"owner": {
"$eq": "tom"
}
},
"fields": [
"owner",
"asset_name",
"color",
"size"
],
"sort": [
"size",
"color"
]
}

转换后的查询条件为：

{
"selector": {
"$and": [
{
"chaincodeid": "marble"
},
{
"data.owner": {
"$eq": "tom"
}
}
]
},
"fields": [
"data.owner",
"data.asset_name",
"data.color",
"data.size",
"_id",
"version",
"chaincodeid"
],
"sort": [
"data.size",
"data.color"
],
"limit": 10,
"skip": 0
}

其中查询记录限制limit可以在ledger.state.queryLimit中设置，默认值为1000。

交易高度也是采用键值存储的，当键为statedb_savepoint，值为couchSavepointData时JSON的结构如下所示。

type couchSavepointData struct {
BlockNum uint64 `json:"BlockNum"`
TxNum uint64 `json:"TxNum"`
UpdateSeq string `json:"UpdateSeq"`
}

5.6.3　基于状态数据的区块验证

区块数据提交到账本前，会基于状态数据验证区块数据是否有效。区块验证之前会从区块中解析出有效载荷（详细的消息结构参见后面的章节），根据不同的类型分别进行验证，目前的区块类型有：

·背书交易区块

·配置交易区块

配置交易区块的验证目前暂时没有实现，验证结果都为成功。我们来看看背书交易区块的验证过程，交易区块的验证主要是读写集的验证。从ChaincodeAction中解析出读写集TxRwSet，验证读取的版本是否和状态数据库里的版本一致。如果读写集中还有范围查询，也会验证范围查询中的每个记录是否和状态数据库中的版本完全一致。验证通过的交易，其读写集会添加到UpdateBatch中，同一个区块的交易还会验证是否会读取UpdateBatch中的记录，因为UpdateBatch中的记录是当前区块更新的数据，读取还没有提交到账本中的数据，这会导致和模拟执行时读取的数据版本不一致，所以在这种情况下验证会失败。

区块提交到账本之后，要从区块数据中恢复状态数据和历史数据，就不会再对读写集进行版本检查。

数字签名信封，没有验证签名。

交易验证过程实现了一个验证字节图，每个交易占用一个字节，并标识其状态，定义如下：

type TxValidationFlags []uint8

验证后的字节图会存放在无数据中。验证成功的背书交易区块会生成读写集。

1.基于版本的验证

基于状态数据版本的验证过程比较简单，对每一个状态的kvRead.Key，比较状态数据里保存的版本是否和交易记录里面读取的版本一致，若完全一致就验证通过，表示在模拟执行之后这个kvRead.Key的值没有被修改过。

2.基于范围查询的验证

基于范围查询的验证方法是：比较“范围查询的结果”是否和在最新状态数据基础上更新本次交易数据后的“模拟状态数据”一致，有两种方法可以进行比较：

·基于默克尔树计算哈希值的比较；

·基于范围查询结果的查询。

如果范围查询的结果已经包含了默克尔树哈希计算结果，范围查询就采用基于默克尔树计算哈希值的比较方法。比较的过程是逐个计算模拟状态数据的默克尔树哈希，在计算过程中判断是否和范围查询的默克尔树哈希值一致，若过程中出现不一致就退出。默克尔树的计算过程对新增元素是友好的，可以在已经计算过的结果基础上迭代计算出最后的结果，不需要整个数组一起重新计算。

我们再来看看基于范围查询结果的查询比较过程，比较的方法就是遍历每个元素，比较两个迭代器元素的键值对是否完全一致，包括元素的个数。

5.7　历史数据

历史数据（History Database）记录了每个状态数据的历史信息，历史信息是保存在LevelDB数据库中的。每个历史信息用一个四元组（namespace、writeKey、blockNo、tranNo）来表示，其中：

·namespace：实际代表的是不同的chaincodeID，从这里也可以看出，不同chaincode的数据是逻辑隔离的；

·writeKey：要写入数据的键；

·blockNo：要写入数据所在的区块编号；

·tranNo：要写入数据所在区块内的交易序号，从0开始。

历史信息记录最细的粒度就是交易，如果在一个交易中多次对同一个writeKey更新数据，则会以第一次数据为准，历史信息实际存储的信息是固定的空字节数组[]byte{}。更新区块信息的时候，会同步更新检查点信息，保存的内容是最新的区块高度和最大的交易序号，检查点信息用来判断历史信息的状态是否是最新的。

5.8　数据恢复

区块的提交过程分为3个步骤：

·先保存区块到文件存储的账本数据中；

·然后更新状态数据；

·最后更新历史信息数据。

这3个步骤是顺序执行的，在这个过程中有文件的操作，也有数据库的操作，所以在任何一个步骤都可能出现错误或者中断。恢复的过程会根据账本数据记录的区块信息和状态数据、历史信息数据的检查点进行比较，重新提交检查点之后的区块信息，保持账本数据的一致性。

5.9　本章小结

本章介绍了Hyperledger Fabric 1.0的数据存储，包括账本数据（Ledger）、区块索引（Index）、状态数据（stale Database）、历史数据（History Database）等存储结构。目前的数据存储存在较大的优化空间，账本数据结构的设计带来的开销很大，最新的版本并没有实现账本裁剪（Ledger Prune）功能，不能进行归档，也不能删除无效交易，所以会导致存储空间持续增长。

第6章　集成共识机制的排序服务

本章会介绍共识的一些基本概念和Hyperledger Fabric 1.0中的共识机制，及其可插拔的架构设计。

6.1　概述

在区块链系统中，共识（Consensus）是多个参与方对一个交易是否提交到账本以及提交的顺序达成一致的过程。由于是多个节点参与的分布式系统，所以网络传输可能存在延时。共识一致并不代表在所有时刻都有完全相同的结果，是经过一段收敛时间后，网络中的多数节点对同一个交易执行相同的记账操作。在共识的过程中可能存在一些节点无响应或者响应延迟的情况，也可能存在一些参与方恶意提交请求或者篡改请求内容的情况。根据错误类型的不同，共识算法可以满足两种范围的容错。

1）崩溃故障容错（Crash Fault-Tolerance，CFT）：在区块链网络中存在网络延时或者故障的情况下，共识机制能够确保有效的交易达成一致。

2）拜占庭容错（Byzantine Fault-Tolerance，BFT）：在区块链网络中存在部分恶意节点提交或者篡改请求的情况下，共识机制能够确保有效的交易达成一致。

在共识过程中网络节点要确保交易有序且交易区块有效，需要提供以下核心功能。

1）交易有效：能够根据背书及共识策略确保区块中所有交易有效。

2）交易有序：能够确保所有节点提交和执行交易顺序的一致性，这才能保证执行结果的一致性和最终全局状态的一致性。

3）交易验证：能够利用智能合约的接口，验证交易的有效性和提交顺序。

根据第3章的交易流程，我们看到在Hyperledger Fabric 1.0中，一个交易从提交到最终记账会经历多个阶段，每个阶段都需要多节点的参与，共识是在分阶段共识基础上的全过程共识，对于任何一个阶段的共识失败，最终结果都是共识失败。

6.1.1　共识算法的类型

共识算法必须具备两个特性以保证节点之间数据的一致性。

1）安全性（Safety）：它指每个节点保证相同的输入序列，并在每个节点上产生相同的输出结果。当节点接收到相同顺序的交易时，每个节点将发生同样的状态改变。算法的执行结果必须与单节点系统依次执行每个交易的结果一致。

2）存活性（Liveness）：指在没有通信故障的情况下，每个非故障节点最终都能接收到提交的所有交易。

节点一致性在账本上的体现是：

1）区块的确定性：在不同节点上相同区块号的区块内容完全一致；

2）区块的完整性：在不同节点上有相同的区块数，且按照顺序形成相同的区块链。

共识算法大体可以分为两种类型，基于彩票中奖的算法（Lottery-based Algorithm）和基于投票计数的算法（Voting-based Algorithm）。

基于彩票中奖的算法是一个形象化的说法，加入到区块链网络中的节点都可以生成区块，广播给网络中的其他节点以进行确认，通过竞争获得最终的记账权。实际上，区块是否获取其他节点的确认是有一定概率的，这与买彩票中彩一样。这种算法是一种先记账再共识的算法，优势是其可以很容易地扩展到大量的节点上，缺点是区块记账是一种大概率的确认算法，区块确认的时间较长。常见的一些算法，如消逝时间量证明算法（Proof of Elapsed Time，PoET）和工作量证明算法（Proof of Work，PoW）都属于这一类算法。

基于投票计数的算法是节点参与区块的投票，网络中的节点确认区块以后再记账的过程。这种算法中区块的记账是确定性的，已经记账的区块都是有效的。这是一种绝对一致的机制，确定的结果才能在某些对结果要求比较高的场合使用，比如金融场景，已经完成的转账是不能随意地撤回的。这种算法的缺点是需要更长的时间达成共识，不能满足某些需要高吞吐量、低延迟的场景要求。这就需要不同的应用场景在可扩展性、确定性和速度等方面进行一个权衡。冗余拜占庭容错算法（Redundant Byzantine Fault Tolerance，RBFT）和Paxos都属于这一类型的算法。

表6-1所示为这两种算法的比较。

表6-1　共识算法类型的比较

超级账本应用场景的目标是企业级的区块链平台，网络节点一般是需要授权加入的，所以超级账本是不支持标准的PoW算法的。

6.1.2　Hyperledger Fabric 1.0的共识机制

超级账本中是把共识分为3个阶段，如图6-1所示。

图6-1　超级账本的共识机制

（1）交易背书

应用程序根据背书策略的要求选择背书节点，给这些节点发送需要执行的交易提案（Proposal）。背书节点调用链码（Chaincode）执行这些交易提案，交易过程是执行是模拟执行的，并不真正提交数据到账本中。执行完成以后调用交易背书系统链码ESCC对模拟执行结果进行签名背书。

（2）交易排序

排序阶段接受已经签名背书的交易，确定交易的顺序和数量，将排好序的交易打包到区块中，广播给Peer节点进行验证。通常，从效率方面考虑，排序服务不会输出单个交易作为一个区块，而是把多个交易打包成一个区块。

（3）交易验证

Peer节点验证接收到区块里包含的交易的有效性，包括背书策略验证及双花检测（double-spending）。可以将验证错误分为两大类：语法错误和逻辑错误。语法错误包括无效输入、未验证的签名以及重复的交易（双花攻击），重复的交易应该丢弃。第二类错误比较复杂，比如会导致双花或者MVCC失败的交易，这需要定义策略来决定程序是继续执行还是终止。策略还可以定义是否需要记录日志以便对这类交易进行审计。交易验证依赖链码的业务逻辑，目前默认的交易验证系统链码VSCC只支持背书策略的验证，详细的过程后面的章节会有介绍。

在超级账本中这3个阶段的设计均是可插拔的，应用程序可根据自身需求实现和选择不同的交易背书、交易排序及交易验证模块。

我们先来看下交易背书和交易验证的可插拔设计。交易背书和交易验证由内置的系统链码（System Chaincode）来实现，这两个系统链码都是可替代的。可以把交易背书和交易验证的功能逻辑实现成链码（Chaincode）并提交到链上，在提交交易提案的时候指定新的交易背书和交易验证链码。前面的章节已经详细介绍过peer chaincode命令，里面的“-E”和“-V”参数就是指定新的交易背书和交易验证系统链码的。

排序服务的实现也是可插拔的，它采用的是异步事件的方式，提供了两个基本的接口：

1）broadcast（blob）：客户端调用broadcast接口在通道上广播blob消息。

2）deliver（seqno，prevhash，blob）：排序服务调用deliver给客户端发送blob消息，包含序号seqno（无符号整数）和上一个消息的哈希（prevhash），它是排序服务的事件输出接口。

目前，正式发布的版本只支持Apache Kafka的排序服务。排序服务接口会加入基于BFT协议的算法，目前正在开发中的算法有BFT Smart、简化拜占庭容错算法（SBFT）、蜜罐拜占庭容错算法（Honey Badger of BFT）等。在本章的后面部分我们会详细介绍排序服务的接口，大家可以实现一个自定义的排序服务。

6.2　实现数据隔离的多通道

排序服务给客户端和Peer节点提供了一个共享通信通道（Communication Channel），用来实现交易的广播服务。客户端连接到通道（Channel）上，在通道上广播的消息会最终发送给通道内所有的Peer节点。通道支持消息的原子广播（Atomic Broadcast），通道给所有相连的Peer节点输出相同的消息，并且有相同的逻辑顺序。这种原子通信保证也叫全序广播（Total-order Broadcast）。

排序服务支持多通道（Multi-channel），类似Kafka消息系统的主题（Toptics）。客户端连接到一个指定的通道上，就可以发送或者获取消息了。通道是相互隔离的，客户端连接到一个通道是不知道其他通道的存在的，但是客户端可以连接到多个通道。为简单起见，在本章后面的部分，除非明确提到的其他情况，否则我们都假设排序服务是由单个通道和主题组成的。

图6-2所示为一个多通道的示例，基于SDK开发的应用程序或者命令行程序通过gRPC通道给排序服务提交背书节点以模拟执行后的交易，排序服务会根据交易信息里提案的请求头确定通道信息，添加到对应的队列中进行排序，生成区块后广播给加入了这个通道的节点。比如节点1会收到通道1和通道N的区块信息，节点2会收到通道1、通道2和通道N的区块信息，节点N会收到通道2和通道N的区块信息。没有加入通道中的节点是接收不到这个通道的区块信息的，比如节点1收不到通道2的区块信息，节点N收不到通道1的区块信息。

图6-2　多通道示例

每个通道在节点上都有一个关联的账本，节点2在3个通道上，本地会对应有3个账本数据，账本存储结构参考上一章的介绍。在区块链网络的所有节点上都存在的账本称为系统账本，反之称为子账本。从数据隔离角度看，两个通道上的节点完全一样是没有意义的，有多个系统账本也是没有意义的。由于排序服务能接收到所有链的交易信息，所以数据隔离是对节点来说的，并不针对排序服务节点（Ordering Service Nodes，OSN）。如果业务上希望交易信息的内容对排序服务也保密，可以对交易信息进行哈希或者加密，避免传输明文信息。

6.2.1　排序服务的初始化

排序服务是由多个排序节点组成的，在每个排序节点启动的时候需要有一个创世区块（Genesis Glock）。创世区块包含的信息有：

·排序节点信息及其MSP信息（管理员证书、根证书和TLS根证书）；

·组织信息及其MSP信息（管理员证书、根证书和TLS根证书）；

·共识算法类型；

·区块配置信息；

·访问控制策略。

创世区块是通过configtxgen工具生成的，还有一个工具configtxlator可以实现区块和JSON格式之间的相互转换，下面是一个创世区块转换成JSON后的格式，包含的信息概要如下所示：

{
"data": {
"data": [
{
"payload": {
"data": {
"config": {
"channel_group": {
"groups": {
"Consortiums": {
"groups": {
"SampleConsortium": {
"groups": {
"Org1MSP": {...
},
"Org2MSP": {...
}
},
"mod_policy": "/Channel/
Orderer/Admins",
"values": {...
}
}
},
"mod_policy": "/Channel/Orderer/
Admins",
"policies": {...
}
},
"Orderer": {
"groups": {
"OrdererOrg": {...
}
},
"mod_policy": "Admins",
"policies": {...
},
"values": {
"BatchSize": {
"mod_policy": "Admins",
"value": {
"absolute_max_bytes":
102760448,
"max_message_count": 10,
"preferred_max_bytes":
524288
}
},
"BatchTimeout": {
"mod_policy": "Admins",
"value": {
"timeout": "2s"
}
},
"ChannelRestrictions": {
"mod_policy": "Admins"
},
"ConsensusType": {
"mod_policy": "Admins",
"value": {
"type": "solo"
}
}
}
}
},
"mod_policy": "Admins",
"policies": {...
},
"values": {
"BlockDataHashingStructure": {
"mod_policy": "Admins",
"value": {
"width": 4294967295
}
},
"HashingAlgorithm": {
"mod_policy": "Admins",
"value": {
"name": "SHA256"
}
},
"OrdererAddresses": {
"mod_policy": "/Channel/Orderer/
Admins",
"value": {
"addresses": [
"orderer.example.com:7050"
]
}
}
}
}
}
},
"header": {...
}
}
}
]
},
"header": {
"data_hash": "etbznpwMod/5MUv9j3Ul9fo8fWj6WYd+0PFWFx2c2X8="
},
"metadata": {...
}
}

其中，…表示信息省略了。

6.2.2　通道的创建

应用程序可以通过SDK或者命令行向排序服务发起创建通道的请求，提交的内容是通道配置交易（Channel Configuration Transaction）。通道配置交易需要由工具configtxgen生成，详细步骤在第2章已经详细介绍过。以下是一个通道配置交易通过工具configtxlator转换成JSON格式的示例：

{
"payload": {
"data": {
"config_update": {
"channel_id": "mychannel",
"read_set": {
"groups": {
"Application": {
"groups": {
"Org1MSP": {},
"Org2MSP": {}
}
}
},
"values": {
"Consortium": {
"value": {
"name": "SampleConsortium"
}
}
}
},
"write_set": {
"groups": {
"Application": {
"groups": {
"Org1MSP": {},
"Org2MSP": {}
},
"mod_policy": "Admins",
"policies": {
"Admins": {
"mod_policy": "Admins",
"policy": {
"type": 3,
"value": {
"rule": "MAJORITY",
"sub_policy": "Admins"
}
}
},
"Readers": {
"mod_policy": "Admins",
"policy": {
"type": 3,
"value": {
"sub_policy": "Readers"
}
}
},
"Writers": {
"mod_policy": "Admins",
"policy": {
"type": 3,
"value": {
"sub_policy": "Writers"
}
}
}
},
"version": "1"
}
},
"values": {
"Consortium": {
"value": {
"name": "SampleConsortium"
}
}
}
}
}
},
"header": {
"channel_header": {
"channel_id": "mychannel",
"timestamp": "2017-08-22T18:53:29.000Z",
"tx_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca49599
1b7852b855",
"type": 2
}
}
}
}

排序服务接收到创建通道的请求，会检查是否是配置交易。检查的方法是查看通道头ChannelHeader的类型是否为HeaderType_CONFIG_UPDATE（新建通道和更新通道配置的类型都是这个类型），然后排序服务节点重新生成一个配置交易，修改交易类型为HeaderType_CONFIG，内容除了包含write_set里的Aplication，还会包含系统链里的Orderer和Consortium，新生成的交易会利用接收消息的排序服务节点的私钥重新进行签名，然后添加到系统链的交易消息队列中进行处理。

每个链（包括系统链）有一个协程专门处理消息队列中的消息，按消息队列的顺序进行排序。系统链增加了newSystemChainFilter的消息过滤器，在排序的过程中会检查系统链中的消息是否满足过滤器的条件，检查的结果可能是转发、拒绝或者接收，接收消息的同时返回系统链的提交器systemChainCommitter。交易切割形成区块后，在区块写入系统账本之前会调用提交器创建一个新的链，在新的链上创建一个包含配置交易的创世区块。新的链会在多账本管理器multiLedger处注册，当接收到这个链上的交易请求时，排序服务节点会分发给这个链来处理。这样，排序服务节点就有了多链的处理逻辑，给不同链的交易划分了不同的通道，实现了数据的隔离。

应用程序发起了创建通道的请求，创建成功后返回这个链的创世区块。新链的创世区块如下：

{
"data": {
"data": [
{
"payload": {
"data": {
"config": {
"channel_group": {
"groups": {
"Application": {
"groups": {
"Org1MSP": {...
},
"Org2MSP": {...
}
},
"mod_policy": "Admins",
"policies": {...
},
"version": "1"
},
"Orderer": {...
}
},
"policies": {...
},
"values": {...
}
},
"sequence": "1"
},
"last_update": {
"payload": {
"data": {
"config_update": {
"channel_id": "mychannel",
"read_set": {..
},
"write_set": {...
}
},
"signatures": [...
]
},
"header": {...
}
},
"signature": "MEUCIQDoU1ZwztKkRhp5f0X2aWef7xQ73P7rqu
MrGGx7EcRNNQIge2inBpxbGHkQ83lyHoMdA7eciNPmX3+QwCLbzOgkUpU="
}
},
"header": {
"channel_header": {
"channel_id": "mychannel",
"timestamp": "2017-08-31T09:24:31.000Z",
"type": 1
},
"signature_header": {...
}
}
},
"signature": "MEQCIA/fOYvcOou5pkHtjtiG3eV4f67z4D/
Myusxu3qAHAdAAiB0j/ekiakdHkbOBM9uuR2bzYjVBrKuoK1UaJ1ZuWkm8Q=="
}
]
},
"header": {...
},
"metadata": {...
}
}

新链的创世区块包含了通道配置交易的内容，扩展了从系统链上保存的组织信息、排序节点服务信息等，这样节点可以根据这个创世区块确定新链的标识、排序服务节点地址等，而不用访问排序服务的系统链。

6.2.3　通道的更新

通道的配置是可以更新的，包括通道组织、组织MSP、访问控制策略、排序服务配置等。通道更新需要线下确定好需要修改的配置项，离线修改后通过SDK或者CLI发送给排序服务节点，详细的过程如图6-3所示。

从图6-3可以看到，通道配置的修改同样需要利用configtxlator服务，详细的使用方法参考附录B的内容。

发送给排序服务的交易请求类型为HeaderType_CONFIG_UPDATE，排序服务节点接收到配置更新交易请求后会重新创建一个类型为HeaderType_CONFIG的信封，添加到被修改通道的交易队列里。每个链都一些过滤器规则，若检测到是类型是HeaderType_CONFIG就提交给configManager来处理，Apply函数处理过程如图6-4所示。

利用排序服务生成的配置区块，也会通过排序服务建立好gRPC连接的主节点（orgLeader）广播给通道内的其他节点。记账节点接收到配置区块以后，账本提交器（LedgerCommitter）在提交到账本之前会检查ChannelHeader的类型，如果是HeaderType_CONFIG就确认是配置区块，从区块里获取链编号，更新本地节点这个链的配置区块为最新收到的区块。

图6-3　利用configtxlator构造通道更新的请求

图6-4　排序服务节点处理通道更新的请求

6.2.4　通道的加入

应用程序通过SDK或者命令行给节点发送包含通道创世区块的JoinChain请求，请求的类型是HeaderType_CONFIG。节点加入链的JoinChain请求是由系统链码CSCC处理的。节点会校验创世区块的合法性（包括内容完整性），比如是否包含应用相关的配置项，还会对提交者的身份进行认证和权限检查。配置交易是需要应用程序用提交者的私钥进行签名的，身份认证需要确认请求头是否包含有效的提交者身份信息，再验证配置交易请求是否是该提交者签名的。然后是权限检查，包括两个方面。

1）是否有权限向节点提交请求：即检查提交者的MSP是否与本地MSP相同。

2）是否满足加入通道请求的策略：提交加入通道请求是需要有管理员权限的，管理员的证书配置在节点的$CORE_PEER_MSPCONFIGPATH/admincerts目录下。

权限检查会在节点本地创建以请求头里channel_header的channel_id为标识的链，节点的本地配置是由主节点（orgLeader）或者选举为主节点后主动和创世区块里设置的排序服务节点建立连接，接收排序服务广播的新区块，再在组织内部节点同步；节点若不是主节点就从组织内部其他节点上同步，详细的过程参见上一章的相关内容。

6.2.5　通道的查询

通道信息是节点本地维护的，有一个值为chain的映射表（list map[string]*chain），在节点启动或者有加入通道等操作的时候更新这个映射表。映射表的键就是通道名称，遍历就能返回节点加入的所有通道。这个映射表只能通过系统链码CSCC查询，命令行的方式是：

peer channel list

应用程序也可以通过SDK发送请求，比如fabric-sdk-go提供的接口QueryChannels（peer Peer）（*pb.ChannelQueryResponse，error）可以查询指定节点加入的通道。命令行和SDK的方式都是发送相同的请求，即构造一个调用CSCC的GetChannels调用请求。查询通道信息是需要权限的，权限的策略配置是在配置区块里的，一般的读取权限是只要成为这个组织的成员就可以了。通过权限检查返回的是由本地节点维护的已加入通道的名称数组。

通道的配置也是可以查询的，比如获取排序服务节点。节点内部查询配置区块是通过调用系统链码CSCC的GetConfigBlock获取的，同样会检查是否有权限查询，通过检查会返回由本地维护的最新配置区块。

应用程序和命令行是怎么从账本里找到最新的配置区块呢？实际上，每个区块的元数据里都有最新配置区块的索引，所以需要先给排序服务发送SeekPosition_Newest请求以获取最新的区块，从区块里解析出BlockMetadataIndex_LAST_CONFIG以获取配置区块高度，根据区块高度获取指定的配置区块。需要说明的是，最新的配置区块都是全量的，如果通道的配置信息更新过，更新的内容都会更新到最新的配置区块里，获取最新的配置区块就能查询最新的配置信息了。

命令行获取通道配置的命令：

peer channel fetch config config_block.pb -o $OSN_IP:$OSN_PORT -c $CHANNEL_ID

其中：OSN_IP和OSN_PORT是排序服务节点的地址和端口，CHANNEL_ID是通道编号。config_block.pb是配置区块文件，如果不提供这个参数，则默认生成的配置区块文件是$CHANNEL_ID.block。

通道上的交易处理和智能合约参考上一章的相关部分。截止到目前的版本，通道创建以后还不能终止或者删除，也没有提供功能让节点退出一个通道。

6.3　可插拔的排序服务

排序服务是可插拔的，代码里提供了几种实现：

·基于单进程（Solo）的排序服务；

·基于Kafka的排序服务。

目前的版本没有基于*BFT的排序服务。

6.3.1　排序服务接口

排序服务的业务需求可以实现不同的逻辑，Hyperledger Fabric 1.0已经预留了一些接口，需要修改地方如下所示：

·创建链的接口；

·链消息处理的接口；

·增加新的排序服务支持。

1.创建链的接口

创建链的接口定义如下：

// Consenter定义了后台的排序机制
type Consenter interface {
// 创建并返回一个对Chain的引用，用于提供资源
// 每个进程会被指定的chain调用一次。通常情况，发生错误不可恢复，并会导致系统关闭，有关的
// 详细信息，请参考Chain的描述
// 第二个参数是一个指针，指向该Chain中账本最后一个提交块ORDERER的存储元数据。由于
// genesis块中没有存储元数据字段定义，所以在新的Chain中值是nil。
HandleChain(support ConsenterSupport, metadata *cb.Metadata) (Chain, error)
}

当排序服务节点接收到创建通道的请求时，会根据链创世区块里配置的通道类型创建新的通道，调用的是不同通道类型的HandleChain函数，返回能够处理链上交易的Chain对象。传入的参数support ConsenterSupport提供交易过滤、交易切割、区块签名等功能。参数metadata*cb.Metadata可以保存一些跟排序服务相关的元数据（BlockMetadataIndex_ORDERER），比如保存Kafka最新的偏移（Offset）。元数据的写入和读取是由不同的排序类型服务完成的，不同类型的排序服务有不同的处理逻辑，比如solo就不需要这个元数据信息。需要注意的是，元数据会写入到区块数据中，不同的排序服务区块元数据是不一样的，所以排序服务是不能动态切换的。

2.链消息处理的接口

排序服务接收到某个通道上的交易后会提交给Chain处理，需要实现的接口如下所示：

type Chain interface {
// 成功接收消息返回true，否则返回false
Enqueue(env *cb.Envelope) bool
// 发生错误时，会返回报错的通道，这对Deliver客户端来说很重要，在没有达成最新共识时可以
// 终止客户端的等待
Errored() <-chan struct{}
// 用于分配Chain的各种资源，并保持相关的最新状态。通常情况，包括从排序服务中读取资源，
// 将消息传递给区块进行拆分，以及将区块结果写入账本Start()
// 用于释放给Chain分配的资源
Halt()
}

接口Chain接收交易请求并进行排序，生成最终的区块。接口Chain提供了可以提交消息进行排序的方法。在实现这个接口的时候，需要把排好序的交易通过blockcutter.Receiver进行交易分割，最后写到账本中。交易的分割有两种模式。

1）交易先进入消息流中，它在消息流中是有序的，消息流中的交易分割到不同的区块里，最后写入到账本中。solo和Kafka都是这种模式。

2）交易分割到不同的区块中，区块是有序的，最后写入账本中。sbft是这种模式。

3.增加新的排序服务支持

排序服务的支持需要配置文件的支持和配置文件参数的识别。在生成创世区块的配置文件configtx.yaml的参数Orderer.OrdererType中，添加新的排序服务类型，比如newconsenter。

添加了配置文件以后，排序服务节点还需要能够识别新增加的参数，在initializeMulti ChainManager的consenters里增加一个类似newconsenter的映射：

consenters["newconsenter"] = newconsenter.New()

在newconsenter里实现新的排序和共识算法。注意：配置参数里配置的名称要和新增的名称保持一致。

如果新的排序服务需要更多的参数支持，可以参考Kafka的参数设置方法，修改相关的代码。

6.3.2　基于单进程的排序服务

1.创建链的实现

单进程（Solo）的实现方式比较简单，利用Golang的并发机制内部构建一个接收消息的通道sendChan，然后返回处理交易信息的链multichain.Chain：

func newChain(support multichain.ConsenterSupport) *chain {
return &chain{
batchTimeout: support.SharedConfig().BatchTimeout(),
support: support,
sendChan: make(chan *cb.Envelope),
exitChan: make(chan struct{}),
}
}

其中，batchTimeout是最长的区块生成间隔时间，support辅助提供交易切割和生成区块的功能，exitChan是服务异常的终止信号。

2.接收交易请求的实现

创建链以后，还需要通过Start（）启动链的处理过程。在Solo的实现中，就是启动一个协程循环的接收发送到sendChan通道的数据，然后进行交易的切割和区块的写入。

排序服务接收到交易请求以后，会根据不同的排序服务类型提交给不同的链进行处理，入口函数就是Enqueue。Solo类型接收到消息以后发送给sendChan就结束了。

3.错误处理的实现

排序服务内部如果出现异常，会给exitChan发送消息，外部程序可以读取Errored返回的通道，进行异常处理。

6.3.3　基于Kafka的排序服务

基于Kafka的排序服务利用Kafka作为交易的消息队列，实现高吞吐量的数据分发。每个通道都对应Kafka的一个主题（topic），排序服务节点在不同阶段充当不同的角色。

1）接收交易阶段：排序服务节点充当的是Kafka的生产者（producer），接收到交易后通过权限检查转发给对应通道的主题。

2）消息处理阶段：排序服务节点充当的是Kafka的消费者（consumer），实时监听消息进行后续的处理，生成区块或者交易分割消息等。

1.创建链的实现

在基于Kafka的排序服务创建链的时候，同样返回一个能处理交易的multichain.Chain对象，实际返回的是实现了Chain接口的chainImpl，实现如下所示：

type chainImpl struct {
consenter commonConsenter
support multichain.ConsenterSupport

channel channel
lastOffsetPersisted int64
lastCutBlockNumber uint64

producer sarama.SyncProducer
parentConsumer sarama.Consumer
channelConsumer sarama.PartitionConsumer

// 当发生分区消耗错误时关闭通道，否则，它是一个开放无缓冲的通道
errorChan chan struct{}

// 当收到Halt()请求时关闭channel，与errorChan不同，channel在关闭时不会重新启动
// 打开，伴随着关闭还将触发processMessagesToBlock退出循环
haltChan chan struct{}

// 在Start重试步骤完成后关闭
startChan chan struct{}
}

特别注意的参数是lastOffsetPersisted和lastCutBlockNumber。参数lastOffsetPersisted记录的是排序服务节点最近读取Kafka集群消息的偏移量（offset）。Kafka的每个分区（partition）都是有序的消息队列，偏移量用来表示队列中每个消息的序列号。通过这个偏移量，排序服务节点就能确定哪些消息是已经处理过的，哪些是还需要后续继续处理的。这个偏移量是持久化保存在区块的Metadata中，类型是BlockMetadataIndex_ORDERER。这样，不同的排序服务节点读取最新的区块就能确定最新读取过的消息偏移量了。那么这个偏移量是如何更新的呢？偏移量记录的是当前区块对应的Kafka分区中最后一个交易的序列号，有两种情况会产生新的区块，在区块中记录当前区块交易在Kafka中的偏移量。

（1）正常的交易分割

当交易数量达到设定的最大交易数Orderer.BatchSize.MaxMessageCount、未打包交易的大小超过设定的区块大小Orderer.BatchSize.PreferredMaxBytes或者新提交的交易加入后未打包交易大小超过设定的区块大小时，会进行区块分割。由于交易是顺序处理的，满足分割条件就会分割出一个区块，所以一次最多会分割出两个区块，分割出的第二个区块最多只有一个交易。同时生成多个区块，并更新每个区块偏移量的时候可以用最后一个交易在Kafka中的偏移量作为最后一个区块的偏移量，前一个交易的偏移量为上一个区块的偏移量，用算法来描述就是：

offset := receivedOffset - int64(len(batches)-i-1)

其中，receivedOffset为从Kafka集群中接收到交易。

（2）超时的交易分割

当距离产生上一个区块的时间间隔超过设定的最大区块间隔时间Orderer.BatchTimeout时，会检查超时打包消息记录的区块号是否正好是下一个分割区块的序号，如果ttcNumber==*lastCutBlockNumber+1就分割未打包的交易形成新的区块。详细的区块分割逻辑参考后面的内容。

参数lastCutBlockNumber记录的是排序服务节点这个链上最近分割区块的序号。由于不同的节点是独立打包的，所以各个节点的时间并不会完全一致。目前的实现方案是每个节点都有一个交易打包超时的定时器，时间到了就会产生一个超时打包消息KafkaMessage TimeToCut并提交到链对应的分区上。多个排序服务节点可能产生多个相同的超时打包消息，每个节点都以第一个超时打包消息为准，自动忽略后面相同区块号的超时打包消息，这样各个节点的区块打包过程就一致了。而且当各个节点打包速度不一致时，这也能根据Kafka里的消息独立完成。这是一种把时间同步转换成消息同步的机制。

2.接收交易请求的实现

基于Kafka的排序服务节点提供两种服务角色：

1）对记账节点和应用程序，排序服务节点是服务端，则提供原子的广播服务，包括Broadcast和Deliver服务。

2）对Kafka集群，排序服务节点是客户端，则接收记账节点和应用程序的交易请求并转发给Kafka集群，再从集群获取交易，进行打包生成区块再广播给记账节点。

排序服务节点接收请求的过程，如图6-5所示。

排序服务节点启动的时候会读取配置文件orderer.yaml的地址General.ListenAddress和端口General.ListenPort（如果设置了环境变量ORDERER_GENERAL_LISTENADDRESS和ORDERER_GENERAL_LISTENPORT，以环境变量为准），在指定的地址和端口上启动服务进行监听。接收到Broadcast的gRPC请求以后，会检查类型是否是HeaderType_CONFIG_UPDATE来判断是否是配置交易，配置交易有两种情况。

1）配置更新请求：它会转换成类型为HeaderType_CONFIG的交易请求，转换的时候会对比配置更新的内容和当前最新的配置信息，生成包含最新更新内容的全量配置交易请求。

2）新链创建请求：它会转换成类型为HeaderType_ORDERER_TRANSACTION的交易请求，转换的时候会读取系统链上的配置信息，生成包含组织信息等全量配置交易请求。

排序服务节点作为Kafka集群的生产者提交请求到通道对应的Kafka集群主题和分区上，提交成功就给发送请求方返回状态为Status_SUCCESS的BroadcastResponse。需要注意的是，新链创建请求是提交到系统链通道上进行处理的，这个时候还没有处理新链消息的链包装对象，需要系统链在处理的时候创建出来，交易请求也会记录到系统链的区块里，只是系统链的区块只在排序服务节点上能看到。提交到Kafka集群中的消息处理过程如图6-6所示。

图6-6所示的消息处理已经对细节部分进行了简化，看起来依然比较复杂。总结一下消息处理的过程，主要包含如下几个部分。

图6-5　排序服务节点接收到请求加入队列的处理流程

图6-6　排序服务节点读取消息队列后的处理流程

（1）通道的启动

启动通道的时候会进行一些初始化操作，比如在Kafka上创建主题和默认分区，创建消息消费者协程等。每个通道会在Kafka上以链编号为名称创建主题，由于每个主题的消息只在一个分区内有序，所以在创建主题的时候默认只会创建一个编号为0的分区，这样从排序服务节点提交到Kafka的消息都是有序的。创建主题和分区的方法是给Kafka发送一个CONNECT的消息，这个消息是在系统链接收到创建链交易请求以后提交区块的过程中发送的。所有的排序服务节点都能作为消费者接收到创建链的交易请求，独立进行消息处理，也都会发送CONNECT消息，所以Kafka的同一个主题的同一个分区会有多个相同的CONNECT消息，各个排序服务节点接收到以后自动忽略即可。由于所有的排序服务节点都同时从Kakfa上读取消息，所以第一个接收到创建链消息的节点并不一定是提交创建链消息给Kafka的排序服务节点，也不能确定第一个发送CONNECT消息的节点是哪个节点。创建消息消费者协程的时候需要指定分区的偏移量，新建分区的偏移量是从0开始的，在通道配置更新的时候，偏移量是从排序服务节点本地账本的最新区块元数据里读取的。如果消费者协程在连接Kafka集群的时候，无法连接指定的主题、分区和偏移量时，就会关闭和Kafka的连接，那么排序服务节点接收到这个通道的交易请求时就会直接丢弃，无法进行排序生成新的区块。

（2）交易消息的排序和分割

交易的排序和分割是由blockcutter.Receiver实现的，这和具体的排序服务类型没有关系。Receiver内部维护的数据结构如下：

type receiver struct {
sharedConfigManager config.Orderer
filters *filter.RuleSet
pendingBatch []*cb.Envelope
pendingBatchSizeBytes uint32
pendingCommitters []filter.Committer
}

其中，sharedConfigManager维护的是区块大小、区块包含的最大交易数、区块生成的间隔时间等。filters是消息过滤器规则，每个消息都需要经过消息过滤器的检查，检查不通过就直接丢弃，检查通过的消息会返回提交器。pendingBatch是内部维护的未打包的交易列表，Receiver按照接收到消息的顺序追加到列表后面，由于从Kafka上获取的消息已经是按照某种顺序排序了，这里的操作其实是按照顺序进行的，所以并不需要加锁。pendingBatchSizeBytes记录的是未打包交易列表中所有的交易大小之和，以避免重复计算。pendingCommitters是未打包交易的提交器列表，也是为了缓存，避免重复计算。

receiver的接口Ordered接收交易的请求，经过过滤器检查以后，默认会追加到未打包的交易列表中等待分割打包。区块的分割有如下几种策略。

1）按照区块中包含的交易数量：这是通过通道的配置Orderer.BatchSize.MaxMessage Count设置的。如果追加到未打包交易列表以后所有的交易数达到设定的交易数量，那么就把所有未打包交易列表pendingBatch中的交易全部打包成区块，并返回消息提交器pendingCommitters，以便对每个交易进行不同的提交处理操作。

2）按照区块的大小：这是通过通道的配置Orderer.BatchSize.PreferredMaxBytes设置的。如果追加到未打包交易列表以后所有的交易大小总和达到设定的区块大小，就需要先进行交易分割，把原来未打包的交易列表分割以后，再把新的交易请求追加到pendingBatch中。如果新交易请求的大小已经超过了区块大小的限制，则会单独打包成一个区块。

3）按照区块的间隔时间：这是通过通道的配置Orderer.BatchTimeout设置的。在接收到新区块的第一个交易以后会启动一个定时器，若在超时时间之内还没有足够的交易生成新的区块，就发送一个KafkaMessageTimeToCut消息主动触发交易的分割，每个排序服务节点接收到超时交易分割消息以后，把所有未打包的交易列表pendingBatch中的交易全部打包生成新的区块。

4）按照区块中包含的交易类型：这是内置规则，配置交易和普通交易存放在不同的区块中。配置交易记录了组织、排序服务配置等内容，是单独打包成区块并添加到账本数据中的。创世区块就是一个配置区块，如果对配置信息进行了修改，还会创建新的配置区块，每个配置区块都包含了全量的配置信息，并把配置区块的区块号保存在后续生成的普通区块中。这样，从普通区块就能快速地找到最新的配置区块，不用遍历所有的区块。配置交易的判断是通过配置消息过滤器（configFilter）来实现的，交易能通过Isolated函数的返回值识别出来是否配置交易。

交易分割是对排序服务节点Receiver维护的未打包交易列表pendingBatch进行操作，如果在此过程中排序服务节点出现异常，则重新启动服务也能从异常中恢复。排序服务节点启动的时候会从本地的账本数据中读取最新的区块，读取元数据里保存的LastOffset Persisted，初始化读取Kafka的消费者对象，重构pendingBatch列表进行交易分割。

（3）系统链和普通链的处理

系统链和普通链的处理过程基本类似，系统链是为了维护多链而存在的，记录了联盟链的组织信息等内容，创建新链的请求也是提交到系统链中处理的。系统链创建并启动新链以后的配置更新就由普通链自行处理，所以系统链只是记录普通链的初始配置信息，最新普通链的配置信息还由普通链自行维护。

（4）重复消息的处理

有两种类型的重复消息，一种是创建主题和分区的CONNECT消息，另外一种是超时设置的交易分割消息KafkaMessageTimeToCut。这两种消息都由每个排序服务节点独立生成的，重复的消息会自动过滤。发送完CONNECT消息就完成了它的作用，所以接收到实际的CONNECT消息都不会有业务逻辑的处理。超时分割消息的过滤方法是通过内部记录的指针lastCutBlockNumber来识别的，具体的过程前面已经介绍过。

3.基于Kafka排序服务的最佳实践

本节提供一些基于Kafka排序服务的参考部署。

（1）Kafka和Zookeeper节点数的选择

假设Kafka和Zookeeper的节点数分别用K和Z来表示。

1）K最少需要4个节点，才可以在1个节点宕机以后还能继续提交交易和排序，并且创建新的通道，后面的步骤会说明为什么最少是4个节点。

2）Z选择3、5或7个节点都可以。选择奇数个节点可以避免脑裂，1个节点会存在单点问题，7个以上的节点就太多了。

（2）创建创世区块

编辑configtx.yaml文件，主要修改项如下所示：

·Orderer.OrdererType设置为Kafka

·Orderer.Kafka.Brokers设置至少2个Kafka的节点地址

·Orderer.AbsoluteMaxBytes设置区块最大的字节数（不包括请求头）

（3）配置Kafka集群

选择Kafka作为消息队列需要保证数据的一致性，需要对每个节点都进行如下的配置，如表6-2所示。

表6-2　Kafka节点配置

（4）连接Kafka节点出现异常时的重试设置

排序服务节点和Kafka节点连接设置了一些重试机制，主要分为如下几类。

·快速重试的设置：Kafka.Retry.ShortInterval和Kafka.Retry.ShortTotal，用来控制快速重试的时间间隔和总时间。

·升级重试的设置：Kafka.Retry.LongInterval和Kafka.Retry.LongTotal，用来控制升级重试的时间间隔和总时间。

·网络重试的设置：Kafka.NetworkTimeouts.DialTimeout、Kafka.NetworkTimeouts.ReadTimeout和Kafka.NetworkTimeouts.WriteTimeout，用来控制网络状况的设置，包括连接超时时间、读取数据超时时间、写入数据超时时间。

·元数据重试的设置：Kafka.Metadata.RetryBackoff和Kafka.Metadata.RetryMax，元数据记录了当前可用的Kafka节点地址及其分区等信息。

·生产者重试的设置：Kafka.Producer.RetryBackoff和Kafka.Producer.RetryMax，生产者写入数据时等待节点稳定的退避间隔时间和重试次数。

·消费者重试的设置：Kafka.Consumer.RetryBackoff，消费者读取数据时等待节点稳定的退避间隔时间。

快速重试和升级重试的关系和策略如下所示。

快速重试是在出现连接异常的情况下的重试策略，间隔时间和重试的总时间较短。升级重试是在快速重试失败的情况下，更长时间间隔的重试。排序服务节点连接Kafka节点失败的重试策略如图6-7所示。

图6-7　排序服务节点连接Kafka节点失败的重试策略

在配置文件orderer.yaml中重试设置项如表6-3所示。

表6-3　orderer.yaml中的重试设置项

（5）排序服务节点和Kafka节点之间的安全传输

在生产环境中，对于排序服务节点和Kafka节点之间的加密传输，需要同时在传输的两端进行设置。

但目前Hyperledger Fabric 1.0对于Kafka TLS的实现存在漏洞，所以若想使用Kafka TLS必须修改源码并重新编译生成orderer镜像。

1）修改源码，重新生成orderer镜像。

源码fabric/orderer/kafka/config.go第39行的X509KeyPair参数错误地将证书路径当成了证书内容来使用，45行同样如此。

所以将这部分源码修改为：

if brokerConfig.Net.TLS.Enable {
// 使用方法LoadX509KeyPair创建公私钥对
keyPair, err := tls.LoadX509KeyPair(tlsConfig.Certificate, tlsConfig.PrivateKey)
if err != nil {
logger.Panic("Unable to decode public/private key pair====:", err)
}
// 创建根CA池
rootCAs := x509.NewCertPool()
for _, certificate := range tlsConfig.RootCAs {
// 现根据路径获取证书
caCert, err := ioutil.ReadFile(certificate)
if err != nil {
logger.Panic("Unable to load CA cert file.")
}
if !rootCAs.AppendCertsFromPEM(caCert) {
logger.Panic("Unable to parse the root certificate authority
certificates (Kafka.Tls.RootCAs)===")
}
}
brokerConfig.Net.TLS.Config = &tls.Config{
Certificates: []tls.Certificate{keyPair},
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
MaxVersion: 0, // 最新支持的TLS版本
}
}

2）生成Kafka TLS证书。

假设有3个Kafka节点，分别是kafka0、kafka1、kafka2，以及1个orderer节点。生成证书脚本如下：

# 利用OpenSSL工具生成CA的私钥和证书
openssl genrsa -out ca.key 2048
printf "CN\nBJ\nBJ\nPS\nPS\nps.com\n\n" |openssl req -x509 -new -nodes -key
ca.key -days 3650 -out ca.crt

# 利用OpenSSL工具生成Kafka节点的私钥
openssl genrsa -out server.key 2048

# 利用OpenSSL工具生成Kafka节点的证书
printf "CN\nBJ\nBJ\nPS\nPS\nkafka0\n\n\n\n" |openssl req -new -key server.key
-out kafka0.csr
openssl x509 -req -in kafka0.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out
kafka0.crt -days 3650

printf "CN\nBJ\nBJ\nPS\nPS\nkafka1\n\n\n\n" |openssl req -new -key server.key
-out kafka1.csr
openssl x509 -req -in kafka1.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out
kafka1.crt -days 3650

printf "CN\nBJ\nBJ\nPS\nPS\nkafka2\n\n\n\n" |openssl req -new -key server.key
-out kafka2.csr
openssl x509 -req -in kafka2.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out
kafka2.crt -days 3650

# 利用OpenSSL工具生成客户端证书
openssl genrsa -out client.key 2048
printf "CN\nBJ\nBJ\nPS\nPS\nclient\n\n\n\n" |openssl req -new -key client.key
-out client.csr
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out
client.crt -days 3650

# 利用OpenSSL工具转换证书格式
openssl pkcs12 -export -in kafka0.crt -inkey server.key -out server.pk12 -name
kafka0 -passout pass:test1234
printf "test1234\ntest1234\nY\n\n" | keytool -importkeystore -deststorepass
test1234 -destkeypass test1234 -destkeystore kafka0.keystore.jks -srckeystore
server.pk12 -srcstoretype PKCS12 -srcstorepass test1234 -alias kafka0

openssl pkcs12 -export -in Kkafka1.crt -inkey server.key -out server.pk12 -name
kafka1 -passout pass:test1234
printf "test1234\ntest1234\nY\n\n" | keytool -importkeystore -deststorepass
test1234 -destkeypass test1234 -destkeystore kafka1.keystore.jks -srckeystore
server.pk12 -srcstoretype PKCS12 -srcstorepass test1234 -alias kafka1

openssl pkcs12 -export -in kafka2.crt -inkey server.key -out server.pk12 -name
kafka2 -passout pass:test1234
printf "test1234\ntest1234\nY\n\n" | keytool -importkeystore -deststorepass
test1234 -destkeypass test1234 -destkeystore kafka2.keystore.jks -srckeystore
server.pk12 -srcstoretype PKCS12 -srcstorepass test1234 -alias kafka2

# 导入签名证书到JKS中
printf "test1234\ntest1234\nY\n\n" | keytool -keystore server.truststore.jks
-alias CARoot -import -file ca.crt
printf "test1234\n\n" | keytool -keystore server.truststore.jks -alias kafka0
-import -file kafka0.crt
printf "test1234\n\n" | keytool -keystore server.truststore.jks -alias kafka1
-import -file kafka1.crt
printf "test1234\n\n" | keytool -keystore server.truststore.jks -alias kafka2
-import -file kafka2.crt
printf "test1234\n\n" | keytool -keystore server.truststore.jks -alias client
-import -file client.crt

其中：

①printf为后边OpenSSL或keytool命令的输入参数，在执行命令时去掉这部分，可按照提示手动输入相关参数。

②test1234为密码，可换成自己的正式密码，此处用于测试。

3）服务配置。生成证书后，需配置Kafka和orderer服务。

①orderer服务配置。

orderer作为Kafka服务的客户端，需要配置私钥、证书及CA。具体参数配置如下所示：

- ORDERER_KAFKA_TLS_ENABLED=true
- ORDERER_KAFKA_TLS_PRIVATEKEY=xxx/client.key
- ORDERER_KAFKA_TLS_CERTIFICATE=xxx/client.crt
- ORDERER_KAFKA_TLS_ROOTCAS=[xxx/ca.crt]

其中client.key、client.crt、ca.crt为上一步生成的文件。

②Kafka服务配置。对于每一个kafka都有如下配置：

- KAFKA_LISTENERS=PLAINTEXT://:8092,SSL://:9092
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://:8092,SSL://:9092
- KAFKA_SSL_CLIENT_AUTH=required
- KAFKA_SSL_KEYSTORE_LOCATION=xxx/kafka0.keystore.jks
- KAFKA_SSL_TRUSTSTORE_LOCATION=xxx/server.truststore.jks
- KAFKA_SSL_KEY_PASSWORD=test1234
- KAFKA_SSL_KEYSTORE_PASSWORD=test1234
- KAFKA_SSL_TRUSTSTORE_PASSWORD=test1234
- KAFKA_SSL_KEYSTORE_TYPE=JKS
- KAFKA_SSL_TRUSTSTORE_TYPE=JKS
- KAFKA_SSL_ENABLED_PROTOCOLS=TLSv1.2,TLSv1.1,TLSv1
- KAFKA_SSL_INTER_BROKER_PROTOCOL=SSL

其中kafka0.keystore.jks、server.truststore.jks为上一步生成的文件，test1234为上一步使用的密码。对于不同的Kafka服务要将KAFKA_SSL_KEYSTORE_LOCATION设置为对应的文件。

4）启动服务并执行交易。

（6）Kafka节点的异常处理

Kafka提供的是可回溯的消息队列，消费者读取消息以后还可以重复读取数据，在排序服务中存放的是原始的交易请求。排序服务节点读取交易请求并生成区块以后，就不再需要这些交易信息了，所以Kafka里的数据是可以定期清理的。如果Kafka集群出现异常，并不会丢失所有的数据，只会影响还没有打包的交易请求。极端情况下，可以重构Kafka集群，只要初始化通道对应的主题，填充任意数据使其超过原有的偏移量即可。不用担心Kafka里插入数据带来的安全问题，最终记账还需要经过多重检查，比如消息类型的检查和签名验证、记账节点对背书策略的验证和交易内容的校验。

Kafka只提供崩溃故障容错，并不提供拜占庭容错。就是说，目前版本并不能防止恶意节点攻击。

6.3.4　链消息过滤器

排序服务节点定义了一些规则来对消息进行过滤，每个过滤器处理后的状态分为3种。

·转发（Forward）：不确定消息是否合法，转发给下一个过滤器进行处理；

·接收（Accept）：确认消息合法，调用本过滤器的规则进行处理；

·拒绝（Reject）：该消息是非法消息，不进行下一步的处理。

图6-8所示为系统链中消息过滤器的过滤顺序。

图6-9所示为通道中标准消息过滤器的过滤顺序。

各个过滤器的解释如下。

1）空消息过滤器（emptyRejectRule）：空消息过滤器会检查信封的有效载荷是否为空，如果为空就返回拒绝，不进行后续处理，否则转发给下一个过滤器处理。

2）消息最大字节过滤器（maxBytesRule）：消息最大字节过滤器会检查信封的大小（包括有效载荷和签名）是否超过消息的最大字节数，如果超过就返回拒绝，不进行后续处理，否则转发给下一个过滤器处理。

3）消息签名验证过滤器（sigFilter）：消息签名验证过滤器会检查信封的签名是否满足策略，如果不满足就返回拒绝，不进行后续处理，否则转发给下一个过滤器处理。

图6-8　系统链的消息过滤流程

图6-9　普通链的消息过滤流程

4）系统链消息过滤器（systemChainFilter）：系统链消息过滤器会检查信封是否是系统链的消息，即检查类型是否是HeaderType_ORDERER_TRANSACTION，如果是就会创建一个新的通道以处理这个链上的消息，否则转发给下一个过滤器处理。

5）配置消息过滤器（configFilter）：配置消息过滤器会检查信封是否是通道配置信息，即检查类型是否是HeaderType_CONFIG，如果是就提交给configManager处理，否则转发给下一个过滤器处理。

6）接收消息过滤器（acceptRule）：接收消息过滤器通常是最后一个过滤器，默认的操作是返回接收，不对消息进行任何处理。

6.4　本章小结

本章首先介绍了数据一致性是区块链系统必须满足的特性，以及在分布式系统环境下，可能会遇到的一些问题。不同的共识机制解决的问题不一样，并对不同的算法类型进行对比。然后介绍了Hyperledger Fabric 1.0的共识机制，它是由3个阶段组成的，每个节点都是可插拔的架构设计。最后详细介绍了目前版本的两种排序服务：Solo和Kafka，以及它们的实现和最佳实践。

第7章　实现数据隔离的多链及多通道

多链（multi-chain）是Hyperledger Fabric 1.0新增的一个重要功能。在0.6版中，所有的节点都属于一个链，所有的节点都会同步相同的数据，这会带来几个问题：

·随着业务量的增加，数据会越来越大，每个节点都会同步和存储一些不必要的数据，这增加了数据同步的压力、数据存储的压力和数据处理的压力；

·网络中所有的节点都能读取到所有的数据，一些敏感数据可能分发给其他不应该访问这些数据的节点，这会带来数据安全隐患。

在Hyperledger Fabric 1.0的版本中，增加了对多链的支持。在一个由很多Peer节点组成的区块链网络中，可能同时存在多个链。每个链可能由不同的节点组成，这些节点维护着相同的数据，包括账本数据和状态数据等，不同链的数据是相互隔离的。一个节点根据应用需求，可以加入到不同的链中，在同步数据的时候只同步已加入链的数据，这能减少数据同步的时间，减少数据的存储空间。基于多链的部署结构，在不同链上运行的智能合约访问的是不同的数据，以实现数据的隐私保护，也不会受到数据依赖的限制，提高了并行处理的效率。

多链是全局设计，其实现需要底层架构的支持：

·数据存储对多链的支持；

·链码对多链的支持；

·多通道对多链的支持。

下面我们分开来看每个部分是如何支持多链实现的。

7.1　数据存储对多链的支持

数据存储包含账本数据、索引数据、状态数据和历史数据等几个部分，记账节点包含所有的数据，排序节点只包含账本数据及其索引数据，不包含状态数据及其历史数据。

7.1.1　账本数据

记账节点和排序节点都会存储账本数据，即区块文件。

前面的章节已经介绍过，记账节点的账本数据是基于文件系统存储的，每个链的账本数据存储在不同的目录下。只有属于某个链，才会存在以这个链的通道命名的账本目录。

记账节点的账本数据存储目录一般是/var/hyperledger/production/ledgersData/chains/chains，其中/var/hyperledger/production可以通过core.yaml文件中的peer.fileSystemPath选项指定，后面的ledgersData/chains/chains是记账节点中固定的目录后缀。下面是一个记账节点的账本数据目录结构：

root@peer0:/var/hyperledger/production/ledgersData/chains/chains# tree .
.
|-- businesschannel
| `-- blockfile_000000
`-- pocchannel
`-- blockfile_000000

2 directories, 2 files

这个目录下面有两个目录：businesschannel和pocchannel。它们代表的是两个通道，也就是两个链的数据，每个链现在只有一个区块文件，blockfile_是文件名中固定的前缀，000000是固定的6位占位符，下一个文件名会依次递增。从这个目录结构可以看到，记账节点在底层账本数据存储的时候就对不同链的数据进行了隔离。

排序节点会存储所有链的账本数据，排序节点除了可以选择序列化区块文件的格式外，还支持JSON文件格式和内存数据结构的账本数据，后面两种都只在测试环境下使用。序列化区块文件和JSON文件格式区块文件的存储目录一般是/var/hyperledger/production/orderer/chains，其中，orderer/chains是固定的目录后缀。同样地，不同链的账本数据存储在以通道名称为目录名称的目录中，以实现不同链账本数据的物理隔离。内存数据的账本数据没有持久化的存储，不同链的账本数据存储在不同的数据结构中。

7.1.2　索引数据

记账节点和排序节点都会给账本数据建立索引，不同的是排序节点只会建立以BlockNum为属性的索引。

索引文件存储的目录是/var/hyperledger/production/ledgersData/chains/index，其中，ledgers Data/chains/index是记账节点上固定的目录后缀，排序节点上的目录后缀是orderer/index。下面是一个记账节点上的索引数据的目录：

root@peer0:/var/hyperledger/production/ledgersData/chains/index# tree
.
|-- 000002.ldb
|-- 000007.log
|-- CURRENT
|-- LOCK
|-- LOG
`-- MANIFEST-000008

0 directories, 6 files

索引数据是存储在LevelDB数据库里的，数据库的类型目前是不可选的。LevelDB是持久化的K-V数据库，在保存索引的时候会加上ledgerid作为前缀，当然生成的组合键在构造的时候是要先转换成[]byte数组的。由于索引数据存储在同一个数据库中，所以对于不同链的数据，索引数据的实现是逻辑隔离的，并非是物理隔离的。

7.1.3　状态数据

排序节点不需要查询具体的交易信息和状态数据，也不会存储状态数据及其历史数据。

Peer节点上状态数据存储的目录是/var/hyperledger/production/ledgersData/stateLeveldb，其中，ledgersData/stateLeveldb是固定的后缀：

root@peer0:/var/hyperledger/production/ledgersData/stateLeveldb# tree
.
|-- 000002.ldb
|-- 000007.log
|-- CURRENT
|-- LOCK
|-- LOG
`-- MANIFEST-000008

0 directories, 6 file

状态数据也是基于K-V存储的，同一个节点的状态存储在同一个数据库中，没有进行物理隔离。和索引数据不同的是，状态数据是和chaincodeID相关的，不同chaincodeID的数据是逻辑隔离的，而chaincodeID同样是以chainID为前缀进行了逻辑隔离。

7.1.4　历史数据

历史数据存储的目录是/var/hyperledger/production/ledgersData/historyLeveldb，其中，ledgersData/historyLeveldb是固定的后缀：

root@peer0:/var/hyperledger/production/ledgersData/historyLeveldb# tree
.
|-- 000002.ldb
|-- 000010.ldb
|-- 000012.log
|-- CURRENT
|-- LOCK
|-- LOG
|-- LOG.old
|-- MANIFEST-000013
`-- level-party.sock

0 directories, 9 files

历史数据目前内置的数据库是LevelDB，也是不可替换的。记录的是状态数据的历史记录，同状态数据一样，通过在构建chaincodeID的时候增加ChainID前缀来逻辑隔离不同链的数据。

7.2　链码对多链的支持

链码是Hyperleger Fabric 1.0提供的智能合约方案，实现了交易的模拟执行。链码从多个纬度对多链提供了支持，比如链码的生命周期管理、链码和背书节点的通信、链码的部署方法等。

7.2.1　链码的生命周期管理

智能合约在Hyperledger Fabric 1.0上称为链码（Chaincode），是独立运行的应用程序，只接收启动它的背书节点的指令，执行指定的业务逻辑。在多链的情况下，同一个智能合约可能会在不同的链上运行。为了重用智能合约代码，智能合约的部署拆分成了安装和实例化两个步骤，安装只是把链码的源代码序列化后和链码名称、版本等封装成ChaincodeDeploymentSpec保存到Peer节点上，链码安装跟具体的链没有关系，也不需要ChainID参数。

换个说法，不同链的链码是没有隔离的，也就是说，在一个链安装的链码可能和另外一个链的链码产生冲突。链码安装的时候会检查是否存在相同名称和版本的链码，如果不同的上层应用同时都部署了相同的链码和版本，可能存在一个链的链码安装成功，另外一个链的链码安装失败的情况。

实例化和链码升级，是在指定的链上操作的，实际过程分为两个步骤，第一步是调用系统链码LSCC的部署操作，通过LSCC把链码计算哈希后生成的ChaincodeData存放在状态数据库中；第二步是从文件系统中读取保存的链码源码，生成镜像后执行初始化操作。链码操作包括初始化和调用，它们都是在具体链上操作的，链码镜像的命名规则是：

NetworkID-PeerID-ChaincodeName-ChaincodeVersion-SHA256(ChainID)

链码镜像名称的最后一部分是对ChainID计算SHA256哈希后再转换成十六进制的字符串，在逻辑上不同链的链码会有不同的镜像名称。启动的链码容器命名和镜像一样，只是会把“：”替换成“_”。这样，不同链的链码执行是可以在不同的环境中隔离的。不过，实际在启动链码容器的时候并没有指定ChainID这个参数，就是说目前不同链上相同链码是运行在同一个容器中的。但即使运行在相同的链码容器中，也会通过ChainID进行逻辑隔离。详细的通信机制见下一节的介绍。

7.2.2　链码和背书节点的通信

链码容器启动以后，会和启动它的背书节点建立gRPC连接。应用程序或者命令行通过gRPC连接给背书节点发送请求，背书节点校验通过后会通过链码和背书节点建立的gRPC连接将请求发送给链码去执行。链码的执行本身是和具体链无关的，链码容器也不会在本地保存任何数据，是一个无状态的执行环境。需要访问或者写入状态数据时，则通过建立好的gRPC连接发送请求给背书节点，再进行后续的业务逻辑处理。就是说，在链码这一端，是不区分链的，所以不同链才可以共用相同的链码容器。

链码容器启动的时候，和背书节点建立的gRPC连接没有和链相关的信息，链码通过建立好的gRPC连接发送给背书节点的第一个信息是：ChaincodeMessage_REGISTER，内容是序列化后的ChaincodeID，在不同链上其也可能是相同的（虽然ChaincodeID的命名规则是：ChaincodeName：ChaincodeVersion/ChainID，目前的命名也是没有ChainID标识的）。不管是多个容器执行链码，还是在同一个容器中执行链码，链码运行的结果都会通过gRPC发送给背书节点，背书节点怎么知道是哪个链上的操作呢？

链码运行是以交易号作为标识的，客户端发起Proposal请求时需要指定发送到哪个链上，背书节点会把交易号和链标识进行关联，再把消息发送给链码去执行，接收到链码返回的请求就知道属于哪个链了。背书节点的内部维护了一个运行中的链码映射表，键是ChaincodeID，值是封装了Handler的链码运行时环境chaincodeRTEnv，就是同一个链码都对应同一个链码运行时环境，负责处理链码发送过来的消息。Handler内部维护了一个交易的上下文映射表txCtxs，键是交易号txid，值是transactionContext的结构体，如下所示：

type transactionContext struct {
chainID string
signedProp *pb.SignedProposal
proposal *pb.Proposal
responseNotifier chan *pb.ChaincodeMessage

// 记录范围查询的迭代器
queryIteratorMap map[string]commonledger.ResultsIterator

txsimulator ledger.TxSimulator
historyQueryExecutor ledger.HistoryQueryExecutor
}

其中，chainID就是链的标识，通过txsimulator对不同链的状态数据库进行操作，实现不同链数据处理的逻辑隔离。交易上下文映射表txCtxs在每次调用链码时都会更新，链码容器启动后就会给Handler发送sendReady消息，给每个交易创建一个映射表项。接收到链码发送过来的消息后，通过txCtxs能对不同的链进行操作。

7.2.3　链码的部署和调用

多链的实现也会对链码的部署和调用方式有影响，首先需要创建一个链，fabric-sdk-go的接口是：

type FabricClient interface {
NewChannel(name string) (Channel, error)
CreateChannel(request CreateChannelRequest) (txn.TransactionID, error)
}

type CreateChannelRequest struct {
// 必填 - channel名称
Name string
// 必填 - 发送更新请求的Orderer
Orderer Orderer
// 可选 - envelope object包含了初始化channel所需的设置以及签名
// 可通过命令行工具configtx来创建
Envelope []byte
// 可选 - 通过package中的buildChannelConfig()方法来构建ConfigUpdate对象
Config []byte
// 可选 - 使用`config`参数时，指定创建策略所需的签名集合
// 详细参考 signChannelConfig()方法
Signatures []*common.ConfigSignature

// InvokeChannelRequest允许传入TransactionID参数
// 该请求结构虽然包含一致性字段，但也可能删除
TxnID txn.TransactionID
}

其中，NewChannel只是在本地创建一个Channel对象，用来进行后续的初始化和其他操作。实际的创建链请求需要调用CreateChannel，请求参数CreateChannelRequest里的Name是和NewChannel中的name对应的。

然后链码的部署分成两个步骤，链码安装InstallChaincode和链码的实例化Send InstantiateProposal，其中InstallChaincode是定义在FabricClient中的，是和链无关的操作，SendInstantiateProposal是定义在Channel中的。

type FabricClient interface {
InstallChaincode(chaincodeName string, chaincodePath string, chaincodeVersion
string, chaincodePackage []byte, targets []Peer) ([]*txn.TransactionProposal
Response, string, error)
}

type Channel interface {
SendInstantiateProposal(chaincodeName string, args []string, chaincodePath
string, chaincodeVersion string, chaincodePolicy *common.SignaturePolicy
Envelope, targets []txn.ProposalProcessor) ([]*txn.TransactionProposalResponse, txn.
TransactionID, error)
}

最后，在链码调用的时候需要使用创建好的Channel对象：

func InvokeChaincode(client fab.FabricClient, channel fab.Channel, targets []
apitxn.ProposalProcessor,
eventHub fab.EventHub, chaincodeID string, fcn string, args []string,
transientData map[string][]byte) (apitxn.TransactionID, error)

7.3　多通道对多链的支持

排序节点同时会给多个链提供服务，会接收到多个链提交过来的交易并形成不同链的区块。Hyperledger Fabric 1.0采用多通道的方法来隔离不同链的数据。

发送给排序服务的节点怎么区分是哪个链的数据呢？客户端在接收到背书节点返回的执行结果后，会生成最终的交易。其实，交易里面会包含发送给背书节点的Proposal请求，每个Proposal都会包含请求头common.Header，其定义如下：

type Header struct {
ChannelHeader []byte `protobuf:"bytes,1,opt,name=channel_header,json=chann
elHeader,proto3" json:"channel_header,omitempty"`
SignatureHeader []byte `protobuf:"bytes,2,opt,name=signature_header,json=sig
natureHeader,proto3" json:"signature_header,omitempty"`
}

其中的ChannelHeader是包含了通道编号的序列化字节数组，如下所示：

// Header是一种通用的重播预防，包含重放签名的身份信息
type ChannelHeader struct {
Type int32 `protobuf:"varint,1,opt,name=type" json:"type,omitempty"`
// 协议版本信息
Version int32 `protobuf:"varint,2,opt,name=version" json:"version,omitempty"`
// 发件人创建消息的本地时间
Timestamp *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=timestamp"
json:"timestamp,omitempty"`
// ChannelId用于表示消息绑定channel的标识
ChannelId string `protobuf:"bytes,4,opt,name=channel_id,json=channelId"
json:"channel_id,omitempty"`
// 端到端的唯一标识符
// - 高级设置，如用户终端或SDK
// - 传递给背书节点 （用于唯一性检查）
// - header会随消息一直传递，将被committer获取（也用于唯一性检查）
// - 会存入账本
TxId string `protobuf:"bytes,5,opt,name=tx_id,json=txId" json:"tx_id,
omitempty"`
// header中的Epoch的定义取决于块高度
// response中的Epoch表示逻辑窗口时间. 以下两种情况时，peer节点才接受提案响应
// 1. 消息中的epoch与当前epoch匹配
// 2. 消息只在当前epoch中出现一次（即没有被重播）
Epoch uint64 `protobuf:"varint,6,opt,name=epoch" json:"epoch,omitempty"`
// 可以根据header类型进行扩展
Extension []byte `protobuf:"bytes,7,opt,name=extension,proto3"
json:"extension,omitempty"`
}

排序服务在接收到交易请求后，会先反序列化得到ChannelId，在不同的通道上进行排序打包生成区块。多通道（Multi-channel）部分的内容详见第6章。

7.4　命令行和SDK对多链的支持

我们在前面的章节已经介绍过命令行的使用，其中可以指定一个-C参数代表在指定的通道上操作，在入口处提供了对多链的支持。

我们在第10章还会看到在超级账本提供给应用程序的SDK中，也提供了多链的接口。

7.5　关于系统链

系统链是一个特殊的链，含有系统层面全局配置区块链网络的联盟及组织信息、MSP信息和策略信息等，只存在于排序服务中。涉及一些信息的修改，比如增加一个组织，增加排序服务节点，这些都会在系统链上增加一个配置区块。整个系统有且只有一个系统链，系统链是通过创世区块配置的，排序服务启动的时候通过ORDERER_GENERAL_GENESISFILE环境变量指定创世区块文件并创建系统链。系统链的名称可以在创建创世区块的时候通过工具configtxgen的channelID参数指定，默认的系统链名称是testchainid，是否是系统链的判断方法就是配置区块信息里是否有联盟信息配置项。关于创世区块的详细信息也请参考前面已经介绍过的第6章的相关内容。

7.6　本章小结

在本章中，我们介绍了在Hyperledger Fabric 1.0中支持的多链及其内部实现。在整个区块链网络中，支持多个链同时运行是一个系统工程，涉及所有的参与方，包括应用程序、Peer节点、排序服务节点等，如何从这些节点中动态地组建一个链，需要很多方面的支持。从业务流程上看，也涉及多个环节，包括创世区块的创建、链码的部署、链码的调用、链码的运行、交易排序、交易验证和记账等。不同的节点加入到不同的链中，还有很多权限和策略的控制。这是一种全新的架构，增加了很多的复杂度。Hyperledger Fabric 1.0相对于0.6版本而言，很多比较难理解和操作的部分都跟多链和多通道相关。理解了多链的目的后，其实自然就知道为什么要这么设计了。

第8章　基于数字证书的成员管理服务

Hyperledger Fabric 1.0基于PKI体系，生成数字证书以标识用户的身份。每个身份和成员管理服务提供商（Membership Service Provider，MSP）的编号进行关联，本章将会介绍如何对用户身份进行认证。

8.1　实现成员管理的MSP

MSP（Membership Service Provider）：即成员管理服务提供商，是Hyperledger Fabric 1.0中引入的一个组件，其目的是抽象化各成员之间的控制结构关系。MSP将证书颁发、用户认证、后台的加密机制和协议都进行了抽象。每个MSP可以定义自己的规则，这些规则包括身份的认证，签名的生成和认证。每个Hyperledger Fabric 1.0区块链网络可以引入一个或者多个MSP来进行网络管理。这样就将成员本身和成员之间的操作、规则和流程都模块化了。

8.1.1　MSP成员的验证

我们先来看一下MSP的成员身份及身份标识符定义：

type identity struct {
// 身份标识符
id *IdentityIdentifier
// X.509证书
cert *x509.Certificate
// 公钥
pk bccsp.Key
// 所属的MSP
msp *bccspmsp
}

type IdentityIdentifier struct {
// MSP标识
Mspid string
// 身份编号
Id string
}

从上面的定义中我们可以看到，成员身份是基于标准的X.509证书的。利用PKI体系给每个成员颁发数字证书，结合所属的MSP进行身份认证和权限控制。根CA证书（Root Certificate）是自签名的证书，用根CA证书的私钥签名生成的证书还可以签发新的证书，形成一个树型结构。中间CA证书（Intermediate Certificate）是由其他CA证书签发的，也可以利用自己的私钥签发新的证书。签发证书是一个信任背书的过程，从根CA证书到最终用户证书形成一个证书信任链（Chain of Trust）。在PKI体系中，可以利用CRL（Certificate Revocation List）或者OCSP（Online Certificate Status Protocol）管理证书的有效性。在超级账本中，MSP利用PKI的部分特性来管理证书的有效性。

1）MSP标识的检查：身份证书都是和MSP绑定的，必须有相同的MSP标识才能验证证书的有效性。Peer节点的Gossip通信和部分系统链码的调用都要求调用者身份和本地MSP标识相同，背书请求在通道管理策略验证的时候也会检查MSP成员是否有写入权限。

2）证书路径的检查：除了MSP标识的检查，还会对证书签名有效性进行检查，主要是证书路径的检查，校验根CA证书、中间CA证书是否有效，是否有从身份证书到可信根CA证书的有效路径。特别说明一下，在证书验证的时候并不校验证书的有效期，会强制设置当前时间为证书起始时间的下一秒，这确保有效期验证通过。MSP目录下的cacerts和intermediatecerts子目录签发的证书都是有效的，证书校验的时候需要检查是否有到可信根证书的有效路径。

3）CRL的检查：最后是检查证书是否被吊销，目前只支持CRL的方式，并不支持OCSP。CRL会包含在本地MSP的crls子目录下，是由CA证书签发的包含被吊销证书序列号的证书文件。在通道的MSP配置中，也会包含CRL列表。在更新通道配置的时候可以发布吊销的证书。

8.1.2　MSP的目录结构

下面先介绍生成MSP目录的一些必要准备工作。

1.MSP的配置说明

在每一个Peer节点和排序服务节点上设置MSP目录后，Peer节点和排序服务节点就有了签名证书，在通道节点之间传输数据时，要验证节点的签名。

为了能够标识MSP，每个MSP需要指定一个名称，如org1、org2等。在通道的MSP成员规则中可以用MSP名称来代表一个联盟（Consortium）、组织（Organization）或者部门（Organization Division）。若在创世区块中检测到两个MSP用同一个MSP名称，则排序服务节点将启动失败。

MSP的默认实现是基于X.509证书格式的，根据RFC5280文档的内容，给一些MSP的配置参考，如表8-1所示。

表8-1　MSP配置参考

节点需要进行如下的配置才能使用MSP进行签名或者验签：

1）用于节点签名的签名密钥（目前只支持ECDSA密钥）。

2）通过MSP验证是有效的X.509证书将作为节点证书。

怎么才能是MSP的有效身份（Identity）呢？这需要同时满足如下几个条件：

1）身份证书需要符合X.509证书标准，且有到根CA证书或者中间CA证书可验证的证书路径。

2）身份证书不在证书吊销列表中。

3）在X.509证书的OU字段至少包含一个在MSP中配置的部门。

和普通的X.509证书不同的是，MSP的身份证书是没有有效期的，除非被添加到证书吊销列表中。

2.生成MSP证书

有很多的工具可以生成X.509证书，比如广泛使用的OpenSSL。需要注意的是，在Hyperledger Fabric 1.0中，不支持包含RSA密钥的证书。本例中采用cryptogen工具来生成MSP证书，生成过程如下：

# 下载fabric源码，存放在FABRIC_SRC_DIR目录下
git clone git@github.com:hyperledger/fabric.git，
# 生成工具在$FABRIC_SRC_DIR/hyperledger/fabric/examples/e2e_cli目录下
cd $FABRIC_SRC_DIR/hyperledger/fabric/examples/e2e_cli
# 调用e2e的工具生成证书
./generateArtifacts.sh

执行完以上三步操作就会在目录$FABRIC_SRC_DIR/hyperledger/fabric/examples/e2e_cli下产生crypto-config文件夹。在这个MSP中配置了两个组织（即org1与org2），每个组织下有两个Peer节点（即peer0和peer1）：一个排序服务节点，一个CA节点。部分目录的结构如下所示：

├── ordererOrganizations
│ └── example.com
│ ├── ca
│ │ ├── 4514ec148b0b79b58957131cf0f5d516be4b5d79b10f90f5f63ec208d71c8
5e1_sk
│ │ └── ca.example.com-cert.pem
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@example.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.example.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.example.com-cert.pem
│ ├── orderers
│ │ └── orderer.example.com
│ │ ├── msp
│ │ │ ├── admincerts
│ │ │ │ └── Admin@example.com-cert.pem
│ │ │ ├── cacerts
│ │ │ │ └── ca.example.com-cert.pem
│ │ │ ├── keystore
│ │ │ │ └── 550422b59f3ca8cf0df4a9782f1041d906b9100dab8bdf74
409c445efdb96437_sk
│ │ │ ├── signcerts
│ │ │ │ └── orderer.example.com-cert.pem
│ │ │ └── tlscacerts
│ │ │ └── tlsca.example.com-cert.pem
│ │ └── tls
│ │ ├── ca.crt
│ │ ├── server.crt
│ │ └── server.key
│ ├── tlsca
│ │ ├── 88fd39ff8fe5e0d103d804a4a3c8e172b7f6ff5adbcf6182b3cd6c1aa2d68
fa5_sk
│ │ └── tlsca.example.com-cert.pem
│ └── users
│ └── Admin@example.com
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@example.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.example.com-cert.pem
│ │ ├── keystore
│ │ │ └── 000c4fd8fa15b46d1ab0f6d3b538b661585023a1366058c3
93e75ecffa343f9a_sk
│ │ ├── signcerts
│ │ │ └── Admin@example.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.example.com-cert.pem
│ └── tls
│ ├── ca.crt
│ ├── server.crt
│ └── server.key
└── peerOrganizations

crypto-config目录下会生成两个目录，ordererOrganizations和peerOrganizations，它们分别代表排序服务节点和Peer节点的MSP配置信息。peerOrganizations目录下有两个组织org1.example.com和org2.example.com，子目录结构同ordererOrganizations的example.com。我们以ordererOrganizations的example.com为例说明每个组织的目录结构，如表8-2所示。

表8-2　组织的目录结构

MSP目录的说明，如表8-3所示。

表8-3　MSP的目录结构

一个config.yaml的示例，如下所示：

OrganizationalUnitIdentifiers:
- Certificate: "cacerts/cacert.pem"
OrganizationalUnitIdentifier: "fabric-ca"

在每个节点或者用户下除了msp目录，还有一个tls目录，用来进行TLS连接的配置，如表8-4所示。

表8-4　TLS的目录结构

再来看下crypto-config文件夹，其实只需要设置节点下面对应的MSP就可以了，为了方便节点部署，里面有一些冗余文件。比如ordererOrganizations/example.com/msp目录下子目录文件都会在orderer Organizations/example.com/orderers/orderer.example.com/msp目录下，这样会增加节点orderer.example.com对应的签名证书和签名私钥。ordererOrganizations/example.com/orderers/orderer.example.com其实就是管理员ordererOrganizations/users/Admin@example.com。还有根CA证书和TLS根CA证书等也是冗余存储的。

3.配置节点的MSP证书

生成MSP证书后，就可以启动Peer节点和排序服务节点了，配置说明如表8-5所示。

表8-5　MSP配置说明

一个节选的docker-compose.yaml示例文件如下所示：
orderer.example.com：

orderer.example.com:
container_name: orderer.example.com
image: hyperledger/fabric-orderer
environment:
- ORDERER_GENERAL_LOGLEVEL=debug
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_GENESISMETHOD=file
- ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.
genesis.block
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp
# 使能TLS
- ORDERER_GENERAL_TLS_ENABLED=true
- ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/
server.key
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/
server.crt
- ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: orderer
volumes:
- ../channel-artifacts/genesis.block:/var/hyperledger/orderer/orderer.genesis.block
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp:/var/hyperledger/orderer/msp
- ../crypto-config/ordererOrganizations/example.com/orderers/orderer.
example.com/tls/:/var/hyperledger/orderer/tls
ports:
- 7050:7050

peer0.org1.example.com:
container_name: peer0.org1.example.com
extends:
file: peer-base.yaml
service: peer-base
environment:
- CORE_PEER_ID=peer0.org1.example.com
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051
- CORE_PEER_CHAINCODELISTENADDRESS=peer0.org1.example.com:7052
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.example.com:7051
- CORE_PEER_LOCALMSPID=Org1MSP
volumes:
- /var/run/:/host/var/run/
- ../crypto-config/peerOrganizations/org1.example.com/peers/
peer0.org1.example.com/msp:/etc/hyperledger/fabric/msp
- ../crypto-config/peerOrganizations/org1.example.com/peers/
peer0.org1.example.com/tls:/etc/hyperledger/fabric/tls
ports:
- 7051:7051
- 7052:7052
- 7053:7053

对于配置文件中环境变量和节点YAML之间的映射关系，请参考节的配置参数传递规则。从上面的配置文件可以看出，节点配置直接用到了cryptogen生成的目录结构文件。排序服务节点orderer.example.com的本地MSP名称是OrdererMSP，本地MSP路径是crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/msp，TLS的CA根证书、密钥、证书文件目录是在crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls下。Peer节点peer0.org1.example.com的本地MSP名称是Org1MSP，本地MSP路径是crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp，TLS的CA根证书、密钥、证书文件目录是在crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls下。

8.1.3　MSP的配置最佳实践

本节将详尽地描述能够满足常用场合下的MSP配置最佳实践。

1.组织与MSP之间建立映射关系

我们建议实际的组织和MSP之间建立一一对应关系。也可以选择其他类型的映射关系，我们一起来看一下。

1）一个组织对应多个MSP的情况。这种情况是一个组织有多个部门，从方便管理的角度或者隐私保护的角度而言，每个部门都要设置不同的MSP。每个Peer节点只设置一个MSP，同一组织内不同MSP的Peer节点之间不能互相认证，这样相同组织的不同部门之间不会同步数据。

2）多个组织对应一个MSP。这种情况是同一个联盟的不同组织之间采用相同的成员管理架构，数据会在不同组织之间同步。在前面的章节中我们已经了解到，在Peer节点之间的Gossip通信中，数据是在相同通道配置了相同MSP的Peer节点之间同步的。如果多个组织对应一个MSP，则数据就不会限制在组织内部，会跨组织进行同步。

其实这是由MSP定义的粒度问题，一个MSP可以和一个组织对应，也可以和多个组织对应，还可以和一个组织内部的多个部门对应，根据MSP配置好Peer节点后，数据同步就限制在了MSP定义的范围内。

2.一个组织内部实现不同的权限控制

一个组织内部有多个部门，从而实现不同部门的权限控制。有两种方法可以实现这个场景：

（1）给组织内的所有部门定义一个MSP

给Peer节点配置MSP的时候，包含相同的可信根CA证书列表、中间CA证书、管理员证书，不同的Peer节点设置不同的所属部门。节点所属的部门是利用证书和部门之间映射的OrganizationalUnitIdentifiers定义的，它包含在MSP目录下的配置文件“config.yaml”中，本章前面已经介绍过。在第3章的相关部分，还介绍过可以按照基于部门验证的方法来定义交易背书策略和通道管理策略，这样就可以实现不同的权限控制了。这种方法会有一个问题，就是数据实际还是会在不同的Peer节点之间同步。因为Peer节点在识别组织身份类型OrgIdentityType的时候获取的是MSP标识，它会认为通道内相同MSP的节点都是可以分发数据的。

（2）给组织内的每个部门单独定义MSP

给Peer节点配置MSP的时候，不同部门配置的可信中间CA证书、管理员证书可以是不同的，不同部门成员的证书路径也是不同的。这种方式解决了所有部门定义在一个MSP中的问题，但是会带来管理上的复杂度。另外一个办法是每个部门都设置不同的MSP，利用证书和部门之间映射的OrganizationalUnitIdentifiers实现不同部门的权限控制，数据同步仍然会限制在组织的不同部门内，这同样也会有管理上的复杂度。

3.不同类型的节点分别使用不同的MSP

可能有这样的需求，希望给客户端、Peer节点、排序服务节点分别设置不同的MSP，因为身份信息会包含MSP标识，设置不同的MSP后就能确定身份类型。这在很多情况下是有用的，比如能够验证背书的确是由Peer节点签名的，而不是由客户端或者排序服务节点签名的。实际上，这样设置会带来一些问题，我们一起来分析一下。

不同节点类型分别设置不同的MSP，对应的可信中间CA证书也不一样。在通道设置的时候需要包含不同的MSP及其可信中间CA证书，组织内的不同MSP成员才可以访问通道里的数据。同时背书策略可以指定只有Peer节点对应的MSP成员背书的交易才有效，这就能实现只能是Peer节点才能背书签名的目的。同一个组织按不同节点类型设置MSP之后，Peer节点都有相同的MSP，Peer节点之间的数据同步不会受到影响，但会影响Peer节点和客户端之间的交互。

在Peer节点上某些系统链码的调用是和本地MSP相关的，比如只执行本地MSP配置的由管理员发起的安装链码（install）请求、加入通道（JoinChain）请求等。应用程序执行这类系统调用时还需要用到Peer节点相同MSP的管理员签名密钥和证书。当然，如果不把这类系统调用功能放在应用程序中去实现，正常的背书请求响应是没有问题的，因为Peer节点接收背书请求检查的是有没有通道的写入权限。因此可以在配置通道管理策略时增加客户端的MSP，客户端就可以向Peer节点提交请求了。

还有一个问题是，注册事件回调函数的时候，Peer节点只处理和本地MSP相同的客户端发起的请求，这个时候若Peer节点和客户端属于不同的MSP，就会拒绝客户端发起的请求。如果业务依赖事件处理的话，应用程序和Peer节点还是需要采用相同的MSP。

4.区分管理员和CA证书

不要把可信根CA证书或者中间CA证书设置成MSP管理员证书，这样能把成员管理、签发证书与验证证书等不同职责拆分开来，方便管理和问题定位，这其实是一种常见的安全做法。

5.区分根CA证书和TLS的根CA证书

MSP的根CA证书和TLS根CA证书以及相关的中间CA证书需要存放在不同的文件夹中，这是为了避免混淆不同类别的证书。虽然并没有禁止根CA证书和TLS根CA证书采用相同的证书，在生产环境中建议还是区分开。

6.吊销已经颁发的证书

由于权限管理或者其他方面的原因，已经颁发的证书是可以被吊销的。参考前面MSP成员证书的有效性验证过程，吊销已颁发的证书有多种办法：

1）删除中间CA证书：删除intermediatecerts目录下的证书，这样由中间CA证书签发的证书都属于无效证书。

2）增加CRL列表：可以把中间CA证书或者单个证书添加到CRL列表中。

重新配置节点本地MSP的时候，要删除intermediatecerts目录下的证书或者在crls目录下增加CRL列表证书就可以了。重新配置通道MSP的时候，需要提交CONFIG_UPDATE的交易请求，生效以后修改排序服务节点和Peer节点的MSP配置。

8.2　颁发数字证书的Fabric CA

本节介绍可选的Fabric CA服务，这是官方提供数字证书管理的一个默认实现。

8.2.1　概述

Fabric CA是超级账本的数字证书认证中心，它提供了如下功能：

·用户信息的注册；

·数字证书的发行；

·数字证书的延期与吊销。

Fabric CA由服务端和客户端组件组成，图8-1阐述了Fabric CA的组件在整个超级账本架构中的作用。

图8-1　Fabric CA架构示意图

Fabric CA服务端提供用户登记和注册的数字证书管理功能，数据存储后端可以是MySQL、PostgreSQL、LDAP等。如果配置了LDAP，用户信息存在于LDAP中，而不是存放在MySQL或者PostgreSQL数据库中。通过数据存储和业务逻辑的分离，Fabric CA服务能够采用无状态的集群部署，通过HAProxy等软件实现负载均衡功能，实现服务的高可用。Fabric CA服务端提供了RESTful的接口供客户端工具和HFC SDK访问。手工部署的方式可以采用客户端工具来实现，如果集成到应用程序中，可以采用HFC SDK来实现。通过HFC SDK注册的证书有多种类型，包括user、app、peer、orderer、client、validator、auditor等，这些在Peer节点、Orderer节点的部署时会用到，应用程序提交交易请求的时候也会用到。

8.2.2　Fabric CA服务端的安装部署

本节介绍Fabric CA服务端的安装部署。

（1）准备工作

安装Fabric CA之前需要做一些准备工作：

·安装Go语言1.9以上版本；

·设置好GOPATH环境变量；

·确认libtool和libtdhl-dev已安装。

Go语言的安装请参考第11章的相关内容。在Ubuntu系统下可以直接通过命令行安装libtool和libtdhl-dev：

sudo apt install libtool libltdl-dev

（2）安装Fabric CA服务端和客户端

使用下面的命令即可将fabric-ca-server和fabric-ca-client安装至$GOPATH/bin下：

go get -u github.com/hyperledger/fabric-ca/cmd/...

（3）通过命令行启动Fabric CA服务

初始化fabric-ca-server：

fabric-ca-server init -b admin:adminpw

-b选项提供注册用户的名称和密码，如果没有使用LDAP，这个选项是必需的。默认的配置文件的名称为fabric-ca-server-config.yaml，路径可以自定义。

启动fabric-ca-server，使用默认设置：

fabric-ca-server start -b admin:adminpw

-b选项提供注册用户的名称和密码，如果没有使用LDAP，这个选项是必需的。默认的配置文件的名称为fabric-ca-server-config.yaml，路径可以自定义。

（4）通过Docker Hub下载的镜像启动Fabric CA服务

访问https://hub.docker.com/r/hyperledger/fabric-ca/tags ，找到你想要获取的fabric-ca的版本。进入代码目录$GOPATH/src/github.com/hyperledger/fabric-ca/docker/server，在编辑器中打开docker-compose.yml，修改image为指定的镜像版本。下面是这个文件的示例：

fabric-ca-server:
image: hyperledger/fabric-ca:x86_64-1.0.0
container_name: fabric-ca-server
ports:
- "7054:7054"
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
volumes:
- "./fabric-ca-server:/etc/hyperledger/fabric-ca-server"
command: sh -c 'fabric-ca-server start -b admin:adminpw'

打开一个终端，进入docker-compose.yml所在的目录，执行如下命令：

docker-compose up -d

如果指定的镜像不存在，则Docker会主动拉取此镜像，然后启动fabric-ca服务实例。

（5）通过源码编译生成的Docker镜像，以启动Fabric CA服务

你可以通过下面的命令使用docker-compose编译和启动服务：

cd $GOPATH/src/github.com/hyperledger/fabric-ca
make docker
cd docker/server
docker-compose up -d

hyperledger/fabric-ca镜像同时包含Fabric CA的服务端fabric-ca-server和客户端fabric-ca-client。

（6）fabric-ca-server命令行选项

//帮助:
fabric-ca-server [command]
//子命令:
init // 初始化fabric-ca-server
start // 启动fabric-ca-server
version // 打印fabric-ca-server版本信息
//选项:
--address string // fabric-ca-server的监听地址（默认"0.0.0.0"）
-b, --boot string // 指定启动时的用户和密码，格式为user:pass
--ca.certfile string // PEM编码的证书文件（默认"ca-cert.pem"）
--ca.chainfile string // PEM编码的链文件（默认"ca-chain.pem"）
--ca.keyfile string // PEM编码的密钥文件（默认"ca-key.pem"）
-n, --ca.name string // CA名字
--cacount int // 非默认CA实例的个数
--cafiles stringSlice // 逗号分隔的CA配置文件
--crl.expiry duration // 通过gencrl请求生成CRL的过期时间（默认24h0m0s）
--crlsizelimit int // 可接受的CRL大小限制（默认512000）
--csr.cn string // 对上级CA的证书签名请求的CN
--csr.hosts stringSlice // 对上级CA的证书签名请求中的主机列表，空格分割
--csr.serialnumber string // 对上级CA的证书签名请求中的序列号
--db.datasource string // 数据库特定的数据源（默认"fabric-ca-server.db"）
--db.tls.certfiles stringSlice
// PEM格式的信任的证书文件列表，逗号分隔（例如. root1.pem,root2.pem）
--db.tls.client.certfile string
// DB客户端PEM编码的证书文件，仅当服务器要求双向认证时才指定
--db.tls.client.keyfile string
// DB客户端PEM编码的密钥文件，仅当服务器要求双向认证时才指定
--db.type string
//数据库的类型，可以是SQLite、Postgres、MySQL（默认sqlite3）
-d, --debug // 打开调试级别日志
-H, --home string
// server的home目录（默认"/etc/hyperledger/fabric-ca"）
--intermediate.enrollment.label string // 使用HSM操作的标签
--intermediate.enrollment.profile string // 发行证书时签名profile的名字
--intermediate.parentserver.caname string // 连接上级CA时用的CN
-u, --intermediate.parentserver.url string
// 上级CA的URL（格式http://<username>:<password>@<address>:<port）
--intermediate.tls.certfiles stringSlice
// 逗号分隔的PEM编码的证书列表(e.g. root1.pem,root2.pem)
--intermediate.tls.client.certfile string
// PEM编码的证书文件，仅当服务器要求双向认证时才指定
--intermediate.tls.client.keyfile string
// 客户端PEM编码的密钥文件，仅当服务器要求双向认证时才指定
--ldap.enabled // 打开LDAP
--ldap.groupfilter string
// LDAP组过滤（默认"(memberUid=%s)"）
--ldap.tls.certfiles stringSlice
// LDAP信任的，用逗号分隔的证书列表（例如，root1.pem,root2.pem）
--ldap.tls.client.certfile string
// LDAP客户端PEM编码的证书文件，仅当服务器要求双向认证时才指定
--ldap.tls.client.keyfile string
// 客户端PEM编码的密钥文件，仅当服务器要求双向认证时才指定
--ldap.url string
// LDAP客户端URL（格式ldap:adminDN:adminPassword@host[:port]/base）
--ldap.userfilter string
// LDAP用户过滤器（默认"(uid=%s)"）
-p, --port int
// fabric-ca-server监听端口（默认7054）
--registry.maxenrollments int
// 注册的最大次数，LDAP关闭时有效（默认-1）
--tls.certfile string
// 服务器监听地址所用的PEM编码的TLS证书文件（默认"tls-cert.pem"）
--tls.clientauth.certfiles stringSlice
// 逗号分隔的可信的客户端证书列表（例如. root1.pem,root2.pem）
--tls.clientauth.type string
// 客户端认证策略.（默认"noclientcert"）
--tls.enabled // 以TLS方式监听
--tls.keyfile string
// 服务器监听地址所用的PEM编码的私钥文件

使用“fabric-ca-server[command]--help”可获取更多子命令信息。

下面我们来看一下Fabric CA的数据库存储。默认的数据库采用的是嵌入式数据SQLite，文件名是fabric-ca-server.db。如果需要考虑集群部署，可以采用MySQL或者PostgreSQL数据库。

1.基于MySQL的数据存储

在Fabric CA服务端的配置文件中添加下面的内容就可以访问MySQL数据库。但要确保其他相关变量也要配置正确。比如数据库名字对字符集的限制，可参考MySQL文档https://dev.mysql.com/doc/refman/5.7/en/identifiers.html 。

db:
type: mysql
datasource: root:rootpw@tcp(localhost:3306)/fabric_ca?parseTime=true&tls=
custom

如果要通过TLS连接MySQL数据库，必须设置db.tls，同时MySQL也要配置允许TLS访问。配置文件的示例如下所示：

db:
...
tls:
enabled: true
certfiles:
- db-server-cert.pem
client:
certfile: db-client-cert.pem
keyfile: db-client-key.pem

其中，certfiles是PEM编码的可信根证书文件列表，certfile和keyfile是PEM编码的证书和密钥文件，用于Fabric CA服务器与MySQL数据库之间的安全连接。

2.基于PostgreSQL的数据存储

如果采用PostgreSQL数据库，Fabric CA服务端的配置文件可以参考如下的配置：

db:
type: postgres
datasource: host=localhost port=5432 user=Username password=Password
dbname=fabric_ca sslmode=verify-full

在PostgreSQL上配置SSL的步骤如下：

1）在postgresql.conf中，打开SSL，设置为on（SSL=on）。

2）将你信任的CA证书root.crt放在PostgreSQL的data目录中。

3）在pg_hba.conf中hostssl位置，设置clientcert参数为1。

关于如何生成签名证书，可参考https://www.postgresql.org/docs/9.5/static/ssl-tcp.html ，自签名证书可用于测试，不应该用于产品环境中。

更多详细配置，可参考PostgreSQL的官方文档https://www.postgresql.org/docs/9.4/static/libpq-ssl.html 。

3.基于LDAP的数据存储

Fabric CA服务端可以通过配置连接至LDAP服务器，实现如下的功能：

·用户注册时从LDAP服务器中读取信息进行认证；

·用户鉴权时从LADP服务器中读取属性信息进行验证。

配置的方式是修改Fabric CA服务器配置文件内的LDAP选项，如下是配置文件的模板：

ldap:
# 使能或者禁止LDAP客户端(默认为: false)
enabled: false
# LDAP服务器的URL
url: <scheme>://<adminDN>:<adminPassword>@<host>:<port>/<base>
userfilter: filter

其中：

·scheme为ldap或ldaps；

·adminDN为管理员唯一的名字；

·pass为管理员的密码；

·host LDAP为服务器的主机名或IP地址；

·Port为可选端口，默认值为389（ldap）或636（ldaps）；

·base为用于搜索LDAP树的根路径；

·filter为过滤器，在搜索时将登录名转换为唯一名字。

下面是一个具体的示例，可连接默认配置的OpenLDAP服务器。

ldap:
enabled: true
url:
ldap://cn=admin,dc=example,dc=org:admin@localhost:10389/dc=example,dc=org
userfilter: (uid=%s)

OpenLDAP服务的配置使用可参考https://github.com/osixia/docker-openldap 上的说明。

配置好LDAP服务器后，用户注册过程如下所示：

1）Fabric CA客户端或客户端SDK发送带有基本授权头部的用户注册请求；

2）Fabric CA服务器接收到用户注册请求后，解析出头部中的用户名称和注册密码。通过在配置文件中设置“userfilter”可以查找用户对应的可识别名称（Distinguished Name，DN），尝试执行LDAP绑定用户的注册密码进行身份验证。如果绑定成功，则用户注册就被认证通过了。

8.2.3　Fabric CA服务端的操作使用

本节我们会介绍两种访问Fabric CA服务端的方法：Fabric CA客户端工具和RESTful接口，客户端本身也是调用服务端RESTful接口实现的。客户端工具能比较直观地使用命令就能实现和服务端的交互，所以我们先介绍客户端工具，再介绍服务端的RESTful接口。

1.Fabric CA客户端的使用

本节先介绍fabric-ca-client工具的命令行选项，再逐个介绍各个子命令的使用。

（1）fabric-ca-client命令行选项

我们先看一下Fabric CA客户端的命令行选项：

//帮助
fabric-ca-client [command]

//子命令:
enroll //用户注册
gencrl //生成CRL
gencsr //生成CSR
getcacert //获取CA证书链
reenroll //重新注册
register //用户登记
revoke //用户注销
version //打印Fabric CA客户端版本

//参数
--caname string // CA的名字
--csr.cn string // 证书签名请求的CN名字
--csr.hosts stringSlice // 证书签名请求的主机名列表，用空格分隔
--csr.names stringSlice
// 证书签名请求的名字列表，格式为<name>=<value> （例如，C=CA,O=Org1）
--csr.serialnumber string // 证书签名请求的序列号
-d, --debug // 打开调试级别日志
--enrollment.attrs stringSlice
// 用逗号分隔的属性列表，格式为<name>[:opt] （例如，foo,bar:opt）
--enrollment.label string // 使用HSM的标签
--enrollment.profile string // 发行证书使用签名profile的名字
-H, --home string // 客户端home目录（默认为"$HOME/.fabric-ca-client"）
--id.affiliation string // 身份对应的隶属关系
--id.attrs stringSlice
// 用逗号分隔的属性列表，格式为<name>=<value>（例如，foo=foo1,bar=bar1）
--id.maxenrollments int // 注册的最大次数（默认为-1）
--id.name string // 注册者身份的唯一名字（DN）
--id.secret string // 注册者的密码
--id.type string
// 注册者的类型（例如，'peer, app, user'）（默认为"user"）
-M, --mspdir string // MSP的目录路径（默认为"msp"）
-m, --myhost string
// 注册时指定CSR中的主机名（默认为"$HOSTNAME"）
-a, --revoke.aki string // 注销证书的AKI
-e, --revoke.name string // 注销的身份名字
-r, --revoke.reason string // 注销原因
-s, --revoke.serial string // 注销证书的序列号
--tls.certfiles stringSlice
// PEM编码的信任证书列表，用逗号分隔（例如，root1.pem,root2.pem）
--tls.client.certfile string
// 客户端PEM编码的证书，仅当服务器要求双向认证时才指定
--tls.client.keyfile string
// 客户端PEM编码的密钥，仅当服务器要求双向认证时才指定
-u, --url string // fabric-ca-server的URL （默认为"http:localhost:7054"）

使用“fabric-ca-client[command]--help”可查看子命令帮助

后面我们会介绍其中主要的命令行功能。

（2）注册初始化的管理员用户

在Fabric CA服务端启动的时候有一个管理员用户，需要先注册初始化的管理员用户，获取注册证书以后才能进行后续的操作。注册初始化的管理员用户的步骤如下：

首先，在Fabric CA服务端配置文件fabric-ca-server-config.yaml中设置客户端的CSR（证书签名请求），里面的选项是可以自定义的，只有“csr.cn”必须设置成初始化的管理员ID。默认的CSR配置如下：

csr:
cn: <<enrollment ID>>
key:
algo: ecdsa
size: 256
names:
- C: US
ST: North Carolina
L:
O: Hyperledger Fabric
OU: Fabric CA
hosts:
- <<hostname of the fabric-ca-client>>
ca:
pathlen:
pathlenzero:
expiry:

然后，运行fabric-ca-client enroll命令来获取注册证书。在下面的命令中，先设置获取注册证书存储目录的环境变量，访问运行在本地7054端口的fabric-ca服务端，注册的用户ID和密码分别是admin和adminpw。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/admin
fabric-ca-client enroll -u http://admin:adminpw@localhost:7054

上面的注册命令将获取的注册证书ECert和对应的私钥，CA证书链PEM文件存储在了环境变量FABRIC_CA_CLIENT_HOME目录下的msp子目录中。

（3）登记一个新用户

只有已经注册的用户才可以发起登记（Register）请求，发起登记请求的用户称为登记员（Registrar），登记新用户的时候还需要有相应的权限。Fabric CA服务端在接收到登记请求时需要进行如下几个方面的检查：

1）登记员需要有登记用户的登记权限：登记员可以登记的用户类型记录在“hf.Registrar.Roles”属性中，如果这个属性保存的内容是“peer，app，user”，则登记员就可以登记peer、app和user类型的用户，但是不能登记orderer类型的用户。

2）登记员只能登记自己归属范围内的用户：比如登记员的归属是a.b，可以登记归属是a.b.c的用户，不能登记归属是a.c的用户。如果不指定登记用户的归属，那么默认就和登记员的归属一样。

3）登记的用户属性需要满足如下一些条件。

①登记的用户属性需要包含在登记员的用户属性“hf.Registar.Attributes”中。目前只支持“*”通配符，比如a.b.*代表所有以a.b开头的属性名称。

②如果登记的用户也有hf.Registar.Attributes属性，需要其是登记员用户属性hf.Registar.Attributes的子集。比如登记员的hf.Registar.Attributes属性值是a.b.*，x.y.z，则登记用户的hf.Registar.Attributes属性值是a.b.c，x.y.z就是允许的，因为登记用户的属性a.b.c满足登记员的属性a.b.*，同时都有相同的属性x.y.z。

我们再来看几个在登记用户时属性检查的几个例子，表8-6所示为登记员和登记用户属性的名称都是hf.Registar.Attributes。

表8-6　登记用户时的属性检查示例

在下面的命令中，登记员是admin，登记的新用户名称是“admin2”，affiliation属性为“org1.department1”，属性名“hf.Revoker”的值是“true”，属性名“admin”的值是“true”。命令中“：ecert”后缀的意思是admin属性及其值会添加到用户注册证书中，从而用来实现访问控制。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/admin
fabric-ca-client register --id.name admin2 --id.affiliation org1.department1 --id.
attrs 'hf.Revoker=true,admin=true:ecert'

命令运行后会打印注册密码，用户注册的时候需要这个注册密码。

在登记用户的时候可以使用“-id.attrs”选项同时指定多个属性，属性之间用逗号分隔，每个属性使用双引号，下面是一个具体的例子，同时登记了“hf.Registrar.Roles”属性和“hf.Revoker”属性：

fabric-ca-client register -d --id.name admin2 --id.affiliation org1.department1
--id.attrs '"hf.Registrar.Roles=peer,user",hf.Revoker=true'

指定属性的“-id.attrs”选项也可以有多个同时使用，下面的例子登记的是相同的“hf.Registrar.Roles”属性和“hf.Revoker”属性：

fabric-ca-client register -d --id.name admin2 --id.affiliation org1.department1
--id.attrs '"hf.Registrar.Roles=peer,user"' --id.attrs hf.Revoker=true

登记用户的默认值可以保存在Fabric CA的客户端配置文件中，假设配置文件如下所示：

id:
name:
type: user
affiliation: org1.department1
maxenrollments: -1
attributes:
- name: hf.Revoker
value: true
- name: anotherAttrName
value: anotherAttrValue

用客户端命令行工具执行下面的命令登记用户，这样会从上面的配置文件中读取默认值，包括身份类型“user”，归属“org1.department1”，以及两个属性“hf.Revoker”和“anotherAttrName”。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/admin
fabric-ca-client register --id.name admin3

下面我们登记一个Peer节点，登记时指定用户类型为“peer”，用户名称是“peer1”，用户归属是“org1.department1”，注册密码不采用服务端自动生成，手动设置为“peer1pw”：

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/admin
fabric-ca-client register --id.name peer1 --id.type peer --id.affiliation org1.
department1 --id.secret peer1pw

（4）Peer节点的注册

通过命令行在Fabric CA服务端登记了Peer节点后，就可以通过登记的用户名称和注册密码获取注册证书了。在下面的命令中，用户名称和注册密码分别是：“peer1”和“peer1pw”，选项-M的值为Peer节点的MSP目录，它保存获取到的注册证书等信息。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/peer1
fabric-ca-client enroll -u http://peer1:peer1pw@localhost:7054 -M $FABRIC_CA_
CLIENT_HOME/msp

Orderer节点的注册过程是类似的，只需将对应的MSP目录路径改为你的orderer对应的MSP目录。

（5）重新获取用户的注册证书

假如你的注册证书将要过期或者私钥泄露，那么你可以调用reenroll命令重新申请新的注册证书。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/peer1
fabric-ca-client reenroll

（6）用户注销

一个身份或证书是可以被注销的。注销身份会注销其拥有的所有证书，也会同时阻止其申请新的证书。注销证书只是对单个证书进行无效处理。

下面的命令禁用了一个身份，并注销了与身份相关的所有的证书。注销后，此身份发起的任何请求都会被拒绝。

fabric-ca-client revoke -e <enrollment_id> -r <reason>

例如，下面的命令使用admin注销了peer1的身份。

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/admin
fabric-ca-client revoke -e peer1

也可以通过API和序列号注销：

fabric-ca-client revoke -a xxx -s yyy -r <reason>

例如，你可以使用openssl命令获得证书的API和序列号，然后传递给revoke命令

serial=$(openssl x509 -in userecert.pem -serial -noout | cut -d "=" -f 2)
aki=$(openssl x509 -in userecert.pem -text | awk '/keyid/ {gsub(/
*keyid:|:/,"",$1);print tolower($0)}')
fabric-ca-client revoke -s $serial -a $aki -r affiliationchange

（7）获取CA证书链

在MSP目录的子目录cacerts下，存储的是可信的CA根证书。可以通过fabric-ca-client getcacerts命令从Fabric CA服务器获取根CA证书。

我们来看一个例子，下面的命令启动一个名称为“CA2”的Fabric CA服务，监听的端口是7055，初始的管理员用户名称和密码分别是：“admin”和“ca2pw”：

export FABRIC_CA_SERVER_HOME=$HOME/ca2
fabric-ca-server start -b admin:ca2pw -p 7055 -n CA2

下面的命令可以获取CA2的根CA证书并保存到peer1的MSP目录下：

export FABRIC_CA_CLIENT_HOME=$HOME/fabric-ca/clients/peer1
fabric-ca-client getcacert -u http://localhost:7055 -M $FABRIC_CA_CLIENT_HOME/
msp

（8）使用TLS

Fabric CA客户端可以通过配置fabric-ca-client-config.yaml使用TLS。

tls:
# 使能TLS（默认: false）
enabled: true
certfiles:
- root.pem
client:
certfile: tls_client-cert.pem
keyfile: tls_client-key.pem

其中，certfiles选项设置信任的根证书，client选项仅在服务器设置双向认证后才必须指定。

2.Fabric CA的RESTful接口

Fabric CA提供了多个接口，包括获取CA信息、获取注册证书、重新获取注册证书、用户登记、用户注销、批量获取交易证书等。Fabric CA提供的RESTful的接口，可以通过http或https访问。

（1）获取CA信息

表8-7是获取CA信息请求的URL。

表8-7　获取CA信息请求的URL

获取CA信息请求的Header信息如下所示：

Content-Type: application/json

获取CA信息请求的Body内容如下所示：

{
"caname": "ca.fabric.chainnova.xyz"
}

获取CA信息返回的Body如下所示：

{
"success":true,
"result":{
"CAName":"ca.fabric.chainnova.xyz",
"CAChain":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNVekNDQWZtZ0F3SUJB
Z0lRRnlXZ1piZy9OUjZKUUw2SDR3TS9TakFLQmdncWhrak9QUVFEQWpCN01Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RWRNQnNHQTFVRUNoTVVabUZpY21sakxtTm9ZV2x1Ym05MllTNTRlWG94SURBZUJnTlZCQU1UCkYyTmhMbVpoWW5KcFl5NWphR0ZwYm01dmRtRXVlSGw2TUI0WERURTNNRGd5T1RBeU16Z3dNMW9YRFRJM01EZ3kKTnpBeU16Z3dNMW93ZXpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RmpBVQpCZ05WQkFjVERWTmhiaUJHY21GdVkybHpZMjh4SFRBYkJnTlZCQW9URkdaaFluSnBZeTVqYUdGcGJtNXZkbUV1CmVIbDZNU0F3SGdZRFZRUURFeGRqWVM1bVlXSnlhV011WTJoaGFXNXViM1poTG5oNWVqQlpNQk1HQnlxR1NNNDkKQWdFR0NDcUdTTTQ5QXdFSEEwSUFCT2tBVnhlaWZ4c09kSzU5enJmeXFNWEx1eFU3MTI0SW1VdDRSYzN0VWpLNgpUVGt6dGRpNFJyUitXVDBoaHNzRVA3N1RBZjNGSVBzWXFoanZxbXVzVUFXalh6QmRNQTRHQTFVZER3RUIvd1FFCkF3SUJwakFQQmdOVkhTVUVDREFHQmdSVkhTVUFNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdLUVlEVlIwT0JDSUUKSU1nWTdaOGx3OS9QdksyWlk3T2tsS1MzM3pKUU5wNjZZTWc5RTcwR0lJL0VNQW9HQ0NxR1NNNDlCQU1DQTBnQQpNRVVDSVFEZU5Qa1Z6UUZxbWlSUEJodDZyRmNCS3BDWnhPYVpjRkdzbmM3QW81Nklnd0lnRHozSldzTTFWRmozCnlxTVgxYXNzb1hLMUlFODhuVzBlVE5rUHdHTStsSXc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
},
"errors":{
},
"messages":{
}
}

（2）获取注册证书

表8-8是用户注册请求的类型和URL。

表8-8　用户注册请求的类型和URL

用户注册请求的Header信息如下所示：

Authorization: YWRtaW46YWRtaW5wdw==
Content-Type: application/json

用户注册请求的Body内容如下所示：

{
"request": "string",
"profile": "Unknown Type: string,null",
"label": "Unknown Type: string,null",
"caname": "Unknown Type: string,null"
}

用户注册请求返回的Body内容如下所示：

{
"Success": true,
"Result": "string",
"Errors": [
{
"code": 0,
"message": "string"
}
],
"Messages": [
{
"code": 0,
"message": "string"
}
]
}

（3）重新获取注册证书

表8-9是用户重新注册请求的类型和URL。

表8-9　重新注册请求的类型和URL

重新注册请求的Header信息如下所示：

Authorization: YWRtaW46YWRtaW5wdw==
Content-Type: application/json

重新注册请求的Body内容如下所示：

{
"request": "string",
"profile": "Unknown Type: string,null",
"label": "Unknown Type: string,null",
"caname": "Unknown Type: string,null"
}

重新注册请求返回的Body内容如下所示：

{
"Success": true,
"Result": "string",
"Errors": [
{
"code": 0,
"message": "string"
}
],
"Messages": [
{
"code": 0,
"message": "string"
}
]
}

（4）用户登记

表8-10是用户登记请求的类型和URL。

表8-10　用户登记请求的类型和URL

用户登记请求的Header信息如下所示：

Authorization: YWRtaW46YWRtaW5wdw==
Content-Type: application/json

用户登记请求的Body内容如下所示：

{
"id": "string",
"type": "string",
"secret": "Unknown Type: string,null",
"max_enrollments": "Unknown Type: integer,null",
"affiliation_path": "string",
"attrs": [
{
"name": "string",
"value": "string"
}
],
"caname": "Unknown Type: string,null"
}

用户登记请求返回的Body内容如下所示：

{
"Success": true,
"Result": {
"credentials": "string"
},
"Errors": [
{
"code": 0,
"message": "string"
}
],
"Messages": [
{
"code": 0,
"message": "string"
}
]
}

（5）用户注销

表8-11是用户注销请求的类型和URL。

表8-11　用户注销的类型和URL

用户注销请求的Header信息如下所示：

Authorization: YWRtaW46YWRtaW5wdw==
Content-Type: application/json

用户注销请求的Body内容如下所示：

{
"id": "Unknown Type: string,null",
"aki": "Unknown Type: string,null",
"serial": "Unknown Type: string,null",
"reason": "Unknown Type: string,null",
"caname": "Unknown Type: string,null"
}

用户注销请求返回的Body内容如下所示：

{
"Success": true,
"Result": "string",
"Errors": [
{
"code": 0,
"message": "string"
}
],
"Messages": [
{
"code": 0,
"message": "string"
}
]
}

（6）批量获取交易证书

表8-12是批量获取交易证书请求的类型和URL。

表8-12　批量获取交易证书请求的类型和URL

批量获取交易证书请求的Header信息如下所示：

Authorization: YWRtaW46YWRtaW5wdw==
Content-Type: application/json

批量获取交易证书请求的Body内容如下所示：

{
"count": 0,
"attr_names": [
"string"
],
"encrypt_attrs": true,
"validity_period": 0,
"caname": "Unknown Type: string,null"
}

批量获取交易证书请求返回的Body内容如下所示：

{
"Success": true,
"Result": {
"id": 0,
"ts": 0,
"key": "string",
"tcerts": [
{
"cert": "string",
"keys": [
{
"name": "string",
"value": "string"
}
]
}
]
},
"Errors": [
{
"code": 0,
"message": "string"
}
],
"Messages": [
{
"code": 0,
"message": "string"
}
]
}

8.3　本章小结

本章介绍了与成员管理相关的两个服务，MSP和Fabric CA。Fabric CA负责颁发证书，可以有多种方式实现。Fabric CA是超级账本提供的默认实现，并不是强制使用的。MSP利用生成的证书进行签名和验证，把不同的证书归属到不同的MSP中，利用PKI机制验证成员身份，实现成员的认证和授权管理。

第9章　支持多种语言的智能合约

智能合约（smart contract）最早是1996年尼克·萨博（Nick Szabo）在他的文章“Smart Contracts：Building Blocks for Digital Markets”中提出来的：

A smart contract is a set of promises，specified in digital form，including protocols within which the parties perform on these promises.

维基百科对智能合约做了扩展：

A smart contract is a computer protocol intended to facilitate，verify，or enforce the negotiation or performance of a contract.Smart contracts were first proposed by Nick Szabo in 1996.

Proponents of smart contracts claim that many kinds of contractual clauses may be made partially or fully self-executing，self-enforcing，or both.The aim with smart contracts is to provide security that is superior to traditional contract law and to reduce other transaction costs associated with contracting.

概括起来，智能合约有如下几个特性。

1）智能合约必须是一种合约。合约是平等的当事人之间执行约定内容的协议。参与方对合约的内容要达成一致，并且遵守合约执行的结果。相比于传统合约，不同的地方在于：智能合约是“数字形式”的，需要转换成计算机可读和可执行的代码。

2）智能合约是部分或者完全自我执行（self-executing）和自我强制（self-enforcing）的：自我执行是指合约能立即自动生效。自我强制是指合约的参与方能够根据最优的结果自行决定是否参与或者终止和关联方的关系，继续参与得不偿失才会终止合约。整个决策过程是不需要可信的第三方干预的，这是一种有用的特性，因为第三方参与仲裁会有一些额外的开销。这是一种防欺诈的协议，参与方会计算违约的得失，做出理性的选择，客观上避免了任何一个参与方欺骗对方。

3）智能合约需要安全的运行环境：智能合约的运行由相关方执行或者调用执行完成约定的业务逻辑，运行环境是安全可靠的，执行的结果能在各方达成一致。

4）只有智能合约才能修改账本数据：账本数据是各个节点各自独立维护的分布式数据，智能合约执行的结果达成一致后才能修改账本数据，这里的修改是指追加数据，数据本身是不可篡改的。

本章介绍Hyperledger Fabric 1.0的智能合约，即链码（Chaincode）。

9.1　概述

链码是独立可运行的应用程序，运行在基于Docker的安全容器中，在启动的时候和背书节点建立gRPC连接，在运行过程中通过接口和背书节点通信。链码是线下开发好再部署到链上的，部署的时候需要一定的权限，成功部署就代表了链码的业务逻辑是关联方已经达成一致的。达成一致的过程其实是在线下完成的，线上的共识过程只是权限检查和业务逻辑的执行，并没有线上审核的流程。也就是说，如果链码并没有真正地做线下的确认，只要有部署的权限，也是可以直接上线的。目前智能合约部署的权限管理完全是基于成员管理的机制，这种权限管理的粒度是很粗的，不能做到完全像RBAC一样精确到某个资源。成员管理的内容参考前面相关的章节。

9.2　链码的生命周期管理

Hyperledger Fabric 1.0中，区块链网络中的各种节点（Peer节点、排序服务节点等）都提供了gRPC接口，只要有权限，应用程序就可以访问它们提供的功能。背书节点是Peer节点的一种角色，管理和维护了链上的链码，可以通过背书节点开放的接口，执行智能合约的功能。有两种方式可以访问接口，命令行和各种语言的SDK都可以。前面的章节已经介绍过命令的方式，第10章会详细介绍SDK的接口。

9.2.1　链码的生命周期

目前的版本中，链码提供了4个管理链码生命周期的命令，分别是链码的打包（package）、安装（install）、实例化（instantiate）、升级（upgrade）。在以后的版本中，还可能提供链码的停止（stop）和启动（start）命令，在不删除链码的情况下可以停止和启用链码。链码在安装和实例化以后，就处于激活的状态，应用程序或者命令行就可以调用和触发智能合约的功能了。链码安装以后随时都是可以升级的。

1.链码的打包

链码的内容主要包含以下3个部分。

·链码源码，但需通过ChaincodeDeploymentSpec或CDS定义。CDS依据代码及其他一些属性（名称、版本等）来定义链码。

·实例化策略，这是可选的，背书策略前面已经介绍过，可以参考前面相关的内容。

·链码的签名。

链码的签名实现了下面三个目标：

·表明是谁创建的链码；

·允许验证链码包里的内容；

·可以检测链码包是否被篡改。

链码的实例化策略会验证链码所有者的身份，进而验证其提交的链码源码、实例化策略是否有效。

（1）链码的创建

创建链码有两种方式。

·多个所有者：需要多个所有者对链码进行签名，先创建一个链码包SignedChaincode DeploymentSpec，然后发送给多个所有者进行签名。

·单一所有者：只有安装链码的节点对链码签名。

我们先来看复杂的情况。当然，如不需要了解多用户的情况，可以直接跳转到本节后面的“链码的安装”小节。

可通过下面的命令行创建带签名的链码包。

peer chaincode package -n mycc -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02 -v 0 -s -S -i "AND('OrgA.admin')" ccpack.out

其中，-s选项生成一个有多个所有者签名的链码包，而不是简单地创建一个不带签名的ChaincodeDeploymentSpec。如果指定了-s选项，当其他所有者要签名时，还需要指定-S选项。否则，创建的链码包SignedChaincodeDeploymentSpec只会在ChaincodeDeploymentSpec基础上添加实例化策略，不会包含所有者的签名。

-S选项可以使MSP（core.yaml中localMspid属性值定义的）对程序包进行签名。-S是可选的，如果创建了一个没有签名的包，那么其他的所有者不能通过对其使用signpackage命令签名。

可以通过-i选项为链码指定实例化策略。实例化策略与背书策略类似，它指明哪些身份可以对链码实例化。在上面的例子中，只允许OrgA管理员进行链码实例化。如果没有提供任何策略，系统将会采用默认策略，该策略只允许Peer节点MSP的管理员实例化链码。

（2）链码的签名

链码在创建时签名了才可以由其他的所有者校验签名和继续签名，签名的过程可以是线下操作（out-of-band）的。

链码SignedChaincodeDeploymentSpec是封装了ChaincodeDeploymentSpec的结构，主要是增加了实例化策略和所有者的签名，定义如下。

type SignedChaincodeDeploymentSpec struct {
// ChaincodeDeploymentSpec序列号后的字节数组
ChaincodeDeploymentSpec []byte
// 实例化策略，结构同背书策略
InstantiationPolicy []byte
// 所有者的签名，可以是多个
OwnerEndorsements []*Endorsement
}

从上面的结构可以看出，主要包含如下几个部分。

·ChaincodeDeploymentSpec包含链码的源码、名称和版本。

·实例化策略，在实例化的时候VSCC会验证。

·所有者的签名背书，包含了链码所有者的列表。

实例化策略定义了签名的MSPPrincipal和需要满足的规则，是需要线下确定的，链码实例化的时候会读取这些信息进行验证。如果不指定实例化策略，默认通道的管理员才能对链码实例化。实例化策略的定义如下。

type SignaturePolicyEnvelope struct {
// 策略的版本
Version int32 `json:"version,omitempty"`
// 策略的规则
Rule *SignaturePolicy `json:"rule,omitempty"`
// 策略的主体：可以是基于角色、基于组织和具体的身份
Identities []*common1.MSPPrincipal `json:"identities,omitempty"`
}

链码所有者可以对一个之前创建好的链码包进行签名，具体使用如下命令。

peer chaincode signpackage ccpack.out signedccpack.out

其中，命令中的ccpack.out和signedccpack.out分别指输入与输出包。signedccpack.out包含一个用本地MSP对包进行的附加签名。

2.链码的安装

链码安装的时候会把链码的源码打包到前面已经介绍过的结构Chaincode DeploymentSpec中，安装到需要运行链码的Peer节点上。链码安装是针对节点的，每次安装只对单个节点有效。需要给每个要运行链码的背书节点都安装一遍，具体需要安装到哪些节点，可以根据背书策略来选择。

从安全角度考虑，链码应该只安装在需要执行的背书节点上，可以保护链码的逻辑被未授权的节点获取到。没有安装链码的节点是不能执行链码的智能合约的，但是可以验证链上的交易并且提交到本地账本中。

安装链码的命令如下。

peer chaincode install -n ChaincodeName -v version -p ChaincodePath

上述的-n选项标识的是链码的名字，-v选项标识的是链码的版本，-p选项标识的是链码源码的路径，其必须在用户设置的环境变量GOPATH目录下（比如$GOPATH/src/cc）。

链码安装的时候是需要管理员权限的，SignedProposal必须是Peer节点本地MSP设置的一个管理员签名的。

3.链码的实例化

链码的实例化会调用链码生命周期系统链码（LSCC）在通道上创建和初始化链码。链码在实例化之前是和通道无关的，实例化的时候才和通道绑定。链码可以和多个通道绑定，在通道上初始化后记录到通道的状态数据库中。同一个链码在不同通道上的数据是隔离的，不同通道之间不会有影响，这在前面的第7章已经介绍过。

链码实例化的时候会检查是否符合链码实例化的策略和通道的写入策略。实例化的策略验证过程是检查实例化交易的签名是否符合策略的规则，验证通过才会写入到账本和状态数据中。通道的写入策略是在创建通道时指定的，这也是从安全角度考虑的，避免未授权的用户部署链码和调用链码。前面我们提到过，默认的实例化策略是通道的任何一个管理员，所以链码的实例化交易提交者必须也是通道的管理员。

链码的实例化会指定链码的背书策略，用来确定通道上哪些节点执行的交易才能添加到账本中，这在前面的章节中已经介绍过。

命令行的链码实例化如下。

peer chaincode instantiate -n ChaincodeName -v version -c '{"Args":["john","0"]}'
-P "OR ('Org1.member','Org2.member')"

上面的-P选项指定的就是背书策略，例子中的策略只要是Org1或者Org2的任意一个成员的背书就可以了。

目前命令行的链码实例化只能指定AND和OR的背书策略，内部会转换成NOutOf策略，这在SDK的接口中是可以指定的。

链码实例化以后，就处于ready状态了，可以处理链码的调用和查询交易了，本章后面部分会详细地介绍背书节点侧和链码侧的有限状态机。

4.链码的升级

链码安装以后是随时可以升级的，链码的名称需要保持不变，必须要更新的是链码的版本，其他的部分，比如链码的所有者和实例化策略都是可选的。链码的版本是用字符串表示的，并没有指定比较版本大小的规则，是以更新的先后顺序为准的，具体的操作方法线下自行确定。

同样，升级之前还需要把链码安装到对应的背书节点上。链码的升级和实例化是类似的操作，也是绑定链码到通道上，执行初始化的操作。链码升级只会影响到指定的通道，没有绑定链码新版本的通道还是继续旧的版本。同一个背书节点上，不同通道绑定的链码可能是不同的版本，所以升级的时候不会主动删除旧的版本。

链码升级的时候同样会检查实例化策略，采用的是通道上链码最新版本的实例化策略，检查通过以后才会更新为链码新版本指定的实例化策略。

5.链码的停止和启动

链码的停止与启动功能还没实现，只能手动删除链码的容器和镜像，再删除背书节点本地保存的链码。链码容器和镜像的命名规则可以参考本章后面的容器管理部分。链码代码默认是在下面的目录结构中。

/var/hyperledger/production/chaincodes/<ccname>:<ccversion>

其中，/var/hyperledger/production目录是背书节点的环境变量CORE_PEER_FILESYSTEMPATH指定的。

9.2.2　应用程序和链码的交互流程

普通链码用来执行特定的智能合约功能，图9-1是应用程序和链码的交互流程图，包含如下几个步骤。

图9-1　应用程序和链码的交互流程

第1步：应用程序或者命令行通过gRPC请求向背书节点发起链码的调用请求，背书节点再转发给链码执行，应用程序不能直接和链码通信。

第2步：背书节点会检查对应的链码是否启动，检查的方法是查看本地维护的映射表里是否有指定链上的链码名称和版本的记录。如果没有相关的记录，就会通过Docker的API发起创建或者启动容器的命令。

第3步：Docker服务根据API的命令启动容器，并建立和背书节点的gRPC连接。如果背书节点接收到请求以后检查链码容器已经启动，会直接跳过第2步和3步。

第4步：通过链码和背书节点建立好的gRPC连接，转发应用程序调用的请求，链码在执行过程中会和背书节点有多次的数据交互。

第5步：链码执行完以后，调用背书节点的ESCC对模拟执行的结果进行背书。

第6步：ESCC对模拟执行进行签名，返回背书的结果。

第7步：背书节点返回包含背书节点背书的结果给应用程序。

在整个的交易流程中，链码只参与业务逻辑模拟执行的过程，后续的交易排序和验证分别是排序服务和记账节点完成的。背书节点接收和处理请求后就返回应用程序了，不会转发请求给其他背书节点。应用程序可以自由选择背书节点发起请求，只要最终生成的交易能够满足背书策略就可以。

9.2.3　背书节点接收应用程序的请求处理

应用程序通过gRPC的接口发起请求，命令如下。

ProcessProposal(ctx context.Context, in *SignedProposal, opts ...grpc.CallOption)
(*ProposalResponse, error)

背书节点接收到请求以后，会做一些必要的检查，比如是否有权限提交交易、是否是重复交易等，真正的执行过程是在链码中完成的，ESCC最后对执行的结果进行签名背书。中间有任何异常都会终止后续的执行，返回结果给应用程序。链码调用时序图如图9-2所示。

图9-2　链码调用时序图

下面再来看几个重要流程的实现方式。

9.2.4　采用上下文实现交易的模拟执行

链码在业务逻辑的处理过程中会读取和操作账本数据，对这些数据的修改并没有直接影响到状态数据库，而是内部实现了一个交易模拟器，把过程数据记录到了模拟器中。交易模拟器的接口如下。

// TxSimulator可以通过最新连续快照来模拟交易
// Set* 方法用于支持KV-based数据模型。ExecuteUpdate方法用于支持富数据模型及相关查询
type TxSimulator interface {
QueryExecutor
// 用于对指定的namespace进行key和value的设定。namespace对应Chaincode的
//chaincodeId
SetState(namespace string, key string, value []byte) error
// 删除指定namespace和key
DeleteState(namespace string, key string) error
// 批量设置多个key的值
SetStateMultipleKeys(namespace string, kvs map[string][]byte) error
// 支持富数据模型 (参见上面QueryExecutor)
ExecuteUpdate(query string) error
// 封装了事务模拟的结果，包含丰富的详细信息：
// - 交易的提交状态的更新将引起状态的更新；
// - 在提交交易时，对交易的执行环境进行有效性验证。
// 对不同的账本以不同的形式展示上面提到的两点，实现对不同的数据模型的支持以及更好地展现相关信息
GetTxSimulationResults() ([]byte, error)
}

基本的原理是每次链码调用的时候生成一个交易模拟器TxSimulator，具体实现的lockBasedTxSimulator内部封装了一个能够操作真实状态数据库的queryHelper。所有的数据读取操作会通过queryHelper查询到真实的状态数据，并记录到rwsetBuilder里，数据的写入和删除操作只记录到rwsetBuilder里，并不会直接提交到状态数据库中。当模拟执行完成以后，从rwsetBuilder中生成读写集，返回给应用程序。图9-3是逻辑关系的示意图。

交易模拟器本身是利用Golang的上下文（context）机制，把交易模拟器添加到上下文的txsimulatorkey中。下一节我们来看看如何利用上下文信息实现数据的分发。

9.2.5　链码消息的数据分发

普通链码和背书节点之间建立的是gRPC的长连接，背书节点如何区分同时发起的链码调用请求呢？

实际上，每个链码和背书节点建立的是不同的连接。相同链码的调用复用一个长连接，背书节点接收到链码的请求中有交易号作为唯一标识，有限状态机根据交易号找到上下文信息，根据txsimulatorkey就能找到交易模拟器，对不同的交易做数据的隔离。图9-4链码消息的数据分发示意图。

图9-3　交易模拟器的运行示意图

图9-4是一个链码消息数据分发示意图，图中普通链码、系统链码分别和背书节点建立了不同的连接，普通链码建立的是gRPC连接，系统链码建立的是Golang的通道连接，同一个连接上又分别有2个链码调用在操作数据。背书节点侧的有限状态机记录了不同交易号调用时的上下文信息，根据交易号从交易上下文映射表中获取到交易所对应的上下文信息，进而利用交易模拟器生成模拟执行的结果。

图9-4　链码消息的数据分发示意图

9.2.6　链码运行环境的管理

链码运行环境管理需要实现的接口如下。

type VM interface {
// 部署虚拟机
Deploy(ctxt context.Context, ccid ccintf.CCID, args []string, env []string, reader io.Reader) error
// 启动虚拟机
Start(ctxt context.Context, ccid ccintf.CCID, args []string, env []string, builder BuildSpecFactory, preLaunchFunc PrelaunchFunc) error
// 停止虚拟机
Stop(ctxt context.Context, ccid ccintf.CCID, timeout uint, dontkill bool,
dontremove bool) error
// 销毁虚拟机
Destroy(ctxt context.Context, ccid ccintf.CCID, force bool, noprune bool)
error
// 获取虚拟机名称
GetVMName(ccID ccintf.CCID) (string, error)
}

接口名称定义的是虚拟机，目前只支持Docker和系统进程空间的方式，未来可能支持更多的方式。

1.链码容器的管理

普通链码是运行在容器中的，线上环境中背书节点一般也是以容器的方式运行，背书节点启动的链码容器是和背书节点在同一个宿主机上的。在容器中的程序如何来管理宿主机上的容器呢？实际上，背书节点是通过Docker守护进程提供的Docker Engine API创建和启动容器的，有两种方式可以访问Docker Engine API：

·Unix Socket文件，比如unix：///var/run/docker.sock；

·HTTP（S）连接，比如http://localhost:2375 。

下面是背书节点配置文件core.yaml中的片段，在用docker-compose启动的时候需要确认是否设置了CORE_VM_ENDPOINT这个环境变量。

vm:
# Docker管理方式可以设置为如下几种：
# unix:///var/run/docker.sock
# http://localhost:2375
# https://localhost:2376
endpoint: unix:///var/run/docker.sock

Docker Engine API本身是RESTful API，可以用支持HTTP协议的客户端访问，如wget、curl、postman等，应用程序可以采用支持HTTP协议的库或者SDK。链码采用的是第三方的库：https://github.com/fsouza/go-dockerclient ，调用的API如表9-1所示。

表9-1　调用Docker的API列表

我们先来看下镜像的构建流程，如图9-5所示。

图9-5　构建链码镜像示意图

特别说明一下，构建链码镜像的时候会写入背书节点TLS的根证书，链码节点根据这个根证书验证背书节点的gRPC连接是否是安全的。在镜像构建的时候如果已经存在同名的镜像，就不会重复构建。只要组织和链码名称、版本都相同，则这种情况就很容易出现。尤其是本地搭建环境的时候，如果重新生成了MSP的证书，没有删除已生成的镜像就部署链码，就可能会导致链码容器里的TLS根证书和背书节点的TLS根证书不匹配，出现连接错误。链码在启动的时候会检查账本的链码信息是否和本地文件保存的链码文件信息一致，但并不检查链码容器镜像是否和账本的链码信息一致，其实是有隐藏的问题的。

构建链码镜像的过程是跟链码语言相关的，主要的功能是在源码的基础上添加连接背书节点的SDK和其他的一些依赖，再编译成可执行的二进制文件，编译环境是启动一个语言相关的临时容器，编译完成后拷贝生成的二进制文件到镜像文件里面。这个部分跟链码语言相关，比如Golang会增加环境变量GOPATH和编译参数等，细节的内容就不展开了，感兴趣的读者可以查看源码的core/chaincode/platform目录。

构建完的链码镜像会写入一些构建时的标签信息，使用docker inspect可查看到如下标签，如表9-2所示。

链码容器在编译和启动过程中会继承背书节点的一些环境变量，如表9-3所示。

表9-2　获取Docker的标签信息列表

表9-3　链码容器继承背书节点的环境变量列表

启动链码的命令行参数--peer.address也是从背书节点的环境变量中获取的，这个命令行参数是链码和背书节点建立gRPC连接的服务端地址。有几个相关的环境变量：

·CORE_PEER_CHAINCODELISTENADDRESS；

·CORE_PEER_ADDRESS；

·CORE_CHAINCODE_PEERADDRESS。

这个几个环境变量的使用是有优先级的，如果设置了环境变量CORE_PEER_CHAINCODELISTENADDRESS，则会以这个环境变量为准，否则以环境变量CORE_PEER_ADDRESS为准。最后一个环境变量CORE_CHAINCODE_PEERADDRESS是备用的，只有前面两个设置有异常的情况下才会启用。如果都没有设置或者设置有问题，就是默认值0.0.0.0：7051了。

2.系统链码的管理

系统链码运行在Peer节点的进程空间中，实现方式比容器的方式简单许多。比如部署的过程实际就是在内存中注册名称和入口函数，标识系统链码在运行就可以了。启动的过程主要是建立好和背书节点的Golang通道，启动有限状态机对交互消息进行处理，后面的部分和容器的过程是一样的。系统链码的调用过程如图9-6所示。

系统链码的调用过程跟普通链码不同的地方在于不需要启动容器，只需要检查是否启动，根据链码运行时环境找到对应的Golang通道，通过通道发送调用链码的请求即可。系统链码调用的入口有两个，可以是节点进程内部发起调用，也可以是通过网络的方式调用。两种方式调用的流程并不一样，进程内部发起的调用执行完成以后不会调用ESCC进行背书签名。只有部分系统链码可以外部调用，更详细的内容请看下面一个章节。

图9-6　系统链码运行示意图

9.3　内置的系统链码

链码分为两种类型。

·系统链码：系统内置的链码，用来完成一些系统功能等。

·普通链码：实现应用业务逻辑的链码。

系统链码和普通链码的几个不同点，如表9-4所示。

表9-4　系统链码和普通链码的不同点

下面是系统链码的属性定义。

type SystemChaincode struct {
//system chaincode名称(唯一)
Name string
//system chaincode路径(暂未使用)
Path string
//system chaincode初始化参数
InitArgs [][]byte
// chaincode object实体对象
Chaincode shim.Chaincode
// 用于追踪是否通过发送提案给Peer节点来完成对system chaincode调用
InvokableExternal bool
// 用于追踪是否可以通过chaincode-to-chaincode的调用方式调用system chaincode
InvokableCC2CC bool
// 用于开启/禁用 system chaincode的便捷开关，无须删除importsysccs.go的条目
Enabled bool
}

以下是内置的系统链码及其属性列表，如表9-5所示。

表9-5　内置的系统链码及其属性列表

下面就来详细说明一下每个系统链码的功能。

9.3.1　生命周期管理系统链码

生命周期管理系统链码（Lifecycle System Chaincode，LSCC），主要功能是管理部署在背书节点上的链码，并不是全生周期的管理。

1.链码的安装

链码安装接收的参数如下。

type ChaincodeDeploymentSpec struct {
// 链码描述规范
ChaincodeSpec *ChaincodeSpec
// 控制链码可用事件，预留字段
EffectiveDate *google_protobuf1.Timestamp
// 打包好的链码源代码
CodePackage []byte
// 运行环境:系统进程或者Docker
ExecEnv ChaincodeDeploymentSpec_ExecutionEnvironment
}

其中，CodePackage是已经打包好的链码源代码，会在源码基础上添加所需要的库文件，比如和背书节点通信的github.com/hyperledger/fabric/core/chaincode/shim。链码描述规范ChaincodeSpec约定链码部署或者调用的一些参数，定义如下。

type ChaincodeSpec struct {
// 链码类型:Golang、Node.js、Java、Car（Chaincode Archive）,目前只支持Golang
Type ChaincodeSpec_Type
// 链码的路径、名称和版本信息
ChaincodeId *ChaincodeID
// 链码的输入参数
Input *ChaincodeInput
// 超时时间，预留字段
Timeout int32
}

type ChaincodeID struct {
// 链码路径
Path string
// 链码名称
Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
// 链码版本
Version string `protobuf:"bytes,3,opt,name=version" json:"version,omitempty"`
}

type ChaincodeInput struct {
// 链码输入参数数组
Args [][]byte
}

在调用LSCC的时候，ChaincodeDeploymentSpec是ChaincodeInput的第2个参数，第1个参数是固定的“install”。就是说外层还有一层封装，封装在ChaincodeInvocationSpec中。

type ChaincodeInvocationSpec struct {
// 链码描述规范
ChaincodeSpec *ChaincodeSpec
// 可包含指定用户ID的生成算法，通过此方法来生成ID。
// 若不指定（或留空），将使用sha256base64算法。算法主要包含两部分：
// 1, hash函数；2， 将用户的（字符串）输入进行字节解码
// 目前，支持带BASE64的SHA256（例如： idGenerationAlg='sha256base64'）
IdGenerationAlg string
}

其中，ChaincodeInvocationSpec中的ChaincodeSpec指定的ChaincodeId.Name是固定的“lscc”，消息结构图如图9-7所示。

图9-7　链码安装结构图

再加上必需的消息头，链码安装提交的Proposal逻辑结构如下。

SignedProposal: {
ProposalBytes(Proposal): {
Header: {
ChannelHeader: {
Type: "HeaderType_ENDORSER_TRANSACTION",
TxId: TxId,
Timestamp: Timestamp,
Extension(ChaincodeHeaderExtension): {
PayloadVisibility: PayloadVisibility,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
}
},
Epoch: Epoch
},
SignatureHeader: {
Creator: Creator,
Nonce: Nonce
}
},
Payload: {
ChaincodeProposalPayload: {
Input(ChaincodeInvocationSpec): {
ChaincodeSpec: {
Type: "Golang",
ChaincodeId: {
Name: "lscc"
},
Input(ChaincodeInput): {
"install",
ChaincodeDeploymentSpec: {
ChaincodeSpec: {
Type: Type,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
},
Input(ChaincodeInput): Args,
Timeout: Timeout
},
EffectiveDate: EffectiveDate,
CodePackage: CodePackage,
ExecEnv: "Docker"
}
}
}
},
TransientMap: TransientMap
}
}
},
Signature: Signature
}

其中，有"（）"的地方，比如ProposalBytes（Proposal）中，ProposalBytes代表字段名称，Proposal代表实际的类型。有""的是请求固定的内容，其他的变量会根据实际的请求发生变化。TxId是交易号，是在客户端构建Proposal的时候生成的，生成规则是：

TxID = HASH(Nonce + Creator)

其中，Nonce是随机数，Creator是创建请求的用户身份信息，独立生成的交易号出现冲突的可能性是比较小的。Creator还有别的用处，背书节点接收到请求以后LSCC会根据这个信息验证是否有权限安装链码或者其他的操作。Signature是Creator对整个Proposal的签名，能验证消息的完整性，起到防抵赖的作用。

TransientMap是应用程序提交给背书节点处理但不记录到账本中的数据，链码可以通过GetTransient接口获取到，用来传输一些敏感信息。比如，链码传递的参数Input里的内容都是加密的，密钥通过TransientMap传递给链码，链码可以解密调用参数的内容，最后记录到账本里的只有加密的参数，TransientMap的内容在ESCC签名背书的时候是会删掉的。

LSCC直接处理的是ChaincodeDeploymentSpec结构，首先会根据签名信息验证权限，然后会检查链码的名称和版本是否合法。链码名称和版本的命名都是有内置规范的，如表9-6所示。

表9-6　链码名称和版本的命名规范

最后把ChaincodeDeploymentSpec存储在背书节点文件中安装过程就结束了，并不会在多个节点直接同步。如果想要安装到多个节点，就需要客户端向不同的节点发起多次安装链码的请求。存储的文件名称是：

fileSystemPath/chaincodes/chaincodeName:chaincodeVersion

其中，fileSystemPath是环境变量CORE_PEER_FILESYSTEMPATH或者core.yaml文件配置的peer.fileSystemPath定义的，chaincodes是固定的子目录。从上面的目录结构可以看出，链码存储的时候并没有区分不同链的命名空间，实际多链是共享链码的。在链码部署和调用的时候会根据背书节点本地存储的链码构建链码镜像，启动链码容器。

2.链码的部署

链码部署提交的Proposal逻辑结构与安装的逻辑结构基本相同，区别在于Chaincode InvocationSpec.ChaincodeSpec.Input内容不一样。

Input(ChaincodeInvocationSpec): {
"deploy",
ChaincodeDeploymentSpec: {
ChaincodeSpec: {
Type: Type,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
},
Input(ChaincodeInput): Args,
Timeout: Timeout
},
EffectiveDate: EffectiveDate,
CodePackage: CodePackage,
ExecEnv: "Docker"
},
chainID,
policy(SignaturePolicyEnvelope): {
Version: Version,
Rule(SignaturePolicy): {
Type: SignedBy/NOutOf
},
Identities(common1.MSPPrincipal): [
PrincipalClassification: PrincipalClassification,
Principal: Principal
]
},
"escc",
"vscc"
}

其中，第1个参数是"deploy"，后面增加了几个参数，"escc"和"vscc"默认是内置的系统链码，也可以自行实现替换。SignaturePolicyEnvelope的详细介绍参考成员管理章节的内容。

部署的时候会检查实例化策略，默认的实例化策略是管理员权限。最后会记录ChaincodeData到链上。

type ChaincodeData struct {
// 链码名称
Name string
// 链码版本
Version string
// 链码的ESCC，默认是内置的escc
Escc string
// 链码的VSCC，默认是内置的vscc
Vscc string
// 链码的背书策略
Policy []byte
// 链码源代码哈希和元数据哈希组成的CDSData
Data []byte
// 链码编号，预留字段
Id []byte
// 链码实例化策略
InstantiationPolicy
}

type CDSData struct {
// 链码源代码哈希=hash(chaincode)
CodeHash []byte
// 链码元数据哈希=hash(chaincodeName+chaincodeVersion)
MetaDataHash []byte
}

实例化只能调用执行一次，实例化的时候检查链上有ChaincodeData就说明已经实例化过了。

背书节点在调用LSCC执行完部署的工作后，还会继续执行实例化的操作。这种情况只有链码是LSCC，并且调用是部署（deploy）和升级（upgrade）的时候才执行。就是说，只有LSCC的Proposal里的ChaincodeInvocationSpec才会有嵌套ChaincodeDeploymentSpec的结构。

链码的安装是和具体的链没有关系的，并不会记录数据到账本里，同一个链码是可以多个链共享的，实例化的时候才会记录到不同链的账本数据里，不同链的数据是独立隔离的。

3.链码的升级

链码的升级和部署比较类似，也需要先安装链码，再执行链码的升级。链码升级需要保证链码的名称和升级之前的一样，否则会被当成另外一个链码。由于链码的版本可以是符合命名规范的字符串，并不是严格意义的递增关系，是以提交的先后顺序为准的，链码升级的时候会比较升级的版本是否和最新版本的版本号一致。与链码部署不同的地方在于，链码升级的时候会检查链码最新版本的实例化策略，再执行链码的更新，更新的过程中也会执行实例化的操作。

链码升级并不会删除老版本的链码，多个版本是可以并存的，需要手工维护应用程序和链码之间的对应关系，避免调用出现错误。链码升级的操作过程参考前面介绍的内容。

4.链码信息的查询

可以查询如下几个接口的信息，如表9-7所示。

表9-7　链码信息查询的接口列表

9.3.2　配置管理系统链码

配置管理系统链码（CSCC）的全称是Configuration System Chaincode，主要功能是管理记账节点上的配置信息。

1.记账节点加入链

记账节点加入链的Proposal请求的ChaincodeInput不再是多层的嵌套了，结构如下。

SignedProposal: {
ProposalBytes(Proposal): {
Header: {
ChannelHeader: {
Type: "HeaderType_CONFIG",
TxId: TxId,
Timestamp: Timestamp,
Extension(ChaincodeHeaderExtension): {
PayloadVisibility: PayloadVisibility,
ChaincodeId: {
Path: Path,
Name: Name,
Version: Version
}
},
Epoch: Epoch
},
SignatureHeader: {
Creator: Creator,
Nonce: Nonce
}
},
Payload: {
ChaincodeProposalPayload: {
Input(ChaincodeInvocationSpec): {
ChaincodeSpec: {
Type: "Golang",
ChaincodeId: {
Name: "cscc"
},
Input(ChaincodeInput): {
"JoinChain",
genesisBlock
}
}
},
TransientMap: TransientMap
}
}
},
Signature: Signature
}

其中，genesisBlock是创建通道时生成的创世区块。CSCC的Peer节点加入通道过程如图9-8所示。

图9-8　Peer节点加入通道流程图

其中，区块的有效性验证主要包括如下几个方面。

·类型检查：查看区块类型是否是HeaderType_CONFIG。

·配置检查：查看配置区块里是否包含Application信息。

·权限检查：是否满足提交策略，比如是否和节点同一个MSP。

本地创建账本的时候有两个操作，先是在本地链的数据库idStore里添加记录，然后再把创世区块写到本地文件系统里。Peer节点加入链的时间是不一样的，需要通过Gossip服务同步数据，根据节点的配置是主节点还是参加主节点选举，若是主节点则会主动跟排序服务节点连接，否则从组织内其他节点处同步数据。本地创建链会从创世区块里读取链配置相关的信息，记录到本地链的映射表里。

Peer节点加入链完成以后，利用Golang的通道机制发送一个事件，接收到事件的协程异步发送给注册监听的客户端。详细内容参考前面事件相关的内容。

2.链相关信息查询

通过CSCC可以查询如下几个接口的信息，如表9-8所示。

表9-8　链信息查询的接口列表

9.3.3　查询管理系统链码

查询管理系统链码（QSCC）的全称是Query System Chaincode，主要功能是提供查询记账节点的账本数据，包括区块和交易数据、区块链信息等，如表9-9所示。

表9-9　账本信息查询的接口列表

从索引数据库查询通用的过程是根据查询项从索引数据库查询出FLP（File Location Pointer），再根据FLP在区块中定位获取所需内容。索引相关的内容参考前面已经介绍过的章节。

9.3.4　交易背书系统链码

交易背书系统链码（ESCC）的全称是Endorsement System Chaincode，主要功能是对交易进行结果的结构转换和签名背书。

ESCC是背书节点模拟执行完链码后，对执行的结果Response进行转换，去掉里面的Transient信息，再添加签名背书，最后封装成ProposalResponse，结构如下。

ProposalResponse: {
Version: Version,
Timestamp: Timestamp,
Response: {
Status: Status,
Message: Message,
Payload: Payload
},
Payload(ProposalResponsePayload): {
ProposalHash: ProposalHash,
Extension(ChaincodeAction): {
Results(TxRwSet): {
NsRwSets(NsRwSet): [
NameSpace: NameSpace,
KvRwSet: {
Reads(KVRead): [
Key: Key,
Version: {
BlockNum: BlockNum,
TxNum: TxNum
}
],
RangeQueriesInfo(RangeQueryInfo): [
StartKey: StartKey,
EndKey: EndKey,
ItrExhausted: ItrExhausted,
ReadsInfo: ReadsInfo
],
Writes(KVWrite): [
Key: Key,
IsDelete: IsDelete,
Value: Value
]
}
]
},
Events(ChaincodeEvent): {
ChaincodeId: ChaincodeId,
TxId: TxId,
EventName: EventName,
Payload: Payload
}
Response: {
Status: Status,
Message: Message,
Payload: Payload
},
ChaincodeId: ChaincodeId
}
},
Endorsement: {
Endorser: Endorser,
Signature: Signature
}
}

其中，Endorsement就是背书的内容，包含背书节点的签名证书Endorser和背书节点对ProposalResponsePayload+Endorser的签名，即：

Signature=Endorser.Sign(ProposalResponsePayload+Endorser)

背书代表背书节点用自己的身份对模拟执行结果进行担保，通过证书和签名就可以验证背书是否是有效的。

9.3.5　交易验证系统链码

交易验证系统链码（VSCC）的全称是Validation System Chaincode，主要功能是记账前对区块和交易进行验证。ESCC是背书节点独立对模拟执行结果的背书，VSCC是对多个背书验证是否符合背书策略。VSCC验证交易背书的过程参考第3章的交易背书策略部分。

9.4　链码的相互调用

在第5章中，我们已经介绍过，不同链的账本数据和状态数据等都是物理隔离或者逻辑隔离的。对于同一个链不同链码的状态数据，会按链码名称生成不同前缀的键，对状态数据进行逻辑隔离。不同的链码实现不同的业务逻辑，是可以相互调用的。调用的方法通过shim.InvokeChaincode：

InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response

其中，chaincodeName是被调用链码的名称，链码名称可以指定版本，比如mycc：1.0这样的形式，args是被调用链码的参数，channel是被调用链码的通道名称，默认调用的是同一个链的链码。链码名称会规范化处理，生成的链码名称是：

chaincodeName:chaincodeVersion/channelName

其中的链码版本chaincodeVersion和通道名称channelName都是可选的，对应的分隔符也是可选的，所以链码名称可能有如下几种组合，如表9-10所示。

表9-10　链码相互调用的名称列表

从上面链码的规范化名称，我们可以看到，链码的相互调用分为以下两种情况：

·同一个链的链码相互调用；

·不同链的链码相互调用。

同一个链或者不同链的调用流程是基本一样的，调用shim.InvokeChaincode后会构造一个类型为ChaincodeMessage_INVOKE_CHAINCODE的ChaincodeMessage，消息是通过链码和背书节点之间的gRPC连接直接提交给背书节点的有限状态机处理的，不是通过背书节点接收Proposal的背书流程。背书节点对链码相互调用的处理过程如图9-9所示。

不同链的链码相互调用不同的地方在于会生成一个新的交易模拟器TxSimulator，实现对被调用链数据的访问，最终生成交易的读写集只会包含调用链的数据，并不会修改被调用链的状态数据。如果是调用相同链的链码，会复用相同的交易模拟器TxSimulator，链码执行的结果会修改最终的状态数据。

9.5　背书节点和链码的有限状态机

链码本身是不会存储任何数据的，业务逻辑处理过程中是通过建立好的gRPC连接实现和背书节点的交互，交互过程是通过有限状态机（Finite State Machine）来实现的。有限状态机有下面几个特点：

·状态是有限的，能够遍历完所有的状态；

·有一个初始状态和终止状态以及若干中间状态；

·任意时刻只会处于其中的一个状态；

·处于某个状态下能处理的事件是有限的；

·状态转移之间的转移条件是确定的。

背书节点端和链码端都通过有限状态机定义了各自生命周期内所处的所有状态，以及如何在各种状态下响应各种事件和转移到其他状态。具体实现采用第三方的库http://github.com/looplab/fsm ，我们就用fsm来代表这个库。fsm的状态直接用字符串来表示，定义了状态转移映射表。

图9-9　背书节点对链码相互调用的处理过程图

type EventDesc struct {
// 事件名称
Name string
// 状态转移的源状态列表
Src []string
// 状态转移的目的状态
Dst string
}

其中，状态转移的源状态列表Src是一个数组，是把状态转移合并了，多个状态都可以接收相同的事件转移到相同的目的状态。

回调函数映射表Callbacks定义了事件处理和状态转移的执行函数。

type Callbacks map[string]Callback

type Callback func(*Event)

type Event struct {
// 对当前FSM的引用
FSM *FSM
// 事件名称
Event string
// 交易前的状态
Src string
// 交易后的状态
Dst string
// 回调函数中返回的错误(可选)
Err error
// 回调函数传入的参数(可选)
Args []interface{}
// 数字标识位，当交易取消时设定
canceled bool
// 数字标识位，当异步交易时设定
async bool
}

回调函数映射表Callbacks定义了两种类型的函数。

·事件处理的执行函数：处理某个事件前后的操作，定义规则是before_EVENT和after_EVENT，其中EVENT是某个具体的事件名称。

·状态变化的执行函数：进入某个状态和离开某个状态的操作，定义规则是enter_STATE和leave_STATE，其中STATE是某个具体的状态名称。

有限状态机通用的事件处理流程如图9-10所示。

图9-10　有限状态机处理流程图

9.5.1　背书节点和链码之间的事件

背书节点和链码之间的事件列表，如表9-11所示。

表9-11　背书节点和链码之间的事件列表

9.5.2　背书节点的有限状态机

背书节点的状态列表，如表9-12所示。

表9-12　背书节点的状态列表

如图9-11所示，背书节点的有限状态机进入ready的运行状态后，就可以接收链码的请求了，主要是和状态数据操作相关的接口，比如状态数据的更新PUT_STATE、DEL_STATE，状态数据的GET_STATE、GET_STATE_BY_RANGE等。背书节点接收到链码的请求后，通过交易模拟器实现状态数据的读写，返回给链码进行业务逻辑的处理，实现链码的无状态部署和运行。

图9-11　背书节点的有限状态机

9.5.3　链码的有限状态机

链码的状态列表，如表9-13所示。

表9-13　链码的状态列表

链码是主动给背书节点发起的请求，只需要接收背书节点返回的数据就可以了，所以链码侧的有限状态机比较简单，如图9-12所示。

图9-12　链码的有限状态机

同一个链码也可能同时和背书节点进行交互，链码侧也会存在背书节点侧消息的数据分发问题。链码维护了交易的Golang通道responseChannel映射表。

responseChannel map[string]chan pb.ChaincodeMessage

其中，键是交易号txid，值是不同交易号的Golang通道pb.ChaincodeMessage。链码有限状态机接收到ChaincodeMessage_RESPONSE的消息后，通过给每个交易建立的Golang通道返回给调用接口。能够这么实现的原因是，尽管可能同时在处理不同的交易，但是同一个交易是顺序执行的，返回的结果也是顺序的。

9.6　本章小结

本章详细地介绍了Hyperledger Fabric 1.0的智能合约，链码的生命周期管理及其实现原理。

链码的生命周期包括链码的打包、安装、实例化、升级、停止和启动，其中链码的停止和启动还没有实现，只能手工处理。所有的操作都有权限的控制，通过通道策略、实例化策略和背书策略等进行管理。实际上，目前版本的Hyperledger Fabric 1.0实现的权限控制都是粗粒度的，以后版本可能会有变化。

链码分为普通链码和系统链码，普通链码是应用程序提交的完成业务功能的链码。系统链码包括生命周期管理系统链码（LSCC）、配置管理系统链码（CSCC）、查询管理系统链码（QSCC）、交易背书系统链码（ESCC）和交易验证背书链码（VSCC），其中ESCC和VSCC有默认的实现，也可以根据功能需求实现新的ESCC和VSCC。

最后介绍了背书节点侧和链码侧的有限状态机，了解了链码内部的实现原理，理解下一章的链码接口就比较轻松了。

第三篇　应用篇

第10章　超级账本的应用开发模型

第11章　从零开始部署超级账本网络

第12章　超级账本的应用开发实例

第10章　超级账本的应用开发模型

前面章节介绍的都是Hyperledger Fabric 1.0内部的机制和原理，不做任何源码级别的改动就可以部署起来，提供基本的区块链底层平台服务。本章会从应用的角度出发，介绍如何开发基于Fabric网络的区块链应用。

10.1　应用开发模型

我们从程序开发角度来看看各个模块的交互，首先应用程序接收用户的请求，然后可能调用智能合约，也可能直接访问区块链。智能合约在执行的过程中可能对区块链进行操作，并产生事件。Hyperledger Fabric 1.0的应用开发模型如图10-1所示。

图10-1　Hyperledger Fabric 1.0的应用开发模型

10.2　应用程序开发的SDK

本节先介绍应用开发SDK的基本功能。

10.2.1　概述

HFC（Hyperledger Fabric Client）是提供给应用程序开发的SDK，提供了gRPC连接的API。API包含了交易处理、安全的成员管理服务、区块链查询和事件处理等。Hyperledger Fabric 1.0取消了0.6版本的RESTful接口，只能选择封装了gRPC接口的SDK。采用gRPC的原因主要有以下四点。

·底层的接口调用很多都是异步返回结果的，采用gRPC能够很好地双向传输数据。

·gRPC结合Protocol Buffers能减少传输数据量，提升网络传输性能。

·支持的语言较多，如Go、C#、Java、JavaScript、Python、C++等。

·和内部模块采用相同的通信接口，减少端口开放，也会减少安全的风险。

整个Fabric网络中，除了可选的fabric-ca采用的是RESTful接口之外，其他所有组件之间的通信都采用gRPC接口。

10.2.2　SDK规范

SDK定义了两种模块的接口：一个是访问fabric-ca的接口，一个是访问Fabric的接口。其中fabric-ca模块是可选的，可以选用其他成熟的第三方CA系统。官方提供了如下几种语言的SDK实现。

·Golang：https://github.com/hyperledger/fabric-sdk-go 。

·Node.js：https://github.com/hyperledger/fabric-sdk-node 。

·Python：https://github.com/hyperledger/fabric-sdk-py 。

·Java：https://github.com/hyperledger/fabric-sdk-java 。

下面以Golang为主介绍一下SDK的设计和实现。

1.访问Fabric模块介绍

我们先来看一下Fabric模块的UML图，如图10-2所示。

主要的模块包括FabricClient、Config、Channel、Peer、Orderer、User、KeyValueStore、EventHub、Logger等，SDK本身还会复用fabric源码提供的功能。还有一些其他的模块（比如Proposal、SignedProposal、ProposalResponse、Transaction、CryptoSuite等）在这里没有展示。下面介绍各模块接口。

（1）FabricClient模块

FabricClient是应用程序的入口模块，提供通道管理、链码管理、数据存储、密码学相关的功能。每个FabricClient实例对应一个区块链的网络，包括记账节点、排序节点等。如果应用程序需要访问多个网络，可以建立多个FabricClient的实例，不同的实例对应不同的网络。FabricClient模块的接口说明如表10-1所示。

图10-2　SDK中访问fabric模块的UML图

表10-1　FabricClient模块的接口列表

（2）Config模块

初始化FabricClient的时候需要离线获取配置信息，包括可信的根证书、排序服务节点证书和IP地址、记账节点证书和IP地址等，配置模块Config读取后传递给FabricClient。配置信息是动态传递的，SDK不会持久化存储，应用程序负责维护这些配置信息。Config模块的接口说明如表10-2所示。

表10-2　Config模块的接口列表

（3）Channel模块

通道是排序服务创建的隔离不同链上交易的实例，加入到不同通道的节点接收到的是不同的交易。通道在配置了排序服务节点和Peer节点后需要初始化，初始化的时候给排序服务节点发送获取配置区块的请求。Channel模块的接口说明如表10-3所示。

表10-3　Channel模块的接口列表

（4）Peer模块

Peer节点是HFC模块发送背书请求、交易查询的节点。Peer实例包含了节点名称、地址、角色、注册证书（ECert）等信息。Peer模块的接口说明如表10-4所示。

表10-4　Peer模块的接口列表

（5）Orderer模块

Orderer节点是HFC模块发送交易进行排序的节点。Orderer实例包含了排序服务节点地址信息，定义了发送原子广播请求和获取区块的接口。Orderer模块的接口说明如表10-5所示。

表10-5　Orderer模块的接口列表

（6）User模块

User代表了已经生成注册证书和签名密钥的实体，注册证书必须是区块链网络信任的CA颁发的证书，只有生成了注册证书的实体才能进行部署链码、提交交易和查询交易等操作。注册证书可以从第三方CA获取，也可以通过fabric-ca模块获取。

特别说明一下，用户身份（User Identity）和节点身份（Peer Identity）是有区别的。在SDK里面，用户身份能访问私钥信息，是可以进行签名的。而节点身份不能访问私钥，只能验证签名。

User模块的接口说明如表10-6所示。

表10-6　User模块的接口列表

（7）KeyValueStore模块

KeyValueStore提供给应用程序保存敏感信息的功能，比如用户私钥、证书信息等。KeyValueStore模块的接口说明如表10-7所示。

表10-7　KeyValueStore模块的接口列表

（8）EventHub模块

EventHub封装了与Peer节点交互的事件流，接收Peer的各种异步通知事件。EventHub模块的接口说明如表10-8所示。

表10-8　EventHub模块的接口列表

（9）Logger模块

Logger是日志模块，提供了不同的日志接口，基本都是日常开发过程中用到的通用日志模块，这里就不详细展开了。

2.访问fabric-ca模块介绍

我们再来看一下fabric-ca模块的UML图，如图10-3所示。

图10-3　SDK中访问fabric-ca模块的UML图

这里主要介绍FabricCAClient模块，其他的模块在前面已经介绍过。

FabricCAClient是应用程序的入口模块，提供通道管理、链码管理、数据存储、密码学相关的功能。每个FabricCAClient实例对应一个区块链的网络，包括记账节点、排序节点等。如果应用程序需要访问多个网络，可以建立多个FabricCAClient的实例，不同的实例对应不同的网络。FabricCAClient模块的接口说明如表10-9所示。

表10-9　FabricCAClient模块的接口列表

10.2.3　应用场景介绍

下面介绍几个基于SDK的典型应用场景，这些应用场景在应用开发的过程中都会遇到。

1.用户登记和注册

用户登记和注册是一个可选的场景，采用fabric-ca颁发证书时，应用程序、SDK和fabric-ca之间交互的时序图如图10-4所示。

各个组件之间的交互过程如下。

第1步：根据配置文件获取CA和CSP的配置信息，也可以采用其他的方式获取到这些配置信息；

第2步：根据配置信息创建FabricClient的实例，并设置CryptoSuite和KeyValueStore等信息，FabricClient实例是整个操作的入口；

第3步：获取负责提交用户资料的登记员信息Registrar，如果不存在，则需要先初始化登记员用户，获取登记员的注册证书和私钥信息；

第4步：根据配置信息和组织信息创建FabricCAClient的实例；

第5步：根据需要登记的用户信息生成RegistrationRequest请求，提交给FabricCAClient；

第6步：登记员Registrar会提交访问fabric-ca的POST请求，请求的URL是/api/v1/register；

第7步： fabric-ca验证请求生成用户注册的密码Secret，最终返回给应用程序，完成用户信息登记的步骤；

第8步：应用程序利用申请的用户信息和返回的注册密码，调用FabricCAClient的Enroll接口；

第9步： FabricCAClient生成私钥和证书签名请求CSR（Certificate Signing Request），调用fabric-ca提供的enroll接口生成注册证书；

第10步：返回生成的注册证书和私钥给应用程序；

第11步：可选的保存用户信息到KeyValueStore里。

图10-4　用户登记和注册应用场景下SDK和其他组件交互的时序图
fabric-ca还提供了重新注册生成注册证书和吊销证书的功能，目前fabric并不能完整地支持CRL和OCSP的功能。

2.在排序服务上创建通道

创建通道需要先利用工具configtxgen生成通道配置文件mychannel.tx。创建通道应用场景下SDK和其他组件交互的时序图如图10-5所示。

图10-5　创建通道应用场景下SDK和其他组件交互的时序图

创建通道涉及3个部分，应用程序或者客户端、HFC SDK、排序服务节点。各个组件之间的交互如下。

·第1步：应用程序读取通道配置文件mychannel.tx，这个文件是利用工具configtxgen生成的，包含了通道名称、组织配置等信息，详细的内容请参考第6章。

·第2步：创建通道只和排序服务节点通信，需要通过排序服务节点的配置生成Orderer实例。

·第3步：指定通道名称，并通过通道配置文件和Orderer实例生成创建通道请求CreateChannelRequest。

·第4步：创建FabricClient实例，调用CreateChannel创建通道，输入参数是上一步生成的创建通道请求CreateChannelRequest。

·第5步： HFC SDK转换创建通道的请求CreateChannelRequest，生成HeaderType_CONFIG_UPDATE类型的交易common.Payload。

·第6步： HFC SDK对common.Payload进行签名，签名者需要有通道创建的管理员权限。

·第7步：通过Orderer实例发送SendBroadcast请求，提交请求给排序服务节点。

·第8步：排序服务节点会检查提交的请求，校验是否有权限创建新的通道，创建通道以后排序服务节点就可以接收新通道的请求了。详细的内容请参考第6章。

3.Peer节点加入通道

创建通道完成以后，排序服务节点上就有了新通道的基本信息，可以对新通道的交易进行排序打包生成区块了。下一步需要把Peer加入到新通道中，应用程序或者客户端才能通过Peer节点发起交易请求。Peer节点加入通道的时序图如图10-6所示。

图10-6　加入通道应用场景下SDK和其他组件交互的时序图

Peer节点加入通道时需要先从排序服务节点获取创世区块，再在本地Peer节点初始化链，各个组件之间的交互如下。

·第1步：必要的初始化配置，比如创建FabricClient实例，设置发起加入通道请求的用户、Channel实例、Orderer实例等。

·第2步：调用GenesisBlock的请求获取创世区块，Channel实例会构造HeaderType_DELIVER_SEEK_INFO的请求，通过Orderer实例发送SendDeliver请求给排序服务节点，获取该通道的创世区块。

·第3步：应用程序利用获取到的创世区块构造JoinChannelRequest请求，通过Channel实例发起JoinChannel请求。

·第4步： HFC SDK的JoinChannel操作会根据JoinChannelRequest请求重新构造类型为HeaderType_ENDORSER_TRANSACTION的Proposal，Proposal会用FabricClient实例设置的用户进行签名，生成SignedProposal。

·第5步：需要为每个加入通道的Peer节点创建一个Peer实例，通过Peer实例调用ProcessProposal向Peer节点发送加入通道的SignedProposal。

·第6步： SignedProposal是调用CSCC的JoinChain请求，Peer节点接收到SignedProposal请求后会调用CSCC进行必要的消息有效性检查和权限检查，然后在本地Peer节点初始化链。初始化的过程会根据通道名称在本地目录创建账本数据，写入通道的创世区块，账本数据的目录结构请参考第5章。

·第7步：创建好通道的本地账本以后，Peer节点会启动Gossip服务从排序服务节点同步最新的区块数据。根据Peer节点的配置，参与主节点的选举或者直接作为主节点进行Peer节点之间的P2P通信，详细的过程参考第4章。

·第8步： Peer节点完成初始化链以后就可以接收新链的交易请求了。

4.通过Peer节点实例化链码

Peer节点加入到通道以后，需要部署链码才能模拟执行智能合约的功能。安装链码的过程相对比较简单，第9章已经介绍过。下面直接讨论实例化链码的流程，如图10-7所示。

安装链码只是把包含链码源码的ChaincodeDeploymentSpec上传到Peer节点，并没有进行初始化。实例化链码的时候才会创建链码镜像，启动链码容器并调用链码的Init接口初始化，生成的交易会发送给排序服务节点生成区块记录到账本中。各个组件之间的交互过程如下。

·第1步：实例化操作需要创建多个实例，包含FabricClient实例、User实例、Channel实例、Peer实例等。

·第2步：通过调用Channel实例的SendInstantiateProposal进行链码实例化。

·第3步： HFC SDK会构造包含ChaincodeDeploymentSpec的ChaincodeInvocationSpec，调用的是LSCC的deploy请求，详细的结构请参考第9章。

·第4步：发送给Peer节点的请求同样会用Channel关联的用户进行签名，通过Peer实例的ProcessTransactionProposal提交生成的SignedProposal。

·第5步：每次给Peer节点发送SignedProposal的时候都会新建一个gRPC的连接，通过ProcessProposal接口提交请求。

·第6步： Peer节点通过SignedProposal进行验证以后，会调用LSCC执行链码部署的操作，详细的过程请参考第9章。

·第7步： Peer节点返回的只是背书节点模拟执行和背书签名的结果，还需要提交给排序服务节点生成最终的区块才能生效，调用的过程同Peer节点接入通道的过程。

·第8步：生成的新区块会通过主节点分发给组织内的其他Peer节点，详细的内容请参考第4章。

图10-7　实例化链码应用场景下SDK和其他组件交互的时序图

5.发起交易请求并生成区块

实例化链码的过程本身也是一种交易，所以发起交易请求的过程和上一节的过程非常类似，如图10-8所示。

这里只讨论和上一节实例化链码过程不同的几个地方。

·普通的交易请求调用链码的Invoke接口，实例化链码调用的是Init接口。

·普通的交易请求是不嵌套的ChaincodeInvocationSpec请求，包含通道的名称和调用链码的函数和参数等。

·实例化链码的时候才开始构建链码镜像并启动链码容器，所以实例化链码的过程都比较慢。调用链码的背书节点已经启动了链码容器，所以调用链码的过程是比较快的，除非链码的功能比较复杂或者出于系统的原因。第一次接收调用链码请求的背书节点会自动构建链码镜像并启动链码容器，返回结果的时间会比较长。

图10-8　调用链码应用场景下SDK和其他组件交互的时序图

10.3　链码的开发和调试

链码是通过SDK和背书节点通信的，就是说链码的SDK只需要实现接口的定义就能和背书节点交互。理论上，链码是可以支持多种语言的。目前的版本（1.0.0）支持的语言只有Golang，其他语言（比如Java）还不够完善，正式发布的时候是禁用的。Car支持利用fabric-chaintool打包的代码，目前也只支持Golang。下面详细地介绍Golang语言的链码提供的接口，讨论如何开发和测试链码。

10.3.1　链码需要实现的接口

链码必须要实现的接口如下：

type Chaincode interface {
// 初始化工作，一般情况下仅调用一次
Init(stub ChaincodeStubInterface) pb.Response

// 查询或更新状态数据，可多次调用
Invoke(stub ChaincodeStubInterface) pb.Response
}

其中，查询和调用都是在同一个接口Invoke里实现的，不再实现单独的Query接口。

一个链码的样例模版如下，更多的样例可以参考fabric源码或者fabric-samples下的例子：

package main

import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
)

type SimpleChaincode struct {
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface, function string,
args []string) ([]byte, error) {
fmt.Printf("SimpleChaincode Init: finished\n")

return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface, function
string, args []string) ([]byte, error) {
var err error
func, args := stub.GetFunctionAndParameters()

fmt.Printf("SimpleChaincode Invoke: func=%v, args = %v\n", func, args)

return shim.Success(nil)
}

func main() {
err := shim.Start(new(SimpleChaincode))
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}

应用程序发起的调用是如何调用这两个接口的呢？

10.3.2　链码的SDK提供给链码的接口

链码的SDK是shim，提供给链码的接口包括如下几种类型：

·与链码调用参数解析相关；

·与交易信息解析相关；

·与状态数据操作相关；

·与链码调用相关；

·与事件处理相关；

·与辅助操作相关。

下面逐一介绍每种类型具体的接口定义及其功能，这在实际的链码编写过程中都会用到。

1.与链码调用参数解析相关的接口
shim提供了多种获取链码调用参数的接口，可以根据不同的场景使用，如表10-10所示。

表10-10　与链码调用参数解析相关的接口

其中，接口GetArgsSlice返回的字符数组中间没有分隔符，没有办法还原成原始的函数名称和参数信息，实际链码编写过程中基本不会使用这个接口。

2.与交易信息解析相关的接口

与提交的交易信息解析相关的接口如表10-11所示。

表10-11　与交易信息解析相关的接口

需要特别说明的是GetBinding接口，binding是和交易相关的一些指纹信息，计算方法如下：

binding = HASH(Nonce, Creator, Epoch)

其中，Epoch代表的是时间窗口，背书节点在验证Proposal或者记账节点验证交易时会确认：

·交易里的Epoch和当前Epoch的一致性；

·同一个Epoch里没有出现相同交易号的交易。

目前Epoch的管理并没有实现，强制设置为0。其他接口的说明参考前面LSCC里的内容。

3.与状态数据操作相关的接口

与状态数据操作相关的接口又分为3类：

·基于单一键操作的接口；

·基于键范围查询的接口；

·基于值内容查询的接口。

链码基于单一键操作的接口如表10-12所示。

表10-12　链码基于单一键操作的接口

链码基于键范围查询的接口如表10-13所示。

表10-13　链码基于键范围查询的接口

部分组合键查询实际按键的前缀查询，例如，某个链码的组合键格式为K1-K2-K3，支持按K1或K1-K2前缀查询，底层转化为区间查询实现。更多组合键查询的内容参考5.6节。

链码基于值内容查询的接口如表10-14所示。

表10-14　链码基于内容查询的接口

基于值的查询需要状态数据库的支持，目前只有CouchDB才能支持。

4.与链码调用相关的接口

与链码调用相关的接口如表10-15所示。

表10-15　与链码相互调用的接口

链码是可以调用其他链码的接口的，目前实现的原则如下。

·如果被调用的链码和调用的链码在相同的链上，会继承交易模拟器，被调用链码生成的读写集添加到交易中，读写集是以链码编号（chaincodeId）作为命名空间的。就是说相同链的链码调用的读写都是有效的，会影响到被调用链码的状态数据库。

·如果被调用的链码和调用的链码不在相同的链上，会生成一个新的交易模拟器，返回调用执行的Response给链码，不会添加读写集到交易中。就是说，不同链的链码调用只能查询状态数据，不能写入。

5.与事件处理相关的接口

与事件处理相关的接口如表10-16所示。

表10-16　与链码事件处理相关的接口

6.与辅助操作相关的接口

与辅助操作相关的接口如表10-17所示。

表10-17　与链码辅助操作相关的接口

日志操作相关的接口：Debug、Info、Notice、Warning、Error、Critical和Debugf、Infof、Noticef、Warningf、Errorf、Criticalf等，其中带'f'的版本是有格式化字符串的。

链码的SDK也会有日志，名称是shim，日志级别由环境变量CORE_CHAINCODE_LOGGING_SHIM控制，是启动链码的时候传递给链码容器的。

10.3.3　链码开发的注意事项

目前链码的设计并没有考虑区块链外部数据源的问题，在链码开发过程中需要注意以下的事项。

·不能使用不确定性的变量作为计算的输入：比如不能采用随机数或者获取系统当前时间等。链码会在不同的节点上多次运行，但是运行的时间并不严格一致，环境的查询导致不同节点上执行的结果不一致，最终无法达成共识。

·避免调用外部数据接口导致重复计算：比如多个节点多次调用外部写数据的接口，可能会导致区块链外部重复计算。这种情况多个链码的执行结果可能是一致的，不会导致共识失败，但会影响外部的一致性，这是一种逻辑错误。

10.3.4　链码的调试

自动部署的链码都是运行在容器中的，开发调试过程比较复杂，需要经历如下的步骤，如图10-9所示。

图10-9　生产环境下链码的调试流程图

其实，Fabric本身提供了开发模式启动节点：

peer node start --peer-chaincodedev=true

省略中间的创建通道的过程，编译和启动链码的过程如下：

cd examples/chaincode/go/chaincode_example02
go build
CORE_CHAINCODE_LOGLEVEL=debug CORE_PEER_ADDRESS=127.0.0.1:7051 CORE_CHAINCODE_ID_NAME=mycc:0 ./chaincode_example02

后面是正常的安装和实例化的操作，步骤如图10-10所示。

图10-10　调试环境下链码的调试流程图

在这个模式下，链码直接以可执行的程序运行，开发调试不再需要创建链码镜像，能够快速调试链码程序。

10.4　本章小结

本章先介绍了基于超级账本的应用开发模型，从应用开发的角度看，主要有两个部分，一个部分是基于不同语言的SDK开发和区块链网络交互的应用程序，另外一个部分是实现超级账本的智能合约。目前SDK提供了4种语言的版本，本章主要以Golang版本为基础介绍了SDK的各个模块及其主要的功能，然后介绍了几种主要应用场景的调用时序图，方便了解应用程序、SDK和超级账本节点之间的交互关系。

最后介绍了超级账本的智能合约，链码的基本框架，链码的主要接口及其功能，如何编写链码和调试。

了解了以上的内容，就能够动手搭建符合自身业务场景的区块链网络，利用区块链技术实现具有智能合约、不可篡改等特性的应用。

第11章　从零开始部署超级账本网络

在第2章我们简要地搭建了超级账本的网络，本章我们详细地介绍如何从头进行初始化的配置，手动部署超级账本的节点和链码，调用链码实现智能合约的功能。

11.1　准备超级账本运行环境

本节介绍多种构建超级账本运行环境的方法，然后介绍如何编译超级账本的镜像文件。

11.1.1　超级账本运行环境

链码依赖于Docker才能启动运行，超级账本的各节点也推荐运行在Docker容器中，方便系统的运维管理。在开发的过程中，有多种运行方式可以选择：基于Vagrant的运行环境、基于Virtualbox的运行环境和基于Docker的运行环境。

1.基于Vagrant的运行环境

Vagrant用一致的工作流程提供了易于配置、可重复、便携的工作环境，让开发人员可以快速地创建和销毁虚拟机，帮助团队最大限度地提高生产力和灵活性。图11-1是基于Vagrant的运行环境示意图，Vagrant同时支持Linux、MacOS或Windows等不同的平台，通过相同的配置文件Vagrantfile启动Virtuabox虚拟机。虚拟机的操作系统是Ubuntu 16.04，启动的过程会调用脚本devenv/setup.sh安装所需要的软件并做相应配置，包括安装Docker、Docker Compose、Golang、Node.js、OpenJDK、Gradle等，还会修改系统的文件描述符数等，这样统一用同一套配置运行相同的程序，就不会再有“我的环境有问题”的借口了，也不会存在工作习惯划分出不同阵营的情况，非常适合工作团队合作。图11-1是基于Vagrant的运行环境示意图。

图11-1　基于Vagrant的运行环境示意图

（1）配置文件Vagrantfile

我们先来看一下配置文件Vagrantfile的内容。

SRCMOUNT = "/hyperledger"
LOCALDEV = "/local-dev"

$script = <<SCRIPT
set -x
echo "127.0.0.1 couchdb" | tee -a /etc/hosts
export DOCKER_STORAGE_BACKEND="#{ENV['DOCKER_STORAGE_BACKEND']}"
cd //{SRCMOUNT}/devenv
./setup.sh
SCRIPT

Vagrant.require_version ">= 1.7.4"
Vagrant.configure('2') do |config|
config.vm.box = "ubuntu/xenial64" // 基础镜像
config.vm.network :forwarded_port, guest: 7050, host: 7050, id: "orderer",
host_ip: "localhost", auto_correct: true // 排序服务
config.vm.network :forwarded_port, guest: 7051, host: 7051, id: "peer", host_ip:
"localhost", auto_correct: true // Peer节点
config.vm.network :forwarded_port, guest: 7053, host: 7053, id: "peer_event",
host_ip: "localhost", auto_correct: true // 事件服务
config.vm.network :forwarded_port, guest: 7054, host: 7054, id: "ca", host_ip:
"localhost", auto_correct: true // fabric-ca
config.vm.network :forwarded_port, guest: 5984, host: 15984, id: "couchdb",
host_ip: "localhost", auto_correct: true // CouchDB
config.vm.synced_folder "..", "#{SRCMOUNT}" // 同步目录
config.vm.synced_folder "..", "/opt/gopath/src/github.com/hyperledger/fabric" //同步目录
config.vm.synced_folder ENV.fetch('LOCALDEVDIR', ".."), "#{LOCALDEV}" // 同步目录
if File.exist?("../../fabric-ca")
config.vm.synced_folder "../../fabric-ca", "/opt/gopath/src/github.com/
hyperledger/ fabric-ca"
end
config.vm.provider :virtualbox do |vb|
vb.name = "hyperledger" // 虚拟机名称
vb.customize ['modifyvm', :id, '--memory', '4096'] // 内存大小
vb.cpus = 2 // CPU
storage_backend = ENV['DOCKER_STORAGE_BACKEND']
case storage_backend
when nil,"","aufs","AUFS"
// 不需要额外的操作
when "btrfs","BTRFS"
// 添加一个btrfs的卷
IO.popen("VBoxManage list systemproperties") { |f|
success = false
while line = f.gets do
// 查找虚拟机文件存储目录
machine_folder = line.sub(/^Default machine folder:\s*/,"")
if line != machine_folder
btrfs_disk = File.join(machine_folder, vb.name, 'btrfs.vdi')
unless File.exist?(btrfs_disk)
// 创建btrfs磁盘
vb.customize ['createhd', '--filename', btrfs_disk,
'--format', 'VDI', '--size', 20 * 1024]
end
// 添加磁盘到虚拟机
vb.customize ['storageattach', :id, '--storagectl', 'SATA
Controller', '--port', 1, '--device', 0, '--type', 'hdd', '--medium', btrfs_disk]
success = true
break
end
end
raise Vagrant::Errors::VagrantError.new, "Could not provision btrfs
disk" if !success
}
else
raise Vagrant::Errors::VagrantError.new, "Unknown storage backend type:
{storage_backend}"
end
end
config.vm.provision :shell, inline: $script
end

Vagrant.require_version“>=1.7.4”是对Vagrant本身版本的要求，这里定义的版本必须大于等于1.7.4。config.vm.box定义了基础镜像的名称和版本，config.vm.network定义了与宿主机之间的端口转发，预留端口间的对应关系如表11-1所示。实际用途可以自己定义，也可以修改这个文件增加端口的映射。

表11-1　Vagrant的端口映射列表

config.vm.synced_folder是和宿主机共享目录的配置，默认是把源代码的目录映射到了/hyperledger、/local-dev、/opt/gopath/src/github.com/hyperledger/fabric等几个目录下，任何一个目录下修改都能在其他目录下看到变化，和宿主机上的目录是同步的。

（2）Vagrant镜像文件Box

Vagrant生成的镜像叫Box，官方提供了存储仓库：https://atlas.hashicorp.com/boxes/search ，可以搜索到公开的Box，比如hyperldger的基础镜像https://atlas.hashicorp.com/hyperledger/boxes/fabric-baseimage ，最新版本是0.3.0。目前官方并不推荐使用Vagrant的方式构建开发环境，已经很久没有更新了。

Box支持Docker、Hyper-V、VMware、VirtualBox等不同的Provider类型。Box是采用tar、tar.gz或者zip压缩的，压缩包里的内容根据Provider不同有所区别，例如，本地的一个baseimage-public.box里包含内容如下。

localhost:fabric-baseimage clarity$ tar -tf baseimage-public.box
Vagrantfile
box.ovf
metadata.json
packer-virtualbox-iso-1482837643-disk1.vmdk

其中metadata.json是必须的，需要指定Privider的类型，内容如下：

localhost:fabric-baseimage clarity$ cat metadata.json
{"provider":"virtualbox"}

VirtualBox的虚拟机默认使用第一个网卡进行通信，需要设置成NAT模式，Vagrantfile里的config.vm.base_mac设置的就是NAT网络设备的macOS地址：

localhost:fabric-baseimage clarity$ cat Vagrantfile
Vagrant.configure("2") do |config|
config.vm.base_mac = "08002730B696"
end

Vagrant启动VirtualBox虚拟机的时候会导入压缩包里的box.ovf文件，OVF是Open Virtualization Format的简称，是一种开放的虚拟机打包和分发的标准，更多的内容参考http://www.dmtf.org/standards/ovf 。根据“References->File的ovf：href”读取真正的虚拟机文件，其他是CPU、网络、内存等的设置。

（3）Vagrant支持的虚拟机环境

Provider是Vagrant支持的虚拟机运行环境，目前支持Docker、Hyper-V、VMware、VirtualBox等几种类型，是和Box的文件格式相对应的。除此之外，还可以对Provider进行定制，https://github.com/mitchellh/vagrant-aws 以插件的形式提供了对AWS的支持，也可以用Ruby语言编写自己的插件。

（4）Vagrant常用命令（见表11-2）

表11-2　Vagrant常用命令

更多的命令可以查看官方文档，或者通过参数-h查看：

vagrant -h

（5）Vagrant的安装和使用

安装过程比较简单，MacOS、Windows、Debian、CentOS等平台都有安装包，直接到下载页面https://www.vagrantup.com/downloads.html 下载最新版本安装即可。提醒一下，在实际的测试过程中发现Vagrant和VirtualBox也存在版本依赖的情况，比如Vagrant 1.8.4和Virtualbox的5.1.X的版本会有冲突，出现“No usable default provider could be found for your system.”的问题。安装的时候不要下载和Virtualbox有冲突的版本就可以。

安装完成以后进入超级账本源码子目录devenv，执行如下命令就可以自动配置好开发环境了：

// 进入超级账本源码子目录devenv
cd $GOPATH/src/github.com/hyperledger/fabric/devenv/
// 启动Vagrant环境，自动配置开发环境。如果vagrant环境已经启动，则不需要执行
vagrant up
// 进入虚拟机环境
vagrant ssh

配置Vagrant环境的过程可能会比较慢，可能会有一些网络方面的问题，可以参考本章后面要介绍的编译镜像文件部分做修改。配置完成以后就安装好了Golang、Docker、Docker Compose等环境。准备好开发环境就可以进行源码编译了，参考本章后面编译镜像文件的内容。

2.基于Virtualbox的运行环境

基于Vagrant的运行环境最大的好处是能自动地配置开发环境，不过Vagrant也会存在和操作系统兼容性的问题，比如在Windows 7上很容易安装失败，最新版本中超级账本官方并不推荐采用Vagrant的方式。我们可以在虚拟机的基础上自行构建所需要的开发环境，这里我们以Virtualbox为例。

VirtualBox也支持Windows、MacOS、Linux、Solaris等多个平台，直接在官网https://www.virtualbox.org/wiki/Downloads 下载安装即可。这里简单地介绍一下在Virtualbox上安装Ubuntu 16.04的过程，先从国内的镜像源下载镜像文件：http://mirrors.163.com/ubuntu-releases/16.04/ubuntu-16.04.3-server-amd64.iso 。

在打开的VirtualBox控制台上点击新建虚拟机，在操作系统类型中选择Ubuntu，虚拟机的名称可以自定义，比如Ubuntu，如图11-2所示，然后按照默认选项点击下一步的按钮。

图11-2　在Virtualbox上新建虚拟机

当虚拟机新建完成以后，在虚拟机列表中选择创建的虚拟机，如图11-3所示。

图11-3　启动虚拟机

这个时候虚拟机是没有安装操作系统的，在启动过程中可以选择刚刚下载的iso文件，如图11-4所示。

图11-4　选择操作系统安装文件

在弹出的文件浏览器中选择文件，按默认步骤安装就可以了，如图11-5所示。

图11-5　选择操作系统安装文件

当操作系统安装完成以后就可以安装一些基础环境了，Docker和Docker Compose的安装请参考第2章的内容。

（1）安装Golang

进入Golang的官网下载页面http://golang.org/dl ，选择不同平台上的安装包安装即可，目前Vagrant里指定的版本是1.7.5，也可以选择最新稳定版。如果由于网络原因无法下载的话，可以选择国内的下载地址http://golangtc.com/download 。Ubuntu系统可以直接通过命令安装：

sudo apt-get install golang

直接通过源安装的版本是1.6.2，安装完成以后需要设置环境变量GOROOT和GOPATH，比如：

clarity@ubuntu:~/gopath$ cat ~/.bashrc
export GOROOT=/usr/local/go
export GOPATH=$HOME/gopath
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

（2）安装libtool

需要安装libtool，否则后面编译的时候会报错：'ltdl.h'file not found。

wget http://ftpmirror.gnu.org/libtool/libtool-2.4.6.tar.gz
tar -zxvf libtool-2.4.6.tar.gz
cd libtool-2.4.6
sudo apt-get install automake
./configure --prefix=/usr
sudo make && sudo make install

做了上述准备以后，就可以开始编译超级账本镜像文件了。

3.基于Docker的运行环境

超级账本在Docker官方镜像仓库Docker Hub维护了不同版本的镜像文件：https://hub.docker.com/u/hyperledger ，可以直接下载部署使用，详细使用方法参考第2章的内容。下面我们介绍源码编译的方法生成镜像。

上述所有的方法中，建议初次部署超级账本网络的时候使用基于Docker的运行环境，能对超级账本的运行流程有最直观的感受。但是Windows和Mac OS上的Docker都极不稳定，容易出现异常，深入研究和最终部署的时候建议采用基于Virtuabox或者申请云主机镜像搭建运行环境。

11.1.2　编译超级账本镜像文件

由于国内网络的原因，我们需要先做一些准备工作才能编译超级账本镜像文件。

1.编译超级账本镜像文件

下面是通过Github下载golang.org/x/tools库的方法，否则可能由于网络问题安装失败。

// 通过Github下载golang.org/x/tools库
sed -i 's/.*@mkdir -p $@\/bin $@\/obj.*/&\n\t@cd $(TOOLS_SRC)\/tools || (mkdir -p
$(TOOLS_SRC) \&\& cd $(TOOLS_SRC) \&\& git clone
https:\/\/github.com\/golang\/tools.git)/' Makefile
sed -i 's/.*@mkdir -p $@\/bin $@\/obj.*/&\n\t@$(eval TOOLS_DST = \/opt\/gotools\/
obj\/gopath\/src\/golang.org\/x)/' Makefile
sed -i 's/.*@mkdir -p $@\/bin $@\/obj.*/&\n\t$(eval TOOLS_SRC = $(dir $(abspath
$<))\/build\/gopath\/src\/golang.org\/x)/' Makefile
// 映射下载的目录到临时构建容器中
sed -i 's/.*-v $(abspath $@):\/opt\/gotools.*/&\n\t\t-u root -v $(TOOLS_
SRC):$(TOOLS_DST) \\/' Makefile
// 构建完成删除增加sudo权限
sed -i 's/rm -rf build /sudo rm -rf build /' Makefile

编译完成以后生成的临时目录build/docker/gotools/obj权限变成了root，在make clean需要sudo权限才能删除。

Hyperledger Fabric 1.0.0中Zookeeper的版本是3.4.9，已经不维护了，替换为最新的稳定版本3.4.10：

sed -i 's/zookeeper-3.4.9/zookeeper-3.4.10/'
images/zookeeper/Dockerfile.in

做好前面的准备工作后，超级账本源码编译就比较简单了，只需要在超级账本的源代码目录下执行如下命令，就可以自动编译出所有的镜像文件了。

# 下载fabric源代码
mkdir -p $GOPATH/src/github.com/hyperledger
cd $GOPATH/src/github.com/hyperledger
git clone https://github.com/hyperledger/fabric.git
cd fabric
git checkout -b v1.0.0 v1.0.0

# 编译镜像文件
make docker

下面是编译fabric-ca的镜像文件：

# 下载fabric-ca源代码
mkdir -p $GOPATH/src/github.com/hyperledger
cd $GOPATH/src/github.com/hyperledger
git clone https://github.com/hyperledger/fabric-ca.git
cd fabric-ca
git checkout -b v1.0.0 v1.0.0
# 编译镜像文件
make docker

2.超级账本镜像文件

编译生成的超级账本镜像文件，如表11-3所示。

表11-3　超级账本镜像文件

图11-6是超级账本的镜像文件的构建和运行关系图。

图11-6　镜像文件的构建和运行关系图

编译超级账本镜像用到很多的免费开源软件（Free and Open-source Software，FOSS），基于Ubuntu 16.04的镜像文件ubuntu/xenial64构建出镜像文件hyperledger/fabric-baseos，这是所有其他镜像文件的基础。镜像文件较小，包含的内容也只在ubuntu/xenial64基础上增加了软件下载工具wget和时区配置工具tzdata。Golang的链码是在hyperledger/fabric-baseos基础上包含通过hyperledger/fabric-ccenv编译出的二进制文件构建的链码容器，这样构建出的链码容器相对较小。镜像文件hyperledger/fabric-basejvm包含了OpenJDK 8的Java编译运行环境，Java的链码基于hyperledger/fabric-javaenv的运行环境，包含了Java的链码SDK的内容。

11.2　快速构建超级账本网络

我们基于fabric-samples里的BYFN（Build Your First Network）介绍超级账本的构建过程，首先是利用提供的脚本快速地构建网络，后面是详细的构建过程。

11.2.1　下载BYFN的代码

BYFN是包含在fabric-samples的first-network目录下的，先通过git下载源代码：

cd $GOPATH/src/github.com/hyperledger
git clone https://github.com/hyperledger/fabric-samples.git
cd fabric-samples/first-network

后面的操作默认都在此路径下进行。

11.2.2　BYFN脚本介绍

运行BYFN脚本需要已经安装好Docker基础环境，编译出镜像文件和系统工具cryptogen、configtxgen等。BYFN脚本的帮助说明如下：

claritys-MacBook-Pro:first-network clarity$ ./byfn.sh -h
// 用法：
byfn.sh -m up|down|restart|generate [-c <channel name>] [-t <timeout>]
byfn.sh -h|--help 显示帮助信息
-m <mode> - one of 'up', 'down', 'restart' or 'generate'
- 'up' - 使用docker-compose启动网络
- 'down' - 使用docker-compose停止网络
- 'restart' - 重启网络
- 'generate' - 生成证书和创世区块
-c <channel name> - 通道名称，默认是"mychannel"
-t <timeout> - 超时时间，默认是10000毫秒

// 下面是通用的流程，先生成证书和创世区块，再启动网络：
byfn.sh -m generate -c <channelname>
byfn.sh -m up -c <channelname>
byfn.sh -m down -c <channelname>

//默认选项：
byfn.sh -m generate
byfn.sh -m up
byfn.sh -m down

其中，'-c'参数指定通道名称，默认是mychannel，'-t'是连接的超时时间，默认是10秒。下面简要介绍其中的几个功能，如表11-4所示。

表11-4　BYFN脚本的命令选项

下面我们利用BYFN脚本构建出超级账本网络。

11.2.3　生成网络初始化配置

执行脚本./byfn.sh-m generate生成MSP证书和创世区块：

./byfn.sh -m generate

执行结果如下：

Generating certs and genesis block for with channel 'mychannel' and CLI timeout
of '10000'
Continue (y/n)? y
proceeding ...
/Users/clarity/Projects/bin/cryptogen

##########################################################
######### 用cryptogen 工具生成证书 #########
##########################################################
org1.example.com
org2.example.com

/Users/clarity/Projects/bin/configtxgen
##########################################################
######### 生成排序服务的创世区块 ##############
##########################################################
2017-08-09 14:03:16.410 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 14:03:16.437 CST [common/configtx/tool] doOutputBlock -> INFO 002 Generating
genesis block
2017-08-09 14:03:16.440 CST [common/configtx/tool] doOutputBlock -> INFO 003 Writing
genesis block

#################################################################
######### 生成通道配置交易 'channel.tx' #########
#################################################################
2017-08-09 14:03:16.460 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 14:03:16.464 CST [common/configtx/tool]
doOutputChannelCreateTx -> INFO 002 Generating new channel configtx
2017-08-09 14:03:16.464 CST [common/configtx/tool]
doOutputChannelCreateTx -> INFO 003 Writing new channel tx

#################################################################
######### 生成Org1MSP锚节点配置 #########
#################################################################
2017-08-09 14:03:16.488 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 14:03:16.493 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
002 Generating anchor peer update
2017-08-09 14:03:16.493 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
003 Writing anchor peer update

#################################################################
######### 生成Org2MSP锚节点配置 #########
#################################################################
2017-08-09 14:03:16.525 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 14:03:16.529 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
002 Generating anchor peer update
2017-08-09 14:03:16.531 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
003 Writing anchor peer update

执行成功以后生成的Peer节点和排序服务节点的MSP证书文件在目录crypto-config下，包含4个Peer节点（分成两个组织，即org1.example.com和org2.example.com）和1个排序服务节点，排序后端默认使用SOLO，详细的目录结构参考第8章的内容。

生成的创世区块在目录channel-artifacts下：

claritys-MacBook-Pro:first-network clarity$ tree channel-artifacts
channel-artifacts
├── Org1MSPanchors.tx
├── Org2MSPanchors.tx
├── channel.tx
└── genesis.block

0 directories, 4 files

其中几个参数说明如下。

·genesis.block：排序服务创世区块；

·channel.tx：通道配置创世区块；

·Org1MSPanchors.tx：Org1锚节点的配置；

·Org2MSPanchors.tx：Org2锚节点的配置。

11.2.4　启动超级账本网络

执行脚本./byfn.sh-m up会根据Docker Compose配置文件docker-compose-cli.yaml启动超级账本网络，还会执行scripts/script.sh脚本安装和实例化链码，并且执行简单链码调用和查询操作。

./byfn.sh -m up

输出的结果如下：

localhost:first-network clarity$ ./byfn.sh -m up
Starting with channel 'mychannel' and CLI timeout of '10000'
Continue (y/n)? y
proceeding ...
/Users/clarity/Projects/bin/cryptogen

##########################################################
########## 使用cryptogen工具生成密钥和证书 #########
##########################################################
org1.example.com
org2.example.com

/Users/clarity/Projects/bin/configtxgen
##########################################################
######### 生成排序服务启动的创世区块 ##############
##########################################################
2017-08-09 19:53:51.192 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 19:53:51.225 CST [common/configtx/tool] doOutputBlock -> INFO 002 Generating
genesis block
2017-08-09 19:53:51.227 CST [common/configtx/tool] doOutputBlock -> INFO 003 Writing
genesis block

#################################################################
######### 生成通道配置交易文件'channel.tx' #########
#################################################################
2017-08-09 19:53:51.253 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 19:53:51.256 CST [common/configtx/tool] doOutputChannelCreateTx -> INFO
002 Generating new channel configtx
2017-08-09 19:53:51.256 CST [common/configtx/tool] doOutputChannelCreateTx -> INFO
003 Writing new channel tx

#################################################################
########## 生成组织Org1MSP的锚节点配置 ##########
#################################################################
2017-08-09 19:53:51.280 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 19:53:51.284 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
002 Generating anchor peer update
2017-08-09 19:53:51.285 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
003 Writing anchor peer update

#################################################################
########## 生成组织Org2MSP的锚节点配置 ##########
#################################################################
2017-08-09 19:53:51.303 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 19:53:51.306 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
002 Generating anchor peer update
2017-08-09 19:53:51.306 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
003 Writing anchor peer update

Creating network "net_byfn" with the default driver
Creating orderer.example.com ...
Creating peer1.org2.example.com ...
Creating peer0.org2.example.com ...
Creating peer0.org1.example.com ...
Creating peer1.org1.example.com ...
Creating peer1.org1.example.com
Creating peer0.org2.example.com
Creating orderer.example.com
Creating peer1.org2.example.com
Creating peer0.org1.example.com ... done
Creating cli ...
Creating cli ... done

看到上面的信息，表明超级账本网络已经启动完毕。在启动cli容器的时候会自动执行scripts/script.sh脚本，包括创建通道、Peer节点加入通道、更新通道锚节点信息、安装链码、实例化链码、链码调用和链码查询等，每一步操作都会显示对应的日志信息。下面是创建通道的结果：

____ _____ _ ____ _____
/ ___| |_ _| / \ | _ \ |_ _|
\___ \ | | / _ \ | |_) | | |
___) | | | / ___ \ | _ < | |
|____/ |_| /_/ \_\ |_| \_\ |_|
Build your first network (BYFN) end-to-end test

Channel name : mychannel
Creating channel...
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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org1MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
...
============= Channel "mychannel" is created successfully ===============

4个Peer节点依次加入通道：

Having all peers join the channel...
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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org1MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
...
================ PEER0 joined on the channel "mychannel" ================

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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org1MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer1.org1.example.com:7051
...
=============== PEER1 joined on the channel "mychannel" ==============

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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org2MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer0.org2.example.com:7051
...
=============== PEER2 joined on the channel "mychannel" ===============

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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org2MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer1.org2.example.com:7051
...
=============== PEER3 joined on the channel "mychannel" ================

更新组织的锚节点：

Updating anchor peers for org1...
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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org1MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer0.org1.example.com:7051
...
=== Anchor peers for org "Org1MSP" on "mychannel" is updated successfully ===

Updating anchor peers for org2...
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_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
CORE_PEER_LOCALMSPID=Org2MSP
CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/
peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
CORE_PEER_TLS_ENABLED=true
CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/
crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
CORE_PEER_ID=cli
CORE_LOGGING_LEVEL=DEBUG
CORE_PEER_ADDRESS=peer0.org2.example.com:7051
...
=== Anchor peers for org "Org2MSP" on "mychannel" is updated successfully ===

在Peer节点peer0.org1.example.com、peer0.org2.example.com上安装链码：

Installing chaincode on org1/peer0...
...
========== Chaincode is installed on remote peer PEER0 ===========

Install chaincode on org2/peer2...
...
=========== Chaincode is installed on remote peer PEER2 ==========

在Peer节点peer0.org2.example.com上实例化链码：

Instantiating chaincode on org2/peer2...
...
====== Chaincode Instantiation on PEER2 on channel 'mychannel' is successful ======

调用前在Peer节点peer0.org1.example.com上执行链码查询a的值：

Querying chaincode on org1/peer0...
...
Query Result: 100
...
====== Query on PEER0 on channel 'mychannel' is successful ======

在Peer节点peer0.org1.example.com上执行链码调用，从a转移10到b：

Sending invoke transaction on org1/peer0...
...
2017-09-10 02:56:13.627 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 00a
Chaincode invoke successful. result: status:200
...
====== Invoke transaction on PEER0 on channel 'mychannel' is successful ======

在Peer节点peer1.org2.example.com上安装链码并查询a的值：

Installing chaincode on org2/peer3...
...
============ Chaincode is installed on remote peer PEER3 ===========

Querying chaincode on org2/peer3...
====== Querying on PEER3 on channel 'mychannel'... ======
...
Query Result: 90
...
====== Query on PEER3 on channel 'mychannel' is successful ======

可以看到，Peer节点peer1.org2.example.com并没有参与背书的过程，在转移调用完成以后再安装链码，也能查询到最新的结果。到此，脚本自动执行的过程就结束了，超级账本网络还在继续运行：

========= All GOOD, BYFN execution completed ===========
_____ _ _ ____
| ____| | \ | | | _ \
| _| | \| | | | | |
| |___ | |\ | | |_| |
|_____| |_| \_| |____/

11.2.5　关闭超级账本网络

测试完成以后，调用脚本关闭超级账本网络：

./byfn.sh -m down

显示如下结果：

Stopping with channel 'mychannel' and CLI timeout of '10000'
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.
Stopping cli ... done
Stopping peer0.org1.example.com ... done
…
Removing cli ... done
Removing peer0.org1.example.com ... done
…
Removing network net_byfn
d1866048b1d5
b091ac28a1db
2a840ddfb530
Untagged: dev-peer1.org2.example.com-mycc-1.0:latest
Deleted: sha256:fd58f0b0679c2509acb668400c7b041d7d3a266f454e56bb1b49ff4484d30185
…

以上日志显示已经关闭Peer节点、排序服务节点容器和链码容器，删除自动生成的链码镜像文件。执行docker ps-a可以看到容器已经全部停止，执行docker images可以看到已经没有了dev-peer1.org2.example.com-mycc-1.0等容器镜像。

11.3　逐步建立超级账本网络

下面我们手工逐步建立超级账本网络，理解了后面的操作步骤，就可以根据需求自行定制和部署超级账本网络了。

11.3.1　生成MSP证书

使用cryptogen工具生成证书。MSP证书是超级账本网络实体的身份标识，实体在通信和交易时使用证书进行签名和验证。生成证书需要crypto-config.yaml配置文件，详细的文件解析参考附录B的内容。这个文件定义了组织结构，据此可以为组织和其内的成员生成数字证书和签名密钥：

localhost:first-network clarity$ cryptogen generate
--config=./crypto-config.yaml
org1.example.com
org2.example.com

生成的MSP目录结构请参考第8章的内容。

11.3.2　生成排序服务创世区块

添加环境变量指定configtx.yaml文件的位置，生成创世区块：

export FABRIC_CFG_PATH=$PWD
configtxgen -profile TwoOrgsOrdererGenesis
-outputBlock ./channel-artifacts/genesis.block

执行结果如下：

2017-08-09 17:29:18.732 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 17:29:18.761 CST [common/configtx/tool] doOutputBlock -> INFO 002 Generating
genesis block
2017-08-09 17:29:18.763 CST [common/configtx/tool] doOutputBlock -> INFO 003 Writing
genesis block

执行成功以后会在channel-artifacts目录下生成创世区块geness.block，区块内容包含了联盟和组织信息，还包含通道的访问控制策略信息，可以通过configtxlator或者configtxgen工具查看详细内容。

11.3.3　生成通道配置创世区块

设置环境变量通道名称，同样利用configtxgen生成通道配置：

export CHANNEL_NAME=mychannel
configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./channel-artifacts/
channel.tx -channelID $CHANNEL_NAME

执行结果如下：

2017-08-09 17:36:09.651 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 17:36:09.654 CST [common/configtx/tool] doOutputChannelCreateTx -> INFO
002 Generating new channel configtx
2017-08-09 17:36:09.654 CST [common/configtx/tool] doOutputChannelCreateTx -> INFO
003 Writing new channel tx

执行成功以后会在channel-artifacts目录下生成通道配置channel.tx，可以通过configtxlator或者configtxgen工具查看详细内容。

11.3.4　定义组织锚节点

定义Org1和Org2的两个锚节点：

export CHANNEL_NAME=mychannel
// 组织Org1的锚节点
configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/
Org1MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org1MSP
// 组织Org2的锚节点
configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./channel-artifacts/
Org2MSPanchors.tx -channelID $CHANNEL_NAME -asOrg Org2MSP

执行结果如下：

2017-08-09 18:28:36.129 CST [common/configtx/tool] main -> INFO 001 Loading configuration
2017-08-09 18:28:36.132 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
002 Generating anchor peer update
2017-08-09 18:28:36.132 CST [common/configtx/tool] doOutputAnchorPeersUpdate -> INFO
003 Writing anchor peer update

执行成功以后会在channel-artifacts目录下生成锚节点配置Org1MSPanchors.tx和Org2MSPanchors.tx，可以通过configtxlator或者configtxgen工具查看详细内容。

11.3.5　启动超级账本网络

使用docker-compose启动超级账本网络，需要的配置文件是docker-compose-cli.yaml，定义了1个排序服务节点、4个Peer节点。还有一个命令行容器cli，默认通过和peer0.org1.example.com通信实现与超级账本网络交互，进行链码部署等操作，切换环境变量也会和其他Peer节点进行通信。这里还会用到之前已经编译或者下载成功的Hyperledger Fabric v1.0.0镜像文件，以及上面刚刚生成的创世区块genesis.block用于排序服务节点的启动。

由于启动cli容器的时候默认会执行./scripts/script.sh脚本，所以需要先屏蔽启动脚本，再启动我们的网络：

// 屏蔽./scripts/script.sh启动脚本
sed -i '' 's/command/#command/' docker-compose-cli.yaml
// 设置通道名称
CHANNEL_NAME=$CHANNEL_NAME
// 启动网络
docker-compose -f docker-compose-cli.yaml up -d

执行成功以后调用docker ps-a查看启动的容器如下（简化的输出信息）：

localhost:first-network clarity$ docker ps -a
CONTAINER ID IMAGE NAMES
d294b6b493f9 hyperledger/fabric-tools cli
86e686846262 hyperledger/fabric-peer peer1.org2.example.com
7d1040f33537 hyperledger/fabric-peer peer0.org2.example.com
78c4daafb95f hyperledger/fabric-peer peer1.org1.example.com
f902601d3b89 hyperledger/fabric-peer peer0.org1.example.com
08a04df7cf36 hyperledger/fabric-orderer orderer.example.com

11.3.6　创建并加入通道

网络启动以后，需要先创建通道，进入cli容器进行操作：

// 进入cli容器
docker exec -it cli bash
// 设置通道名称的环境变量
export CHANNEL_NAME=mychannel
// 创建通道
peer channel create -o orderer.example.com:7050 -c $CHANNEL_NAME -f ./channel-
artifacts/channel.tx --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.
com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/
orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem

执行成功会在cli容器的当前路径下生成以通道名称命名的mychannel.block，这个是通道配置的创世区块，里面包含通道配置信息，加入通道操作时需要使用。

1.Peer节点peer0.org1.example.com加入通道

现在把Peer节点peer0.org1.example.com加入通道：

peer channel join -b mychannel.block

执行结果中包含“Peer joined the channel”，可见加入通道成功：

2017-08-09 13:07:16.816 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2017-08-09 13:07:16.816 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 13:07:16.824 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and
orderer connections initialized
2017-08-09 13:07:16.824 UTC [msp/identity] Sign -> DEBU 004 Sign: plaintext: 0A8
A070A5C08011A0C0884C996D00510...8DBFBB0981111A080A000A000A000A00
2017-08-09 13:07:16.824 UTC [msp/identity] Sign -> DEBU 005 Sign: digest: E10EC5
D95EE07F8E8793EF652E886F6398F30D9C510BB4420C6AEC0DE31612AD
2017-08-09 13:07:16.859 UTC [channelCmd] executeJoin -> INFO 006 Peer joined the
channel!
2017-08-09 13:07:16.859 UTC [main] main -> INFO 007 Exiting.....

查询当前Peer节点加入的通道列表：

peer channel list

日志中"Channels peers has joined to"后面显示的mychannel就是加入的通道：

2017-08-09 13:14:12.230 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local MSP
2017-08-09 13:14:12.230 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 13:14:12.237 UTC [channelCmd] InitCmdFactory -> INFO 003 Endorser and
orderer connections initialized
2017-08-09 13:14:12.237 UTC [msp/identity] Sign -> DEBU 004 Sign: plaintext: 0A8
9070A5B08031A0B08A4CC96D00510...631A0D0A0B4765744368616E6E656C73
2017-08-09 13:14:12.238 UTC [msp/identity] Sign -> DEBU 005 Sign: digest: 7E0B57
DC621486091E6DB68F41045957A83245292326472FACC493D5B077C3DE
2017-08-09 13:14:12.241 UTC [channelCmd] list -> INFO 006 Channels peers has
joined to:
2017-08-09 13:14:12.241 UTC [channelCmd] list -> INFO 007 mychannel
2017-08-09 13:14:12.241 UTC [main] main -> INFO 008 Exiting.....

2.Peer节点peer1.org1.example.com加入通道

需要依次把其他Peer节点也加入通道，在cli容器中切换环境变量，设置连接的Peer节点和MSP等信息，再加入通道：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer1.org1.example.com:7051
// 设置MSP信息
export CORE_PEER_LOCALMSPID="Org1MSP"
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/org1.example.com/users/Admin@org1.example.com/msp
// 把peer1.org1.example.com加入通道
peer channel join -b mychannel.block

可以通过peer channel list查询到Peer节点peer1.org1.example.com已经成功加入通道mychannel。

3.Peer节点peer0.org2.example.com加入通道

需要先设置连接的Peer节点和MSP等信息，再加入通道：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer0.org2.example.com:7051
// 设置MSP信息
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
// 把peer1.org1.example.com加入通道
peer channel join -b mychannel.block

可以通过peer channel list查询到Peer节点peer0.org2.example.com已经成功加入通道mychannel。

4.Peer节点peer1.org2.example.com加入通道

需要先设置连接的Peer节点和MSP等信息，再加入通道：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer1.org2.example.com:7051
// 设置MSP信息
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/peer1.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
// 把peer1.org1.example.com加入通道
peer channel join -b mychannel.block

可以通过peer channel list查询到Peer节点peer1.org2.example.com已经成功加入通道mychannel。

11.3.7　安装和实例化链码

应用程序通过链码执行智能合约的功能，需要先在每个Peer节点上安装链码，然后在通道上实例化链码。

1.Peer节点peer0.org1.example.com安装链码

在Peer节点peer0.org1.example.com上安装链码，先设置环境变量，再安装链码chaincode_example02：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051
// 设置MSP信息
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
// 安装链码
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02

执行结果中包含“Installed remotely response”说明执行成功：

2017-08-09 13:53:41.941 UTC [golang-platform] getCodeFromFS -> DEBU 005
getCodeFromFS
github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02
…
2017-08-09 13:53:42.123 UTC [chaincodeCmd] install -> DEBU 00d Installed remotely
response:<status:200 payload:"OK" >
2017-08-09 13:53:42.123 UTC [main] main -> INFO 00e Exiting.....

重新打开一个终端，进入Peer节点peer0.org1.example.com容器确认是否有安装的链码文件：

localhost:fabric clarity$ docker exec peer0.org1.example.com ls /var/hyperledger/
production/chaincodes

如果输出结果中包含mycc.1.0，就说明已经安装成功。

2.Peer节点peer1.org1.example.com安装链码

在Peer节点peer1.org1.example.com上安装链码，先设置环境变量，再安装链码chaincode_example02：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer1.org1.example.com:7051
// 设置MSP信息
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/peer1.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
// 安装链码
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02

安装成功返回结果中会包含“Installed remotely response：”，再进入Peer节点容器确认是否有安装的链码文件：

localhost:fabric clarity$ docker exec peer1.org1.example.com ls /var/hyperledger/
production/chaincodes

如果输出结果中包含mycc.1.0，就说明已经安装成功。

3.Peer节点peer0.org2.example.com安装链码

在Peer节点peer0.org2.example.com上安装链码，先设置环境变量，再安装链码chaincode_example02：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer0.org2.example.com:7051
//设置MSP信息
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
// 安装链码
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02

安装成功返回结果中会包含“Installed remotely response：”，再进入Peer节点容器确认是否有安装的链码文件：

localhost:fabric clarity$ docker exec peer0.org2.example.com ls /var/hyperledger/
production/chaincodes

如果输出结果中包含mycc.1.0，就说明已经安装成功。

4.Peer节点peer1.org2.example.com安装链码

在Peer节点peer1.org2.example.com上安装链码，先设置环境变量，再安装链码chaincode_example02：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer1.org2.example.com:7051
// 设置MSP信息
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/peer1.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
// 安装链码
peer chaincode install -n mycc -v 1.0 -p github.com/hyperledger/fabric/examples/
chaincode/go/chaincode_example02

安装成功返回结果中会包含“Installed remotely response：”，再进入Peer节点容器确认是否有安装的链码文件：

localhost:fabric clarity$ docker exec peer1.org2.example.com ls /var/hyperledger/
production/chaincodes

如果输出结果中包含mycc.1.0，就说明已经安装成功。

5.实例化链码

实例化链码只需要执行一次，任意一个Peer节点都可以处理实例化的请求，同样是在cli容器中执行如下操作：

peer chaincode instantiate -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --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 "OR('Org1MSP.member','Org2MSP.member')"

其中，-c参数确定了账户初始化情况为a=100和b=200，-P参数指定了背书策略，链码的每个交易需要由Org1MSP或Org2MSP两个组织中的任意一个成员背书签名，才能通过背书策略，继续进入后续交易流程。

上面的命令执行完成以后没有错误提示就是成功的，会启动链码的容器dev-peer1.org2.example.com-mycc-1.0：

localhost:first-network clarity$ docker ps -a
CONTAINER ID IMAGE NAMES
f851c41073b8 dev-peer1.org2.example.com-mycc-1.0 dev-peer1.org2.example.com-
mycc-1.0
d294b6b493f9 hyperledger/fabric-tools cli
86e686846262 hyperledger/fabric-peer peer1.org2.example.com
7d1040f33537 hyperledger/fabric-peer peer0.org2.example.com
78c4daafb95f hyperledger/fabric-peer peer1.org1.example.com
f902601d3b89 hyperledger/fabric-peer peer0.org1.example.com
08a04df7cf36 hyperledger/fabric-orderer orderer.example.com

11.3.8　执行链码查询

先查看当前的环境变量：

env | grep CORE_PEER_ADDRESS

输出结果是：

CORE_PEER_ADDRESS=peer1.org2.example.com:7051

表明cli容器目前是从Peer节点peer1.org2.example.com查询的：

peer chaincode query -C $CHANNEL_NAME -nmycc -c '{"Args":["query","a"]}'
peer chaincode query -C $CHANNEL_NAME -nmycc -c '{"Args":["query","b"]}'

可以看到a和b的账户分别返回了之前初始化的数值。

我们切换环境变量然后通过peer0.org2.example.com做同样的查询工作：

// 设置连接的Peer节点
export CORE_PEER_ADDRESS=peer0.org2.example.com:7051
// 设置MSP信息
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
// 查询a和b的值
peer chaincode query -C $CHANNEL_NAME -nmycc -c '{"Args":["query","a"]}'
peer chaincode query -C $CHANNEL_NAME -nmycc -c '{"Args":["query","b"]}'

结果和从Peer节点peer1.org2.example.com查询是一样的，由于Peer节点peer0.org2.example.com的链码之前没有调用过，所以在做查询操作前会自行启动dev-peer0.org2.example.com-mycc-1.0的链码容器，然后再完成查询：

localhost:first-network clarity$ docker ps -a
CONTAINER ID IMAGE NAMES
f851c41073b8 dev-peer1.org2.example.com-mycc-1.0 dev-peer1.org2.example.com-
mycc-1.0
8537e573f7a5 dev-peer0.org2.example.com-mycc-1.0 dev-peer0.org2.example.com-
mycc-1.0
d294b6b493f9 hyperledger/fabric-tools cli
86e686846262 hyperledger/fabric-peer peer1.org2.example.com
7d1040f33537 hyperledger/fabric-peer peer0.org2.example.com
78c4daafb95f hyperledger/fabric-peer peer1.org1.example.com
f902601d3b89 hyperledger/fabric-peer peer0.org1.example.com
08a04df7cf36 hyperledger/fabric-orderer orderer.example.com

11.3.9　执行链码调用

链码调用也可以在任意一个节点上执行，下面的操作完成从a账户转账10到b账户：

peer chaincode invoke -oorderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED
--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 -c'{"Args":["invoke","a","b","10"]}'

执行成功的结果包含“Chaincode invoke successful.result：status：200”：

2017-08-09 15:25:32.202 UTC [msp] GetLocalMSP -> DEBU 001 Returning existing local
MSP
2017-08-09 15:25:32.202 UTC [msp] GetDefaultSigningIdentity -> DEBU 002 Obtaining
default signing identity
2017-08-09 15:25:32.208 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 003
Using default escc
2017-08-09 15:25:32.208 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 004
Using default vscc
2017-08-09 15:25:32.209 UTC [msp/identity] Sign -> DEBU 005 Sign: plaintext: 0A9
4070A6608031A0B08EC8997D00510...696E766F6B650A01610A01620A023130
2017-08-09 15:25:32.209 UTC [msp/identity] Sign -> DEBU 006 Sign: digest: E5807B
923EAB14944DFCE2268413765F629D489159E345F919C8171E1704C3BA
2017-08-09 15:25:32.224 UTC [msp/identity] Sign -> DEBU 007 Sign: plaintext: 0A9
4070A6608031A0B08EC8997D00510...0BF1F0306F009754B827D0419FF8B29F
2017-08-09 15:25:32.224 UTC [msp/identity] Sign -> DEBU 008 Sign: digest: 5C8256
B24D92447294F7AA0012412541712E2BC31E8C7F80AA2AEB0B670EB3CC
2017-08-09 15:25:32.229 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> DEBU 009
ESCC invoke result: version:1 response:<status:200 message:"OK" > payload:"\n \250.\245\310\004\ns\306p\334/\331\306x\2244\347\020\333|\352
\034\323\267M\2719\265\306\n\304q\022Y\nE\022\024\n\004lscc\022\014\n\n\n\004mycc\022\002\010\001\022-\n\004mycc\022%\n\007\n\001a\022\002\010\001\n\
007\n\001b\022\002\010\001\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\007Org2MSP\022\374\005-----BEGIN
-----\nMIICGDCCAb+gAwIBAgIQBhvnD4KTVCkvkyVz4YIMqDAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMi5leGFtcGxlLmNvbTAeFw0xNzExMTAwOTExMDlaFw0yNzExMDgwOTExMDla\nMFsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMR8wHQYDVQQDExZwZWVyMC5vcmcyLmV4YW1wbGUuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaNjBLY2Axe5FqwfBllIKT3KSfYRCDJUm\n7IUWS0qOzmwjbWhAh5+M3rGdE90lIyLgGlVY5T6Rgm5GdAladW51q6NNMEswDgYD\nVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwKwYDVR0jBCQwIoAgjdtfuR1T/7Bh\nYIVSk87JB7i9hBMYVxs5xrTqpmGI+iwwCgYIKoZIzj0EAwIDRwAwRAIgUWz5CSZc\nUjuOf9uzFx5Y2uCQoOZi23RPIyrSD5MgPWoCIHpD6f5QVVYaU0yC1AadOiA81MJf\n+yb/gNSK2jelSiIC\n-----END -----\n"
signature:"0D\002
h\212\000\034-\301\223\230\265\273\357\255\226\344\206\301\210\302T\341\257_\316<\310'\031U\177<;B\002
<\010a\273\025[<\232r\254\275\225\234\002\024\217\013\361\3600o\000\227T\270'\320A\237\370\262\237" >
2017-08-09 15:25:32.230 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 00a Chaincode invoke successful. result: status:200
2017-08-09 15:25:32.230 UTC [main] main -> INFO 00b Exiting.....

同样可以在任意一个Peer节点上查询转账后的结果。

11.4　本章小结

本章详细介绍了搭建超级账本网络的过程，内容由浅入深循序渐进，初学者能够快速体验超级账本的基本功能。再结合前面章节对内部实现原理的介绍，读者能够在详细的步骤中了解每一个步骤的作用，逐步深入地了解超级账本的内在魅力。

第12章　超级账本的应用开发实例

前面章节介绍了如何利用Hyperledger Fabric Client SDK和链码开发基于Fabric网络的区块链应用。本章会从应用开发的角度出发，用一个票据背书的例子来介绍如何进行一个完整的区块链应用开发。

12.1　票据背书场景介绍

这里仅讨论狭义的票据，指出票人依法签发的由自己或指示他人无条件支付一定金额给收款人或持票人的有价证券，分为汇票、本票和支票等。它们的共同特点是，在票据规定的期限内，持票人或收款人可向出票人或指定付款人无条件地支取确定金额的货币；它们都属于反映一定债权债务关系的、可流通的、代表一定数量货币请求权的有价证券。票据是在货币或商品流动中为体现债权和债务的发生、转移以及偿付而用的一种信用工具，可用作贸易中的支付结算和企业短期融资。票据的特征和意义如下。

1）票据业务支持实体经济发展：票据承兑环节能为实体的企业支付、结算提供便利，满足企业间短期资金支付的需要。以企业间的背书转让和票据贴现为代表的交易方式能为实体经济（特别是中小企业）提供便捷的融资渠道和低成本资金。

2）票据业务有助于推动货币市场发展：票据为金融机构之间的转贴现业务加快了短期资金的融通和调剂，已经成为银行等金融机构一项重要的资产业务。同时，以央行的再贴现、回购为代表的货币政策工具，使得票据在货币政策传导、增强货币政策实施效果、促进信贷机构调整、引导扩大中小企业融资范围等方面发挥了重要的作用。

3）票据业务有助于丰富金融市场产品：由于票据兼有的支付、资金、信贷、资产等多重属性，加上持票机构的多样化和跨领域流通，使得以票据为载体的衍生产品成为连接货币市场和资本市场的重要探索。

12.1.1　票据关系人

票据的主要关系人如表12-1所示。

表12-1　票据主要的关系人

票据在整个生命周期中并不一定能涉及上表中所有的关系人，最重要的几个关系人是出票人、付款人、收款人、承兑人、背书人等。

12.1.2　票据行为分类

票据行为有广义和狭义两种。广义的票据行为是指以发生、变更或消灭票据的权利义务关系为目的法律行为，包括出票、背书、涂改、禁止背书、付款、保证、承兑、参加承兑、划线、保付等。狭义的票据行为是票据当事人以负担票据债务为目的的法律行为，包括出票、背书、承兑、参加承兑、保证、保付等。

1）出票。出票是指出票人依照法定款式做成票据并交付于受款人的行为。它包括“做成”和“交付”两种行为。所谓“做成”就是出票人按照法定款式制作票据，在票据上记载法定内容并签名。由于现在各种票据都由一定机关印制，因此所谓“做成”只是填写有关内容和签名而已。所谓“交付”是指根据出票人本人的意愿将其交给受款人的行为，不是出于出票人本人意愿的行为（如偷窃票据）不能称作“交付”，因此也不能称作出票行为。

2）背书。背书是指持票人转让票据权利与他人。票据的特点在于其流通。票据转让的主要方法是背书，当然，除此之外，还有单纯交付。背书转让是持票人的票据行为，只有持票人才能进行票据的背书。背书是转让票据权利的行为，票据一经背书转让，票据上的权利也随之转让给被背书人。

3）承兑。承兑是指汇票的付款人承诺负担票据债务的行为。承兑为汇票所独有。汇票的发票人和付款人之间是一种委托关系，发票人签发汇票，并不等于付款人就一定付款，持票人为确定汇票到期时能得到付款，在汇票到期前向付款人进行承兑提示。如果付款人签字承兑，那么他就对汇票的到期付款承担责任，否则持票人有权对其提起诉讼。

4）参加承兑。参加承兑是指票据的预备付款人或第三人为了特定票据债务人的利益，代替承兑人进行承兑，以阻止持票人于汇票到期日前行使追索权的一种票据行为。它一般是在汇票得不到承兑、付款人或承兑人死亡、逃亡或其他原因无法承兑、付款人或承兑人被宣告破产的情况下发生。

6）保证。保证是指除票据债务人以外的人为担保票据债务的履行、以负担同一内容的票据债务为目的的一种附属票据行为。票据保证的目的是担保其他票据债务的履行，适用于汇票和本票，不适用于支票。

7）保付。保付是指支票的付款人向持票人承诺负绝对付款责任的一种附属票据行为。保付是支票付款人的一种票据行为。支票一旦经付款人保付，在支票上注明“照付”或“保付”字样，并经签名后，付款人便负绝对付款责任，不论发票人在付款人处是否有资金，也不论持票人在法定提示期间是否有提示，或者即使发票人撤回付款委托，付款人均须按规定付款。

8）贴现。贴现指银行承兑汇票的持票人在汇票到期日前，为了取得资金，贴付一定利息将票据权利转让给银行的票据行为，是持票人向银行融通资金的一种方式。

9）转贴现。转贴现指商业银行在资金临时不足时，将已经贴现但仍未到期的票据，交给其他商业银行或贴现机构给予贴现，以取得资金融通。

10）再贴现。再贴现指中央银行通过买进商业银行持有的已贴现但尚未到期的商业汇票，向商业银行提供融资支持的行为。

在具体操作时，票据行为表现为票据当事人把行为的意思按照法定的方式记载在票据上，并由行为人签章后将票据交付。它包括三方面内容，即记载、签章和交付。

1）记载，通俗地讲，就是票据当事人在票据上写明所要记载的内容，如签发票据时应写明票据的种类、金额、无条件支付命令、签发票据日期以及其他需要明确的内容，承兑汇票时写上“承兑”字样，保证时应写上“保证”或“担保”字样。

2）签章，指签名、盖章或签名加盖章，它表明行为人对其行为承担责任。自然人签章是指在票据上亲自书写其姓名或加盖其私章。法人和其他使用票据单位的签章为该法人或者该单位的盖章加其法定代表人或其授权的代理人的签章。按照《票据法》规定，在票据上的签名应当为该当事人的本名，而不能用笔名、艺名等来代替。

3）交付，是指票据行为人应将票据交付给执票人。票据行为人在票据上进行记载，并进行签章后，票据还不能发生法律效力，只有把票据交付给了对方，票据才能发生法律效力。

12.1.3　基于区块链技术的数字票据

票据现有的形式有纸质票据和电子票据。纸质票据是传统的票据形式，需要在票据上签字或者加盖有效印章才能生效。电子票据是基于央行牵头开发完成的电子商业汇票系统（ECDS），银行或者企业通过直连或者网银接入，所有的票据承兑、交易等都需要通过ECDS才能完成，是典型的中心化系统。纸质票据和电子票据都有一些尚未解决的痛点。

·票据真实性问题：票据的贸易背景信息可能并不真实存在或者存在偏差，票据信息也容易被克隆和伪造，市场中存在的假票很难识别。

·票据安全性问题：纸质票据在携带过程有遗失或者损坏的风险，也存在打款和背书不同步的情况。

·票据信用风险问题：可能存在汇票到期后承兑人未及时将相关款项划入持票人账户的情况。

·票据处理效率问题：在途时间会造成资金结算延后，监管和审计成本也会很高。

·票据违规交易问题：票据交易主体或者中介机构可能存在一票多卖、清单交易、出租账户等违法违规行为。

基于区块链技术的数字票据能较好地解决以上问题，如图12-1所示。

图12-1　基于区块链技术的数字票据业务

借助区块链技术，可以在实现原有的票据业务的基础上解决现有的一些痛点。

（1）在不依赖可信第三方平台的基础上实现票据信息的真实性

纸质票据和电子票据的交易双方都依赖可信的第三方平台或者票据实物验证票据的真伪。但是在实际的操作过程中，票据的真实信息是很难确认的。超级账本的成员管理利用底层的安全机制确保上链信息的真实性，基于记录到分布式共享账本中的票据信息，杜绝假票、克隆票、变造票等伪假票据。

（2）分布式共享账本避免违规交易

纸质票据中的一票多卖和电子票据中打款背书不同步等违规交易存在的原因是信息的不同步。基于区块链技术的数字票据利用分布式共享账本记录的信息具有不可篡改性，一旦交易完成并记录到账本中，就不会存在纸质票据和电子票据中的数据丢失和信息不同步的问题，交易双方都没有机会进行违规交易。

（3）智能合约实现票据业务自动处理

智能合约功能可以实现复杂的业务逻辑，比如业务操作前可以基于不可篡改的账本信息搜集和评估参与者的信用，拒绝信用评级低的交易方提交的请求，业务操作后可以自动实现转账等功能，解决不同操作导致的信息不同步问题。

（4）数字票据提升票据处理效率

分布式共享账本在区块链网络中有多份数据拷贝，交易结束后就完成了对账，利用本地的账本能快速检索信息，做更多的业务处理，提升票据处理的效率。

（5）区块链技术提高票据安全性

票据安全性包括物理安全、数据安全、网络安全等多个方面。纸质票据在携带过程有遗失或者损坏的风险，属于物理安全范畴。纸质票据和电子票据都可能存在数据被篡改的可能性，属于数据安全范畴。传输过程中被中间人攻击等属于网络安全范畴。基于区块链技术的数字票据综合了多种底层技术，分布式账本解决了物理安全和数据安全的问题，数字签名和安全传输解决网络安全的问题。

12.2　票据背书需求分析

本章旨在通过案例讲述如何基于超级账本开发一个简单的区块链应用，票据背书的应用开发实例会对票据的应用场景进行简化，我们实现的业务逻辑包括票据发布、票据背书、票据签收、票据拒收、票据查询等操作，实际的票据业务需要根据实际需求做调整。

1.票据发布

票据发布操作生成一个票据，包括如下5类信息。

（1）票据基本信息

·票据号码

·票据金额

·票据种类

·票据出票日期

·票据到期日期

（2）出票人信息

·出票人名称

·出票人证件号码

（3）承兑人信息

·承兑人名称

·承兑人证件号码

（4）收款人信息

·收款人名称

·收款人证件号码

（5）持票人信息

·持票人名称

·持票人证件号码

2.票据背书

票据背书是转让票据权利的重要方式和手段。发票票据需要先获取持票人持有的票据，填写被背书人的信息：

·被背书人名称；

·被背书人的证件号码。

发起票据背书的请求后会提交给被背书人。被背书人接收到票据背书的请求后，可以选择签收票据或者拒绝背书。

3.票据背书签收

票据背书签收实现了票据权利的转让，签收前需要仔细地查阅待签收票据的信息，确保内容的完整性。票据签收的过程需要用被背书人的签名密钥对签收的内容进行数字签名，实现防抵赖的功能。

4.票据背书拒收

被背书人可以拒绝对票据背书的请求进行签收，拒绝以后，票据的持有人还是票据背书的发起者。拒绝背书的操作也会记录到票据的历史流转信息中。

5.票据信息查询

票据信息查询可以获取自己持有的票据和待签收的票据，也可以根据票据号码查询票据的详细信息，包括票据的历史流转信息。

12.3　票据背书架构设计

根据票据背书的需求分析，本节设计一个简单的架构，再定义票据背书的数据模型。

12.3.1　票据背书的分层架构

我们利用图10.1所示Hyperledger Fabric 1.0的应用开发模型来实现票据背书的应用场景。我们将基于区块链的数字票据进行分层设计，包括Hyperledger Fabric 1.0底层平台、智能合约、业务层和应用层，如图12-2所示。

图12-2　票据背书的分层架构

每个层的主要功能如下。

1）区块链底层平台：提供分布式共享账本的维护、状态数据库维护、智能合约的全生命周期管理等区块链功能，实现数据的不可篡改和智能合约的业务逻辑。根据第11章的内容搭建区块链网络以后，默认就提供了这部分功能。另外，通过fabric-ca提供成员注册和注销等功能。

2）智能合约：智能合约通过链码来实现，包括票据发布、票据背书、票据背书签收、票据背书拒绝等链码调用功能，链码查询包括查询持票人票据、查询待签收票据、根据链码号码查询票据信息等。票据系统的其他功能（比如贴现、转贴现、再贴现、回购等一系列业务类型）都可以在智能合约里实现，这部分功能留给读者继续完善。

3）业务层：业务层是应用程序的后端服务，给Web应用提供RESTful的接口，处理前端的业务请求。后端服务的基本功能包括用户管理和票据管理，通过Hyperledger Fabric 1.0提供的Node.js SDK和区块链网络进行通信。业务层也可以和其他的业务系统进行交互。

4）应用层： Web应用采用Angular.js+HTML+CSS的前端架构编写具有MVC、模块化、自动数据绑定等特点的单页面应用，提供用户交互的界面操作，包括用户操作的功能和业务操作的功能。用户是内置的，只提供用户登录和用户退出操作。业务操作包括发布票据、查询持票人持有的票据、发起票据背书、查询待签收票据、签收票据背书、拒绝票据背书等功能。

各个层之间采用不同的接口，业务层的Node.js SDK、智能合约和区块链底层平台之间采用gRPC的接口，业务层和Web应用之间采用RESTful的接口。

12.3.2　票据背书的数据模型

本节看一下链码设计的数据模型，包括票据数据结构定义和票据状态定义。

1.票据数据结构

票据信息包括票据基本信息、出票人信息、承兑人信息、收款人信息、持票人信息、待背书人信息、拒绝背书人信息、票据状态和票据背书历史等，数据结构定义如下：

// 票据数据结构
type Bill struct {
BillInfoID string `json:BillInfoID` //票据号码
BillInfoAmt string `json:BillInfoAmt` //票据金额
BillInfoType string `json:BillInfoType` //票据类型
BillInfoIsseDate string `json:BillInfoIsseDate` //票据出票日期
BillInfoDueDate string `json:BillInfoDueDate` //票据到期日期
DrwrCmID string `json:DrwrCmID` //出票人证件号码
DrwrAcct string `json:DrwrAcct` //出票人名称
AccptrCmID string `json:AccptrCmID` //承兑人证件号码
AccptrAcct string `json:AccptrAcct` //承兑人名称
PyeeCmID string `json:PyeeCmID` //收款人证件号码
PyeeAcct string `json:PyeeAcct` //收款人名称
HodrCmID string `json:HodrCmID` //持票人证件号码
HodrAcct string `json:HodrAcct` //持票人名称
WaitEndorserCmID string `json:WaitEndorserCmID` //待背书人证件号码
WaitEndorserAcct string `json:WaitEndorserAcct` //待背书人名称
RejectEndorserCmID string `json:RejectEndorserCmID` //拒绝背书人证件号码
RejectEndorserAcct string `json:RejectEndorserAcct` //拒绝背书人名称
State string `json:State` //票据状态
History []HistoryItem `json:History` //票据背书历史
}

票据历史信息包含了票据的流转信息，比如票据发布、票据签收、票据拒绝等都会记录到历史信息中，数据结构定义如下：

// 背书历史item结构
type HistoryItem struct {
TxId string `json:"txId"`
Bill Bill `json:"bill"`
}

票据历史信息是智能合约自动完成的。

2.票据状态模型

票据状态定义如表12-2所示。

表12-2　票据状态定义

票据背书的状态转移图如图12-3所示。

图12-3　票据背书的状态转移图

票据发布以后进入票据新发布状态NewPublish，票据持票人可以提交票据背书的操作进行票据权利转移，进入票据等待签收的状态EndrWaitSign。票据被背书人接收到票据背书请求后，可以选择签收票据或者拒绝签收，票据签收成功进入状态EndrSigned，持票人转移为被背书人；拒绝签收进入状态EndrReject，持票人保持不变还是原有的持票人。处于EndrSigned和EndrReject状态的持票人都可以再次发起票据背书的请求，进入下一轮的操作。

12.4　票据背书实现

票据背书的实现分为两个部分，即基于Hyperledger Fabric Node.js SDK的应用程序和链码功能的实现。本章所有的代码托管到Github上：https://github.com/ChainNova/trainingProjects/tree/master/billEndorse 。后面只介绍部分业务逻辑的实现。

12.4.1　应用程序实现

应用程序分为Web应用前端和后端服务。这里只介绍后端服务的实现，Web应用前端部分请参考Github上的实现。特别说明一下，本示例中的代码只用来演示和说明如何开发基于Hyperledger Fabric 1.0的区块链应用程序，接口的设计和代码实现都不严格，在实际的项目中需要做优化。

1.后端服务提供的接口定义

后端服务给Web应用提供的是RESTful的接口，全部的请求都是POST请求，Content-Type是“application/json”。

接口主要分为用户登录接口、票据发布接口、查询本人持有票据接口、票据背书请求接口、查询待签收票据接口、查询票据信息接口、票据背书回复接口等。下面逐一以例子形式展示接口的使用，测试可以采用wget、curl等支持RESTful接口的工具，也可以采用Postman等可视化工具，也可以编程实现。

（1）用户登录接口

所有的操作都需要先登录并获取token，作为下一次操作的凭证。用户登录接口URL是http://ip:port/login ，其中，ip和port是Web应用的地址，这些参数都需要根据实际的部署做修改。

输入的Body信息如下：

{
"username": "alice",
"orgName": "org1",
"password": "123456"
}

返回的信息如下：

{
"success": true,
"secret": "BGjQXLFbHgGJ",
"message": "alice enrolled Successfully",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM0MzAsInVzZ
XJuYW1lIjoiYWxpY2UiLCJvcmdOYW1lIjoib3JnMSIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNTEyMTA3NDMwfQ.QnIyaxuq8G4JlolBNq3DMKYfs6q8zjLUYwRjxS1GdxU",
"user": {
"username": "alice",
"name": "A公司",
"passwd": "123456",
"cmId": "ACMID",
"Acct": "A公司"
}
}

其中，token是基于JSON Web Token实现的，详细的介绍参考https://jwt.io 。

（2）票据发布接口

新票据需要先通过发布接口发布到区块链上，发布成功后，初始状态为“新发布”，标记成000001。若区块链上已经有该票据，输出为错误，提示为“票据重复发布”。

票据操作接口的URL都是相同的：http://ip:port/channels/mychannel/chaincodes/mycc/invoke ，其中，ip和port是Web应用的地址，mychannel是通道名称，mycc是链码的名称，这些参数都需要根据实际的部署做修改。

票据操作调用的接口通过Body信息来区分：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM0MzAsI
nVzZXJuYW1lIjoiYWxpY2UiLCJvcmdOYW1lIjoib3JnMSIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNTEyMTA3NDMwfQ.QnIyaxuq8G4JlolBNq3DMKYfs6q8zjLUYwRjxS1GdxU",
"peers": ["peer1"],
"fcn":"issue",
"args":["{\"BillInfoID\":\"POC10000998\",\"BillInfoAmt\":\"222\",\"BillInfoType\":\"111\",\"BillInfoIsseDate\":\"20170910\",\"BillInfoDueDate\":\"20171112\",\"DrwrCmID\":\"111\",\"DrwrAcct\":\"111\",\"AccptrCmID\":\"111\",\"AccptrAcct\":\"111\",\"PyeeCmID\":\"111\",\"PyeeAcct\":\"111\",\"HodrCmID\":\"ACMID\",\"HodrAcct\":\"A公司\"}"]
}

其中，票据操作接口是通用的结构，各参数的含义如表12-3所示。

表12-3　后端服务的接口参数定义

返回信息如下：

{
"success": true,
"message": "9a1525ef5a388530c1757c9c1c565bf52422e9a775a03d20e9aa2273b008aa31"
}

（3）票据背书接口

票据背书接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM0MzAsInVzZXJuYW1lIjoiYWxpY2UiLCJvcmdOYW1lIjoib3JnMSIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNTEyMTA3NDMwfQ.QnIyaxuq8G4JlolBNq3DMKYfs6q8zjLUYwRjxS1GdxU",
"peers": ["peer1"],
"fcn":"endorse",
"args":["POC10000998","BCMID","B公司"]
}

其中，票据背书的fcn是endorse，args参数的信息参考12.4.2节。

返回的信息如下：

{
"success": true,
"message": "ae87c2e1d51f22125e9c16375420aee68acd0bb3dbcb5950a787e4a5cba3b080"
}

（4）票据背书签收接口

票据背书签收接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM4MDMsI
nVzZXJuYW1lIjoiYm9iIiwib3JnTmFtZSI6Im9yZzEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTUxMjEwNzgwM30.RMxkdTOP2e6K03hD_6GpkHV3mcZpeqjcxfdqshb7gKk",
"peers": ["peer1"],
"fcn":"accept",
"args":["POC10000998","BCMID","B公司"]
}

其中，票据背书签收的用户bob需要先登录并获取token，票据背书签收的fcn是accept，args参数的信息参考12.4.2节。

返回的信息如下：

{
"success": true,
"message": "3710f07807521218f4ccadcdb06c7ffba21f44fc2f70a773d3bc707fe48d7f99"
}

（5）票据背书拒收接口

票据背书拒收接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwODkwMjcsI
nVzZXJuYW1lIjoiYWxpY2UiLCJvcmdOYW1lIjoib3JnMSIsInBhc3N3b3JkIjoiMTIzNDU2IiwiaWF0IjoxNTEyMDUzMDI3fQ.oqDRAAhuD8KSgFFuW9wCxlYpGMXXxGHV18SLMyVBAMo",
"peers": ["peer1"],
"fcn":"reject",
"args":["POC10000998","BCMID","B公司"]
}

其中，票据背书拒收的fcn是reject，args参数的信息参考12.4.2节。

返回信息如下：

{
"success": true,
"message": "183b9ea86804f1fbf1cdd172210b612c89514e9266b082d54b5acda5be4b2f69"
}

（6）查询持票人的票据列表接口

查询持票人的票据列表接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM4MDMsI
nVzZXJuYW1lIjoiYm9iIiwib3JnTmFtZSI6Im9yZzEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTUxMjEwNzgwM30.RMxkdTOP2e6K03hD_6GpkHV3mcZpeqjcxfdqshb7gKk",
"peers": ["peer1"],
"fcn":"queryMyBill",
"args":["BCMID"]
}

其中，查询持票人的票据列表的fcn是queryMyBill，args参数的信息参考12.4.2节。

返回的信息如下：

{
"success": true,
"message": "[
{
'BillInfoID': 'POC10000998',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'BCMID',
'HodrAcct': 'B公司',
'WaitEndorserCmID': '',
'WaitEndorserAcct': '',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'EndrSigned',
'History': null
}
]"
}

说明一下，为了方便阅读，上面的返回信息对结果做了格式化处理，把原始的结果中双引号的转义“\"”替换成了单引号“'”，后面的展示结果也做了相同的处理。

（7）查询待签收票据列表接口

查询待签收票据列表接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM4MDMsI
nVzZXJuYW1lIjoiYm9iIiwib3JnTmFtZSI6Im9yZzEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTUxMjEwNzgwM30.RMxkdTOP2e6K03hD_6GpkHV3mcZpeqjcxfdqshb7gKk",
"peers": ["peer1"],
"fcn":"queryMyWaitBill",
"args":["BCMID"]
}

其中，查询待签收票据列表的fcn是queryMyWaitBill，args参数的信息参考12.4.2节。

返回的信息如下：

{
"success": true,
"message": "[
{
'BillInfoID': 'POC10000999',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'ACMID',
'HodrAcct': 'A公司',
'WaitEndorserCmID': 'BCMID',
'WaitEndorserAcct': 'B公司',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'EndrWaitSign',
'History': null
}
]"
}

（8）根据票据号码查询票据信息接口

根据票据号码查询票据信息接口输入的Body信息如下：

{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIxNDM4MDMsI
nVzZXJuYW1lIjoiYm9iIiwib3JnTmFtZSI6Im9yZzEiLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTUxMjEwNzgwM30.RMxkdTOP2e6K03hD_6GpkHV3mcZpeqjcxfdqshb7gKk",
"peers": ["peer1"],
"fcn":"queryByBillNo",
"args":["POC10000998"]
}

其中，根据票据号码查询票据信息的fcn是queryByBillNo，args参数的信息参考12.4.2节。

返回的信息如下：

{
"success": true,
"message": "{
'BillInfoID': 'POC10000998',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'BCMID',
'HodrAcct': 'B公司',
'WaitEndorserCmID': '',
'WaitEndorserAcct': '',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'EndrSigned',
'History': [
{
'txId': '9a1525ef5a388530c1757c9c1c565bf52422e9a775a03d20e9aa2273
b008aa31',
'bill': {
'BillInfoID': 'POC10000998',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'ACMID',
'HodrAcct': 'A公司',
'WaitEndorserCmID': '',
'WaitEndorserAcct': '',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'NewPublish',
'History': null
}
},
{
'txId': 'ae87c2e1d51f22125e9c16375420aee68acd0bb3dbcb5950a787e4a5
cba3b080',
'bill': {
'BillInfoID': 'POC10000998',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'ACMID',
'HodrAcct': 'A公司',
'WaitEndorserCmID': 'BCMID',
'WaitEndorserAcct': 'B公司',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'EndrWaitSign',
'History': null
}
},
{
'txId': '3710f07807521218f4ccadcdb06c7ffba21f44fc2f70a773d3bc707f
e48d7f99',
'bill': {
'BillInfoID': 'POC10000998',
'BillInfoAmt': '222',
'BillInfoType': '111',
'BillInfoIsseDate': '111',
'BillInfoDueDate': '111',
'DrwrCmID': '111',
'DrwrAcct': '111',
'AccptrCmID': '111',
'AccptrAcct': '111',
'PyeeCmID': '111',
'PyeeAcct': '111',
'HodrCmID': 'BCMID',
'HodrAcct': 'B公司',
'WaitEndorserCmID': '',
'WaitEndorserAcct': '',
'RejectEndorserCmID': '',
'RejectEndorserAcct': '',
'State': 'EndrSigned',
'History': null
}
}
]
}"
}

2.HFC Node.js SDK的使用

HFC Node.js SDK的使用包括创建通道、加入通道、安装链码、实例化链码、调用链码等。

（1）创建通道

首先介绍getOrgAdmin函数，其目的是根据传入的orgName将client实例设置为对应该组织，针对后面的操作进行组织的配置，它作为util函数会在后面多次用到，具体实现内容如下：

1）将client实例的CryptoSuit切换为传入组织的CryptoSuit；

2）将client实例的StateStore切换为传入组织的StateStore；

3）返回传入组织的admin用户实例。

var getOrgAdmin = function(userOrg) {
var admin = ORGS[userOrg].admin;
var keyPath = path.join(__dirname, admin.key);
var keyPEM = Buffer.from(readAllFiles(keyPath)[0]).toString();
var certPath = path.join(__dirname, admin.cert);
var certPEM = readAllFiles(certPath)[0].toString();

var client = getClientForOrg(userOrg);
var cryptoSuite = hfc.newCryptoSuite();
if (userOrg) {
cryptoSuite.setCryptoKeyStore(hfc.newCryptoKeyStore({path: getKeyStoreFo
rOrg(getOrgName(userOrg))}));
client.setCryptoSuite(cryptoSuite);
}

return hfc.newDefaultKeyValueStore({
path: getKeyStoreForOrg(getOrgName(userOrg))
}).then((store) => {
client.setStateStore(store);

return client.createUser({
username: 'peer'+userOrg+'Admin',
mspid: getMspID(userOrg),
cryptoContent: {
privateKeyPEM: keyPEM,
signedCertPEM: certPEM
}
});
});
};

下面介绍创建通道的步骤：

1）首先根据传入的channelConfigPath，获取通道配置文件，提取为字节；

2）把client实例切换到传入组织；

3）使用传入组织的加密材料对通道配置字节签名；

4）构建request，向orderer发送创建通道请求；

5）创建成功则返回成功的结构对象，失败则抛出异常。

var createChannel = function(channelName, channelConfigPath, username, orgName) {
logger.debug('\n====== Creating Channel \'' + channelName + '\' ======\n');
var client = helper.getClientForOrg(orgName);
var channel = helper.getChannelForOrg(orgName);

// 取到通道配置文件
var envelope = fs.readFileSync(path.join(__dirname, channelConfigPath));
// 提取通道配置文件字节
var channelConfig = client.extractChannelConfig(envelope);

return helper.getOrgAdmin(orgName).then((admin) => {
logger.debug(util.format('Successfully acquired admin user for
the organization "%s"', orgName));
// 对通道配置字节签名为"背书"，这是由orderer的通道创建策略所要求的
let signature = client.signChannelConfig(channelConfig);

let request = {
config: channelConfig,
signatures: [signature],
name: channelName,
orderer: channel.getOrderers()[0],
txId: client.newTransactionID()
};

// 将创建通道请求发送给orderer
return client.createChannel(request);
}, (err) => {
logger.error('Failed to enroll user \''+username+'\'. Error: ' +
err);
throw new Error('Failed to enroll user \''+username+'\'' + err);
}).then((response) => {
logger.debug(' response ::%j', response);
if (response && response.status === 'SUCCESS') {
logger.debug('Successfully created the channel.');
let response = {
success: true,
message: 'Channel \'' + channelName + '\' created
Successfully'
};
return response;
} else {
logger.error('\n!!!!!!!!! Failed to create the channel \''
+ channelName +
'\' !!!!!!!!!\n\n');
throw new Error('Failed to create the channel \'' +
channelName + '\'');
}
}, (err) => {
logger.error('Failed to initialize the channel: ' + err.stack ?
err.stack :
err);
throw new Error('Failed to initialize the channel: ' + err.stack ?
err.stack : err);
});
};

exports.createChannel = createChannel;

（2）加入通道

加入通道包括如下步骤：

1）client实例切换到传入组织；

2）基于之前创建的通道，获取该通道的创世区块；

3）向要加入通道的peers发送加入通道的请求；

4）在向peers发送加入通道请求的同时，为各个peer分别注册block eventhub来监听区块产生的过程是否正常；

5）通过校验第一个peer的response status是否为200来判断加入通道的结果，成功则返回成功的结构对象，失败则抛出异常。

var joinChannel = function(channelName, peers, username, org) {
// 断开与event hub连接的函数
var closeConnections = function(isSuccess) {
if (isSuccess) {
logger.debug('\n============ Join Channel is SUCCESS ====
========\n');
} else {
logger.debug('\n!!!!!!!! ERROR: Join Channel FAILED !!!!!
!!!\n');
}
logger.debug('');
for (var key in allEventhubs) {
var eventhub = allEventhubs[key];
if (eventhub && eventhub.isconnected()) {
//logger.debug('Disconnecting the event hub');
eventhub.disconnect();
}
}
};
//logger.debug('\n============ Join Channel ============\n')
logger.info(util.format(
'Calling peers in organization "%s" to join the channel', org));

var client = helper.getClientForOrg(org);
var channel = helper.getChannelForOrg(org);
var eventhubs = [];

return helper.getOrgAdmin(org).then((admin) => {
logger.info(util.format('received member object for admin of the
organization "%s": ', org));
tx_id = client.newTransactionID();
let request = {
txId : tx_id
};

return channel.getGenesisBlock(request);
}).then((genesis_block) => {
tx_id = client.newTransactionID();
var request = {
targets: helper.newPeers(peers, org),
txId: tx_id,
block: genesis_block
};

eventhubs = helper.newEventHubs(peers, org);
for (let key in eventhubs) {
let eh = eventhubs[key];
eh.connect();
allEventhubs.push(eh);
}

var eventPromises = [];
eventhubs.forEach((eh) => {
let txPromise = new Promise((resolve, reject) => {
let handle = setTimeout(reject, parseInt(config.
eventWaitTime));
eh.registerBlockEvent((block) => {
clearTimeout(handle);
// 一个peer可能属于多个通道，所以必须检查这个配置block是否来自于我们请求加入的通道
if (block.data.data.length === 1) {
// 配置block只包括一个交易
var channel_header = block.data.
data[0].payload.header.channel_header;
if (channel_header.channel_id
=== channelName) {
resolve();
}
else {
reject();
}
}
});
});
eventPromises.push(txPromise);
});
let sendPromise = channel.joinChannel(request);
return Promise.all([sendPromise].concat(eventPromises));
}, (err) => {
logger.error('Failed to enroll user \'' + username + '\' due to
error: ' +
err.stack ? err.stack : err);
throw new Error('Failed to enroll user \'' + username +
'\' due to error: ' + err.stack ? err.stack : err);
}).then((results) => {
logger.debug(util.format('Join Channel R E S P O N S E : %j',
results));
if (results[0] && results[0][0] && results[0][0].response &&
results[0][0]
.response.status == 200) {
logger.info(util.format(
'Successfully joined peers in organization %s to
the channel \'%s\'',
org, channelName));
closeConnections(true);
let response = {
success: true,
message: util.format(
'Successfully joined peers in organization
%s to the channel \'%s\'',
org, channelName)
};
return response;
} else {
logger.error(' Failed to join channel');
closeConnections();
throw new Error('Failed to join channel');
}
}, (err) => {
logger.error('Failed to join channel due to error: ' + err.stack ?
err.stack :
err);
closeConnections();
throw new Error('Failed to join channel due to error: ' + err.
stack ? err.stack :
err);
});
};
exports.joinChannel = joinChannel;

（3）安装链码

安装链码包括如下步骤：

1）client实例切换到传入组织；

2）client实例发出安装链码请求，请求中包括目标peers、链码路径、链码名称和链码版本；

3）对目标peers返回的proposalResponses结果依次校验，所有peers都成功则返回成功的结构对象，有peer失败则抛出异常。

var installChaincode = function(peers, chaincodeName, chaincodePath,
chaincodeVersion, username, org) {
logger.debug(
'\n============ Install chaincode on organizations ============
\n');
helper.setupChaincodeDeploy();
var channel = helper.getChannelForOrg(org);
var client = helper.getClientForOrg(org);

return helper.getOrgAdmin(org).then((user) => {
var request = {
targets: helper.newPeers(peers, org),
chaincodePath: chaincodePath,
chaincodeId: chaincodeName,
chaincodeVersion: chaincodeVersion
};
return client.installChaincode(request);
}, (err) => {
logger.error('Failed to enroll user \'' + username + '\'. ' + err);
throw new Error('Failed to enroll user \'' + username + '\'. ' + err);
}).then((results) => {
var proposalResponses = results[0];
var proposal = results[1];
var all_good = true;
for (var i in proposalResponses) {
let one_good = false;
if (proposalResponses && proposalResponses[i].response &&
proposalResponses[i].response.status === 200) {
one_good = true;
logger.info('install proposal was good');
} else {
logger.error('install proposal was bad');
}
all_good = all_good & one_good;
}
if (all_good) {
logger.info(util.format(
'Successfully sent install Proposal and received
ProposalResponse: Status - %s',
proposalResponses[0].response.status));
logger.debug('\nSuccessfully Installed chaincode on
organization ' + org +
'\n');
return 'Successfully Installed chaincode on organization '
+ org;
} else {
logger.error(
'Failed to send install Proposal or receive valid
response. Response null or status is not 200. exiting...'
);
return 'Failed to send install Proposal or receive valid
response. Response null or status is not 200. exiting...';
}
}, (err) => {
logger.error('Failed to send install proposal due to error: ' +
err.stack ?
err.stack : err);
throw new Error('Failed to send install proposal due to error: ' +
err.stack ?
err.stack : err);
});
};
exports.installChaincode = installChaincode;

（4）实例化链码

实例化链码包括如下步骤：

1）client实例切换到传入组织；

2）channel调用initialize（），该方法会使用对应组织的MSPs实例化channel对象；

3）发送背书proposal给endorsers（args里面指定的背书节点）；

4）对目标endorsers返回的proposalResponses结果依次校验，所有endorsers都背书成功才进入下一步，有endorsers背书失败则抛出异常；

5）endorsers背书成功后，应用端将背书proposalResponses和之前的proposal打包成request，调用sendTransaction发给orderer，这时因为orderer经过order后再通知peers进行实例化的操作是异步的，需要注册transaction event来监听实例化的最终结果；

6）在sendTransaction和transaction event都成功返回的情况下，才说明实例化链码成功，此时返回成功的结构对象，若transaction event监听到失败则抛出异常。

var instantiateChaincode = function(channelName, chaincodeName, chaincodeVersion,
functionName, args, username, org) {
logger.debug('\n============ Instantiate chaincode on organization ' + org +
' ============\n');

var channel = helper.getChannelForOrg(org);
var client = helper.getClientForOrg(org);

return helper.getOrgAdmin(org).then((user) => {

// channel实例从orderer读取该通道的配置区块，并基于所加入的组织实例化验证MSPs
return channel.initialize();
}, (err) => {
logger.error('Failed to enroll user \'' + username + '\'. ' + err);
throw new Error('Failed to enroll user \'' + username + '\'. ' + err);
}).then((success) => {
tx_id = client.newTransactionID();
// 发送背书proposal给endorser
var request = {
chaincodeId: chaincodeName,
chaincodeVersion: chaincodeVersion,
args: args,
txId: tx_id
};

if (functionName)
request.fcn = functionName;

return channel.sendInstantiateProposal(request);
}, (err) => {
logger.error('Failed to initialize the channel');
throw new Error('Failed to initialize the channel');
}).then((results) => {
var proposalResponses = results[0];
var proposal = results[1];
var all_good = true;
for (var i in proposalResponses) {
let one_good = false;
if (proposalResponses && proposalResponses[i].response &&
proposalResponses[i].response.status === 200) {
one_good = true;
logger.info('instantiate proposal was good');
} else {
logger.error('instantiate proposal was bad');
}
all_good = all_good & one_good;
}
if (all_good) {
logger.info(util.format(
'Successfully sent Proposal and received
ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s',
proposalResponses[0].response.status,
proposalResponses[0].response.message,
proposalResponses[0].response.payload,
proposalResponses[0].endorsement
.signature));
var request = {
proposalResponses: proposalResponses,
proposal: proposal
};

// 设置一个transaction listener并且设置30秒timeout
// 如果在timeout的时限内，transaction没有被有效提交则返回错误
var deployId = tx_id.getTransactionID();

eh = client.newEventHub();
let data = fs.readFileSync(path.join(__dirname, ORGS[org].
peers['peer1'][
'tls_cacerts'
]));

eh.setPeerAddr(ORGS[org].peers['peer1']['events'], {
pem: Buffer.from(data).toString(),
'ssl-target-name-override': ORGS[org].peers['peer1']
['server-hostname']
});
eh.connect();

let txPromise = new Promise((resolve, reject) => {
let handle = setTimeout(() => {
eh.disconnect();
reject();
}, 30000);

eh.registerTxEvent(deployId, (tx, code) => {
logger.info(
'The chaincode instantiate
transaction has been committed on peer ' +
eh._ep._endpoint.addr);
clearTimeout(handle);
eh.unregisterTxEvent(deployId);
eh.disconnect();

if (code !== 'VALID') {
logger.error('The chaincode
instantiate transaction was invalid, code = ' + code);
reject();
} else {
logger.info('The chaincode
instantiate transaction was valid.');
resolve();
}
});
});

var sendPromise = channel.sendTransaction(request);
return Promise.all([sendPromise].concat([txPromise])).
then((results) => {
logger.debug('Event promise all complete and
testing complete');
return results[0]; // Promise all队列的第一个返回值
是'sendTransaction()'的调用结果
}).catch((err) => {
logger.error(
util.format('Failed to send instantiate
transaction and get notifications within the timeout period. %s', err)
);
return 'Failed to send instantiate transaction and
get notifications within the timeout period.';
});
} else {
logger.error(
'Failed to send instantiate Proposal or receive
valid response. Response null or status is not 200. exiting...'
);
return 'Failed to send instantiate Proposal or receive
valid response. Response null or status is not 200. exiting...';
}
}, (err) => {
logger.error('Failed to send instantiate proposal due to error: '
+ err.stack ?
err.stack : err);
return 'Failed to send instantiate proposal due to error: ' +
err.stack ?
err.stack : err;
}).then((response) => {
if (response.status === 'SUCCESS') {
logger.info('Successfully sent transaction to the orderer.');
return 'Chaincode Instantiation is SUCCESS';
} else {
logger.error('Failed to order the transaction. Error code:
' + response.status);
return 'Failed to order the transaction. Error code: ' +
response.status;
}
}, (err) => {
logger.error('Failed to send instantiate due to error: ' + err.
stack ? err
.stack : err);
return 'Failed to send instantiate due to error: ' + err.stack ?
err.stack :
err;
});
};
exports.instantiateChaincode = instantiateChaincode;

（5）调用链码

调用链码和之前实例化链码步骤类似，也是需要先发出背书，包括如下步骤。

1）client实例切换到传入组织。

2）发送proposal给endorsers（args里面指定的背书节点）。

3）对目标endorsers返回的proposalResponses结果依次校验，所有endorsers都背书成功才进入下一步，有endorsers背书失败则抛出异常。

4）endorsers背书成功后，应用端将背书proposalResponses和之前的proposal打包成request，调用sendTransaction发给orderer，这时因为orderer经过order后再通知peers进行调用链码的操作是异步的，需要注册transaction event来监听调用链码的最终结果。

5）在sendTransaction和transaction event都成功返回的情况下，才说明调用链码成功，此时返回成功的结构对象，若transaction event监听到失败则抛出异常。

var invokeChaincode = function(peerNames, channelName, chaincodeName, fcn, args, username, org) { logger.debug(util.format('\n============ invoke transaction on organization %s ============\n', org)); var client = helper.getClientForOrg(org); var channel = helper.getChannelForOrg(org); var targets = (peerNames) ? helper.newPeers(peerNames, org) : undefined; var tx_id = null; var txRequest = null; return helper.getRegisteredUsers(username, org).then((user) => { tx_id = client.newTransactionID(); logger.debug(util.format('Sending transaction "%j"', tx_id)); // 发送背书proposal给endorser var request = { chaincodeId: chaincodeName, fcn: fcn, args: args, chainId: channelName, txId: tx_id }; if (targets) request.targets = targets; var txRequest = channel.sendTransactionProposal(request) return txRequest; }, (err) => { logger.error('Failed to enroll user \'' + username + '\'. ' + err); throw new Error('Failed to enroll user \'' + username + '\'. ' + err); }).then((results) => { var proposalResponses = results[0]; var proposal = results[1]; var all_good = true; for (var i in proposalResponses) { let one_good = false; if (proposalResponses && proposalResponses[i].response && proposalResponses[i].response.status === 200) { one_good = true; logger.info('transaction proposal was good'); } else { logger.error(proposalResponses[i]); logger.error('transaction proposal was bad'); if (proposalResponses[i].message != null) { return proposalResponses[i].message; } } all_good = all_good & one_good; } if (all_good) { logger.debug(util.format( 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', proposalResponses[0].response.status, proposalResponses[0].response.message, proposalResponses[0].response.payload, proposalResponses[0].endorsement .signature)); var request = { proposalResponses: proposalResponses, proposal: proposal }; // 设置一个transaction listener并且设置30秒timeout // 如果在timeout的时限内，transaction没有被有效提交则返回错误 var transactionID = tx_id.getTransactionID(); var eventPromises = []; if (!peerNames) { peerNames = channel.getPeers().map(function(peer) { return peer.getName(); }); }

var eventhubs = helper.newEventHubs(peerNames, org); for (let key in eventhubs) { let eh = eventhubs[key]; eh.connect(); let txPromise = new Promise((resolve, reject) => { let handle = setTimeout(() => { eh.disconnect(); reject(); }, 30000); eh.registerTxEvent(transactionID, (tx, code) => { clearTimeout(handle); eh.unregisterTxEvent(transactionID); eh.disconnect(); if (code !== 'VALID') { logger.error(

'The balance transfer transaction was invalid, code = ' + code); reject(); } else { logger.info(

'The balance transfer transaction has been committed on peer ' + eh._ep._endpoint.addr); resolve(); } }); }); eventPromises.push(txPromise); }; var sendPromise = channel.sendTransaction(request); return Promise.all([sendPromise].concat(eventPromises)). then((results) => { logger.debug(' event promise all complete and testing complete'); return results[0]; // Promise all队列的第一个返回值 // 是'sendTransaction()'的调用结果 }).catch((err) => { logger.error( 'Failed to send transaction and get notifications within the timeout period.' ); return 'Failed to send transaction and get notifications within the timeout period.'; }); } else { logger.error( 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' ); return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'; } }, (err) => { logger.error('Failed to send proposal due to error: ' + err. stack ? err.stack : err); return 'Failed to send proposal due to error: ' + err.stack ? err.stack : err; }).then((response) => { if (response.status === 'SUCCESS') { logger.info('Successfully sent transaction to the orderer.'); return tx_id.getTransactionID(); } else { if (response.status != null) { logger.error('Failed to order the transaction. Error code: ' + response.status); return 'Failed to order the transaction. Error code: ' + response.status; }else { return response; }

} }, (err) => { logger.error('Failed to send transaction due to error: ' + err. stack ? err .stack : err); return 'Failed to send transaction due to error: ' + err.stack ? err.stack :err; }); }; exports.invokeChaincode = invokeChaincode;

12.4.2　链码功能实现

本节我们来看链码对外提供的功能接口和每个功能接口的实现过程。

1.链码接口定义

链码接口由两部分组成，即调用函数名称和调用参数。

（1）票据发布接口

票据发布的函数名称是issue，只有一个参数，是JSON结构的Bill对象：

{
"BillInfoID": "POC10000998",
"BillInfoAmt": "222",
"BillInfoType": "111",
"BillInfoIsseDate": "20170910",
"BillInfoDueDate": "20171112",
"DrwrCmID": "111",
"DrwrAcct": "111",
"AccptrCmID": "111",
"AccptrAcct": "111",
"PyeeCmID": "111",
"PyeeAcct": "111",
"HodrCmID": "ACMID",
"HodrAcct": "A公司"
}

各字段参数说明如表12-4所示。

表12-4　票据发布接口参数

（2）票据背书接口

票据背书的函数名称是endorse，有3个参数按表12-5所示顺序。

表12-5　票据背书接口参数

（3）票据背书签收接口

票据背书签收的函数名称是accept，有2个参数按表12-6的顺序。

表12-6　票据背书签收接口参数

（4）票据背书拒收接口

票据背书拒收的函数名称是reject，有2个参数按表12-7的顺序。

表12-7　票据背书拒收接口参数

（5）查询持票人票据列表接口

查询持票人票据列表的函数名称是queryMyBill，只有1个参数如表12-8所示。

表12-8　查询持票人票据列表接口参数

（6）查询持票人待签收票据列表接口

查询持票人待签收票据列表的函数名称是queryMyWaitBill，只有1个参数如表12-9所示。

表12-9　查询持票人待签收票据列表接口参数

（7）根据票据号码查询票据信息接口

根据票据号码查询票据信息的函数名称是queryByBillNo，只有1个参数如表12-10所示。

表12-10　根据票据号码查询票据信息接口参数

2.链码接口实现

链码初始化默认实现即可：

// chaincode Init 接口
func (a *BillChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
return shim.Success(nil)
}

链码调用接口包含票据发布、票据背书、票据签收、票据拒收、票据查询等：

// chaincode Invoke 接口
func (a *BillChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function,args := stub.GetFunctionAndParameters()

// invoke
if function == "issue" {
return a.issue(stub, args)
} else if function == "endorse" {
return a.endorse(stub, args)
} else if function == "accept" {
return a.accept(stub, args)
} else if function == "reject" {
return a.reject(stub, args)
}

// query
if function == "queryMyBill" {
return a.queryMyBill(stub, args)
} else if function == "queryByBillNo" {
return a.queryByBillNo(stub, args)
} else if function == "queryMyWaitBill" {
return a.queryMyWaitBill(stub, args)
}

res := getRetString(1,"ChainnovaChaincode Unkown method!")
chaincodeLogger.Infof("%s",res)

return shim.Error(res)
}

链码用到的一些通用接口：

// 链码返回结构
type chaincodeRet struct {
Code int // 0 成功 1 其他
Des string // 描述
}

// 根据返回码和描述返回序列号后的字节数组
func getRetByte(code int,des string) []byte {
var r chaincodeRet
r.Code = code
r.Des = des

b,err := json.Marshal(r)

if err!=nil {
fmt.Println("marshal Ret failed")
return nil
}
return b
}

// 根据返回码和描述返回序列号后的字符串
func getRetString(code int,des string) string {
var r chaincodeRet
r.Code = code
r.Des = des

b,err := json.Marshal(r)

if err!=nil {
fmt.Println("marshal Ret failed")
return ""
}
chaincodeLogger.Infof("%s",string(b[:]))
return string(b[:])
}

// 根据票号取出票据
func (a *BillChaincode) getBill(stub shim.ChaincodeStubInterface,bill_No string)
(Bill, bool) {
var bill Bill
key := Bill_Prefix + bill_No
b,err := stub.GetState(key)
if b==nil {
return bill, false
}
err = json.Unmarshal(b,&bill)
if err!=nil {
return bill, false
}
return bill, true
}

// 保存票据
func (a *BillChaincode) putBill(stub shim.ChaincodeStubInterface, bill Bill) ([]
byte, bool) {

byte,err := json.Marshal(bill)
if err!=nil {
return nil, false
}

err = stub.PutState(Bill_Prefix + bill.BillInfoID, byte)
if err!=nil {
return nil, false
}
return byte, true
}

（1）票据发布

票据发布的实现如下：

// 票据发布
// args: 0 - {Bill Object}
func (a *BillChaincode) issue(stub shim.ChaincodeStubInterface, args []string)
pb.Response {
if len(args)!=1 {
res := getRetString(1,"ChainnovaChaincode Invoke issue args!=1")
return shim.Error(res)
}

var bill Bill
err := json.Unmarshal([]byte(args[0]), &bill)
if err!=nil {
res := getRetString(1,"ChainnovaChaincode Invoke issue unmarshal
failed")
return shim.Error(res)
}

// 根据票号查找是否票号已存在
_, existbl := a.getBill(stub, bill.BillInfoID)
if existbl {
res := getRetString(1,"ChainnovaChaincode Invoke issue failed :
the billNo has exist ")
return shim.Error(res)
}

if bill.BillInfoID == "" {
bill.BillInfoID = fmt.Sprintf("%d", time.Now().UnixNano())
}

// 更改票据信息和状态并保存票据:票据状态设为新发布
bill.State = BillInfo_State_NewPublish

// 保存票据
_, bl := a.putBill(stub, bill)
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke issue put bill
failed")
return shim.Error(res)
}
// 以持票人ID和票号构造复合key 向search表中保存 value为空即可 以便持票人批量查询
holderNameBillNoIndexKey, err := stub.CreateCompositeKey(IndexName, []
string{bill.HodrCmID, bill.BillInfoID})
if err != nil {
res := getRetString(1,"ChainnovaChaincode Invoke issue put search
table failed")
return shim.Error(res)
}
stub.PutState(holderNameBillNoIndexKey, []byte{0x00})

res := getRetByte(0,"invoke issue success")
return shim.Success(res)
}

（2）票据背书

票据背书的实现如下：

// 背书请求
// args: 0 - Bill_No ; 1 - Endorser CmId ; 2 - Endorser Acct
func (a *BillChaincode) endorse(stub shim.ChaincodeStubInterface, args []string)
pb.Response {
if len(args)<3 {
res := getRetString(1,"ChainnovaChaincode Invoke endorse args<3")
return shim.Error(res)
}
// 根据票号取得票据
bill, bl := a.getBill(stub, args[0])
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke endorse get bill
error")
return shim.Error(res)
}

if bill.HodrCmID == args[1] {
res := getRetString(1,"ChainnovaChaincode Invoke endorse failed:
Endorser should not be same with current Holder")
return shim.Error(res)
}
// 更改票据信息和状态并保存票据: 添加待背书人信息，重置已拒绝背书人，票据状态改为待背书
bill.WaitEndorserCmID = args[1]
bill.WaitEndorserAcct = args[2]
bill.RejectEndorserCmID = ""
bill.RejectEndorserAcct = ""
bill.State = BillInfo_State_EndrWaitSign

// 保存票据
_, bl = a.putBill(stub, bill)
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke endorse put bill
failed")
return shim.Error(res)
}
// 以待背书人ID和票号构造复合key 向search表中保存 value为空即可 以便待背书人批量查询
holderNameBillNoIndexKey, err := stub.CreateCompositeKey(IndexName, []
string{bill.WaitEndorserCmID, bill.BillInfoID})
if err != nil {
res := getRetString(1,"ChainnovaChaincode Invoke endorse put search
table failed")
return shim.Error(res)
}
stub.PutState(holderNameBillNoIndexKey, []byte{0x00})

res := getRetByte(0,"invoke endorse success")
return shim.Success(res)
}

（3）票据背书签收

票据背书签收的实现如下：

// 背书人接受背书
// args: 0 - Bill_No ; 1 - Endorser CmId ; 2 - Endorser Acct
func (a *BillChaincode) accept(stub shim.ChaincodeStubInterface, args []string)
pb.Response {
if len(args)<3 {
res := getRetString(1,"ChainnovaChaincode Invoke accept args<3")
return shim.Error(res)
}
// 根据票号取得票据
bill, bl := a.getBill(stub, args[0])
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke accept get bill
error")
return shim.Error(res)
}

// 维护search表: 以前手持票人ID和票号构造复合key 从search表中删除该key 以便前手持
票人无法再查到该票据
holderNameBillNoIndexKey, err := stub.CreateCompositeKey(IndexName, []
string{bill.HodrCmID, bill.BillInfoID})
if err != nil {
res := getRetString(1,"ChainnovaChaincode Invoke accept put search
table failed")
return shim.Error(res)
}
stub.DelState(holderNameBillNoIndexKey)

// 更改票据信息和状态并保存票据: 将前手持票人改为背书人，重置待背书人，票据状态改为背
书签收
bill.HodrCmID = args[1]
bill.HodrAcct = args[2]
bill.WaitEndorserCmID = ""
bill.WaitEndorserAcct = ""
bill.State = BillInfo_State_EndrSigned

// 保存票据
_, bl = a.putBill(stub, bill)
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke accept put bill
failed")
return shim.Error(res)
}

res := getRetByte(0,"invoke accept success")
return shim.Success(res)
}

（4）票据背书拒收

票据背书拒收的实现如下：

// 背书人拒绝背书
// args: 0 - Bill_No ; 1 - Endorser CmId ; 2 - Endorser Acct
func (a *BillChaincode) reject(stub shim.ChaincodeStubInterface, args []string)
pb.Response {
if len(args)<3 {
res := getRetString(1,"ChainnovaChaincode Invoke reject args<3")
return shim.Error(res)
}
// 根据票号取得票据
bill, bl := a.getBill(stub, args[0])
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke reject get bill
error")
return shim.Error(res)
}

// 维护search表: 以当前背书人ID和票号构造复合key 从search表中删除该key 以便当
前背书人无法再查到该票据
holderNameBillNoIndexKey, err := stub.CreateCompositeKey(IndexName, []
string{args[1], bill.BillInfoID})
if err != nil {
res := getRetString(1,"ChainnovaChaincode Invoke reject put search
table failed")
return shim.Error(res)
}
stub.DelState(holderNameBillNoIndexKey)

// 更改票据信息和状态并保存票据：将拒绝背书人改为当前背书人，重置待背书人，票据状态改
为背书拒绝
bill.WaitEndorserCmID = ""
bill.WaitEndorserAcct = ""
bill.RejectEndorserCmID = args[1]
bill.RejectEndorserAcct = args[2]
bill.State = BillInfo_State_EndrReject

// 保存票据
_, bl = a.putBill(stub, bill)
if !bl {
res := getRetString(1,"ChainnovaChaincode Invoke reject put bill
failed")
return shim.Error(res)
}

res := getRetByte(0,"invoke accept success")
return shim.Success(res)
}

（5）票据信息查询

获取自己持有的票据的实现如下：

// 查询我的票据:根据持票人编号 批量查询票据
// 0 - Holder CmId ;
func (a *BillChaincode) queryMyBill(stub shim.ChaincodeStubInterface, args []
string) pb.Response {
if len(args)!=1 {
res := getRetString(1,"ChainnovaChaincode queryMyBill args!=1")
return shim.Error(res)
}
// 以持票人ID从search表中批量查询所持有的票号
billsIterator, err := stub.GetStateByPartialCompositeKey(IndexName, []
string{args[0]})
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryMyBill get bill list error")
return shim.Error(res)
}
defer billsIterator.Close()

var billList = []Bill{}

for billsIterator.HasNext() {
kv, _ := billsIterator.Next()
// 取得持票人名下的票号
_, compositeKeyParts, err := stub.SplitCompositeKey(kv.Key)
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryMyBill
SplitCompositeKey error")
return shim.Error(res)
}
// 根据票号取得票据
bill, bl := a.getBill(stub, compositeKeyParts[1])
if !bl {
res := getRetString(1,"ChainnovaChaincode queryMyBill get
bill error")
return shim.Error(res)
}
billList = append(billList, bill)
}
// 取得并返回票据数组
b, err := json.Marshal(billList)
if err != nil {
res := getRetString(1,"ChainnovaChaincode Marshal queryMyBill
billList error")
return shim.Error(res)
}
return shim.Success(b)
}

查询我的待背书票据实现如下：

// 查询我的待背书票据: 根据背书人编号 批量查询票据
// 0 - Endorser CmId ;
func (a *BillChaincode) queryMyWaitBill(stub shim.ChaincodeStubInterface, args []
string) pb.Response {
if len(args)!=1 {
res := getRetString(1,"ChainnovaChaincode queryMyWaitBill args!=1")
return shim.Error(res)
}
// 以背书人ID从search表中批量查询所持有的票号
billsIterator, err := stub.GetStateByPartialCompositeKey(IndexName, []
string{args[0]})
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryMyWaitBill
GetStateByPartialCompositeKey error")
return shim.Error(res)
}
defer billsIterator.Close()

var billList = []Bill{}

for billsIterator.HasNext() {
kv, _ := billsIterator.Next()
// 从search表中批量查询与背书人有关的票号
_, compositeKeyParts, err := stub.SplitCompositeKey(kv.Key)
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryMyWaitBill
SplitCompositeKey error")
return shim.Error(res)
}
// 根据票号取得票据
bill, bl := a.getBill(stub, compositeKeyParts[1])
if !bl {
res := getRetString(1,"ChainnovaChaincode queryMyWaitBill
get bill error")
return shim.Error(res)
}
// 取得状态为待背书的票据 并且待背书人是当前背书人
if bill.State == BillInfo_State_EndrWaitSign && bill.
WaitEndorserCmID == args[0] {
billList = append(billList, bill)
}
}
// 取得并返回票据数组
b, err := json.Marshal(billList)
if err != nil {
res := getRetString(1,"ChainnovaChaincode Marshal queryMyWaitBill
billList error")
return shim.Error(res)
}
return shim.Success(b)
}

根据票据号码查询票据的详细信息实现如下：

// 根据票号取得票据 以及该票据背书历史
// 0 - Bill_No ;
func (a *BillChaincode) queryByBillNo(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args)!=1 {
res := getRetString(1,"ChainnovaChaincode queryByBillNo args!=1")
return shim.Error(res)
}
// 取得该票据
bill, bl := a.getBill(stub, args[0])
if !bl {
res := getRetString(1,"ChainnovaChaincode queryByBillNo get bill error")
return shim.Error(res)
}

// 取得背书历史: 通过fabric api取得该票据的变更历史
resultsIterator, err := stub.GetHistoryForKey(Bill_Prefix+args[0])
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryByBillNo GetHistoryForKey error")
return shim.Error(res)
}
defer resultsIterator.Close()

var history []HistoryItem
var hisBill Bill
for resultsIterator.HasNext() {
historyData, err := resultsIterator.Next()
if err != nil {
res := getRetString(1,"ChainnovaChaincode queryByBillNo
resultsIterator.Next() error")
return shim.Error(res)
}

var hisItem HistoryItem
hisItem.TxId = historyData.TxId //copy transaction id over
json.Unmarshal(historyData.Value, &hisBill)
// un stringify it aka JSON.parse()
if historyData.Value == nil { //bill has been deleted
var emptyBill Bill
hisItem.Bill = emptyBill //copy nil marble
} else {
json.Unmarshal(historyData.Value, &hisBill)
// un stringify it aka JSON.parse()
hisItem.Bill = hisBill //copy bill over
}
history = append(history, hisItem) //add this tx to the list
}
// 将背书历史作为票据的一个属性 一同返回
bill.History = history

b, err := json.Marshal(bill)
if err != nil {
res := getRetString(1,"ChainnovaChaincode Marshal queryByBillNo
billList error")
return shim.Error(res)
}
return shim.Success(b)
}

12.5　票据背书快速部署

在Github上提供了快速启动区块链网络和初始化的脚本。启动区块链网络和前端服务的脚本如下：

./setupFabricNetwork.sh &

创建通道及安装实例化链码的脚本如下：

./createChannelAndInstallChaincode.sh

上面的过程可能比较慢，等待出现“Total execution time”的日志就实例化结束了：

POST request Enroll on Org1 ...
{"success":true,"secret":"ILRegbALMUgw","message":"Jim enrolled Successfully","t
oken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwODE1ODcsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9y
ZzEiLCJpYXQiOjE1MTIwNDU1ODd9.OeOZEaMIoH9vVc8LXapDGKV5lrhIoTb7PnLj9sriXYo"}
ORG1 token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwODE1ODcsInVzZ
XJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9y
ZzEiLCJpYXQiOjE1MTIwNDU1ODd9.OeOZEaMIoH9vVc8LXapDGKV5lrhIoTb7PnLj9sriXYo

POST request Enroll on Org2 ...
{"success":true,"secret":"cZNfTxYCZBVJ","message":"Barry enrolled Successfully",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwODE1ODgsInVzZXJuYW1lIjoiQmFycnkiLCJvcmd
OYW1lIjoib3JnMiIsImlhdCI6MTUxMjA0NTU4OH0.C7pWIUY7dNvmLIAj2j52GPWE5KsjwbBhzz8su5b
ZW0Y"}
ORG2 token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTIwODE1ODgsInVzZ
XJuYW1lIjoiQmFycnkiLCJvcmd
OYW1lIjoib3JnMiIsImlhdCI6MTUxMjA0NTU4OH0.C7pWIUY7dNvmLIAj2j52GPWE5KsjwbBhzz8su5b
ZW0Y

POST request Create channel ...
{"success":true,"message":"Channel 'mychannel' created Successfully"}

POST request Join channel on Org1
{"success":true,"message":"Successfully joined peers in organization org1 to the
channel 'mychannel'"}

POST request Join channel on Org2
{"success":true,"message":"Successfully joined peers in
organization org2 to the channel 'mychannel'"}

POST Install chaincode on Org1
Successfully Installed chaincode on organization org1

POST Install chaincode on Org2
Successfully Installed chaincode on organization org2

POST instantiate chaincode on peer1 of Org1
Chaincode Instantiation is SUCCESS

Total execution time : 42 secs ...

接下来就可以通过Web页面访问系统。

12.6　票据背书展示

按照http://ip:4000/ng/src 即可访问，我们下面看下实现的效果。

12.6.1　系统登录

系统登录页面如图12-4所示。

图12-4　系统登录效果图

默认提供的用户名和密码如表12-11所示。

表12-11　票据背书系统默认提供的用户名和密码

12.6.2　发布票据

发布票据页面如图12-5所示。

图12-5　发布票据效果图

点击左边栏发布票据的选项，出现上图所示的发布票据页面，包含了票据的基本信息（票据号码、票据金额、票据类型、票据出票日期、票据到期日期等）、出票人信息（出票人名称、出票人证件号码等）、收款人信息（收款人名称、收款人证件号码等）、承兑人信息（承兑人名称、承兑人证件号码等）、持票人信息（持票人名称、持票人证件号码等）。票据号码是票据的唯一标识，是根据约定的规则离线生成的，发布票据的时候会自动监测是否存在重复票据。

12.6.3　我的票据

我的票据页面如图12-6所示。

图12-6　我的票据效果图

我的票据展示的是登录用户持有的所有票据，包含票号、票据状态、所属关系等基本信息，点击详情按钮会展示该票据的详细信息。

12.6.4　发起票据背书

发起票据背书页面如图12-7所示。

票据的详细信息除了发布票据时候录入的信息外，还展示了历史流转记录，包括交易号、操作业务、操作描述、当前持票人等信息。在浏览和确认了票据的详细信息后，可以选择发起票据背书，需要填入被背书人名称、被背书人证件号码等信息，就可以发起票据背书了。

图12-7　发起票据背书效果图

12.6.5　待签收票据列表

待签收票据列表页面如图12-8所示。

图12-8　待签收票据列表效果图

被背书人登录区块链票据系统后，点击左边栏的待签收列表，右边能展示出所有的待签收票据列表。

12.6.6　签收票据背书

签收票据背书页面如图12-9所示。

点击待签收票据的详情，会展示上图的票据信息。确认票据背书的内容后，可以选择签收背书或者拒绝背书。点击签收背书的按钮后，会提示签收成功，如图12-10所示。

图12-9　签收票据背书效果图

图12-10　票据背书已签收效果图

12.6.7　拒收票据背书

拒收票据背书页面如图12-11所示。

图12-11　票据背书已拒绝效果图

拒收票据背书以后，票据的持票人还是原有的票据持票人，还可以继续发起新的票据背书，同时在票据的历史流转信息里也会记录票据背书和票据背书被拒收的信息。

12.7　本章小结

本章以一个票据背书的案例为背景，介绍如何基于Hyperledger Fabric 1.0开发区块链的应用程序。

程序开发的流程参考第10章的内容，先对票据背书的应用场景进行需求分析，对部分内容进行抽象和简化，设计了票据背书的分层架构和数据模型。实际的开发是基于HFC Node.js SDK开发后端服务，提供给前端的Web应用，业务逻辑的实现都是在链码实现的。本章只介绍了和区块链部分直接相关的后端服务和链码开发，前端的Web应用可以参考任何成熟的技术框架。

通过本章的例子，能够完整地了解如何基于SDK开发和区块链网络交互的应用程序，熟悉链码提供的主要接口及其功能，利用链码实现简单的业务逻辑。本章主要聚焦在Hyperledger Fabric 1.0相关的内容，示例的代码和实现并不是直接可用的，大家需要根据实际情况做优化和完善。

附录A　术语表

附录B　超级账本的实用工具

Hyperledger Fabric 1.0提供了一些实用工具，方便和系统进行交互。

B.1　协议转换工具configtxlator

基于Hyperledger Fabric的区块链网络都是采用gRPC进行通信的，传输的消息和存储的区块采用的都是Protocol Buffer格式序列化的二进制结构，这不是对人友好的方式，尤其不方便修改。系统提供一个工具configtxlator，它提供了RESTful接口的服务，其功能如下。

B.1.1　协议转换

可以在Protocol Buffer和JSON格式间直接转换，Protocol Buffer还包含多种格式，有以下几种格式：

·common.Block：区块结构；

·common.Envelope：带有效载荷和数字签名的数字信封，区块的数据部分就是序列化后的数字信封；

·common.ConfigEnvelope：包含链配置的数字信封，内容包含ConfigUpdateEnvelope；

·common.ConfigUpdateEnvelope：提交给排序节点的配置数字信封；

·common.Config：ConfigEnvelope的配置部分；

·common.ConfigUpdate：ConfigUpdateEnvelope的一部分。

工具configtxlator提供的是标准的HTTP服务，与任何可以处理HTTP协议的工具都可以进行交互，下面用CURL工具来说明如何操作。

（1）Protocol Buffer格式转换为JSON格式

curl -X POST --data-binary @config.pb
http://$SERVER:$PORT/protolator/decode/<message.Name> > config.json

其中，config.pb是Protocol Buffer格式的文件，config.json是解码后的JSON格式文件。$SERVER和$PORT是configtxlator的服务和端口。<message.Name>是消息的格式，可以为common.Block等。

（2）JSON格式转换为Protocol Buffer格式

JSON格式文件是纯文本格式的，可以用任何的文本编辑工具修改，也可以用专门的JSON处理工具jq快速地编辑文件，比如下面的命令把最大区块大小修改为30个交易：

jq ".channel_group.groups.Orderer.values.BatchSize.value.max_message_count = 30"
config.json > updated_config.json

编辑完成后再提交给configtxlator，转换成对应的Protocol Buffer格式。

curl -X POST --data-binary @config.json http://$SERVER:$PORT/protolator/
encode/<message.Name> > config.pb

说明同上。

B.1.2　配置更新计算

配置修改后，提交差异的部分给排序服务可以减少传输量。提交原始的配置文件和修改后的配置文件给通道，如下所示：

curl -X POST -F channel=desiredchannel -F original=@original_config.pb -F
updated=@updated_config.pb
http://$SERVER:$PORT/configtxlator/compute/update-from-configs > config_update.pb

其中，desiredchannel是通道名称，original_config.pb和updated_config.pb分别是原始的配置文件和修改后的配置文件，config_update.pb是生成的差异配置文件，它是Protocol Buffer格式的common.ConfigUpdate结构。

如果调用SDK更新配置，应用程序可以直接对这个差异配置文件签名，然后封装成common.ConfigUpdateEnveloped，然后封装成common.Envelope提交给排序服务，结构的关系图如图B-1所示。

如果是命令行，在封装成common.Envelope之前需要先把config_update.pb解码成JSON格式：

curl -X POST --data-binary @config_update.pb
http://$SERVER:$PORT/protolator/decode/common.ConfigUpdate > config_update.json

构造common.Envelope结构：

echo
'{"payload":{"header":{"channel_header":{"channel_id":"desiredchannel",
"type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' > config_update_
as_envelope.json

图　B-1

再转换成Protocol Buffer格式：

curl -X POST --data-binary @config_update_as_envelope.json
http://$SERVER:$PORT/protolator/encode/common.Envelope > config_update_as_envelope.pb

调用命令行提交给排序节点：

peer channel update -f config_update_as_envelope.pb -c desiredchannel -o
$OSN_IP:$OSN_PORT

其中：$OSN_IP和$OSN_PORT是排序服务节点的地址和端口。

B.2　MSP生成工具cryptogen
cryptogen是Hyperledger Fabric提供的为网络实体生成加密材料（公私钥、证书等）的实用程序。这些证书代表一个身份，并允许在网络实体间通信和交易时进行签名和身份认证。

cryptogen使用一个包含网络拓扑的crypto-config.yaml文件，为文件中定义的组织和属于这些组织的实体生成一组证书和密钥。每个组织都配置了唯一的根证书（ca-cert），并包含了特定实体（peers和orders），这就形成了一种典型的网络结构——每个成员都有所属的CA。Hyperledger Fabric网络中的交易和通信都使用实体的私钥签名，使用公钥验证。

B.2.1　编译生成cryptogen工具
cryptogen源码在fabric/common/tools/cryptogen/中，是一个独立的可执行程序。

生成cryptogen可执行程序有两种方式。

1）在fabric/下执行make cryptogen，如果正常执行，则会在fabric/build/bin/下生成可执行文件cryptogen。

2）直接在fabric/common/tools/cryptogen/下执行go build。

B.2.2　cryptogen命令说明

执行cryptogen--help-long命令显示如下信息。

cryptogen [<flags>] <command> [<args> ...]

（1）超级账本密钥证书生成工具选项

·--help——显示帮助文档；

·--help-long——显示详细的帮助文档。

（2）命令

help [<command>...]

1）显示帮助文档。

generate [<flags>]

2）生成密钥证书。

·--config——指定配置文件，如果不指定，则使用默认配置（即cryptogen showtemplate中的内容）；

·--output——生成密钥证书的目录，默认为crypto-config。

3）showtemplate——显示配置模版。

4）version——显示版本信息。

最常用的命令语法是：

cryptogen generate --config=./crypto-config.yaml

根据crypto-config.yaml文件的配置，生成组织信息及其密钥证书等，保存在crypto-config目录下。

B.2.3　crypto-config.yaml文件解析

Crypto-config.yaml是cryptogen工具使用的配置文件，cryptogen工具根据该配置文件生成加密材料。但该文件名字并非固定，也可自定义，只需在cryptogen generate命令中指定对应文件即可。

下面我们分析一下该文件的内容：

# ------------------------------------------------------------------------------
# "OrdererOrgs" —— 定义排序服务节点的组织
# ------------------------------------------------------------------------------
OrdererOrgs:
# ---------------------------------------------------------------------------
# 排序服务节点的名称和域名
# --------------------------------------------------------------------------
- Name: Orderer
Domain: example.com
# ---------------------------------------------------------------------------
# "Specs" —— 手动定义节点名称，命名规范是:{{.Hostname}}.{{.Domain}}
# —— 如果没有定义CommonName，自动生成的节点名称是:orderer.example.com
# —— 如果定义了CommonName，就以CommonName为准
# —— Hostname可以定义多个
# ---------------------------------------------------------------------------
Specs:
- Hostname: orderer
# CommonName: order.example.net
# ------------------------------------------------------------------------------
# "PeerOrgs" —— 定义Peer节点的组织
# ------------------------------------------------------------------------------
PeerOrgs:
# ---------------------------------------------------------------------------
# 组织Org1的配置
# ---------------------------------------------------------------------------
- Name: Org1
Domain: org1.example.com
# ---------------------------------------------------------------------------
# "CA" —— 默认会生成的名称:ca.{{.Domain}}
# ---------------------------------------------------------------------------
# CA:
# Hostname: ca

# ---------------------------------------------------------------------------
# "Template" —— 按模版生成Peer节点MSP证书
# —— 默认生成的Peer节点名称:peer{{.Index}}.{{.Domain}}
# —— Index是从Start到Count-1，Start默认是0
# —— Hostname可以自定义节点名称规则
# ---------------------------------------------------------------------------
Template:
Count: 2
# Start: 5
# Hostname: {{.Prefix}}{{.Index}}
# ---------------------------------------------------------------------------
# "Users" —— 默认生成的用户数：User1@{{.Domain}}，User2@{{.Domain}}
# —— 默认生成1个管理员：Admin@{{.Domain}}
# ---------------------------------------------------------------------------
Users:
Count: 1
# ---------------------------------------------------------------------------
# 组织Org2的配置
# ---------------------------------------------------------------------------
- Name: Org2
Domain: org2.example.com
Template:
Count: 2
Users:
Count: 1

按照以上文件定义的内容，使用cryptogen generate命令生成的文件目录结构参考第8章的内容。

B.3　配置生成工具configtxgen
configtxgen是Hyperledger Fabric提供的用于通道配置的实用程序，主要生成以下3种文件：

·排序服务节点使用的创世区块；

·创建通道使用的通道配置交易；

·更新通道用的锚节点交易。

目前，该工具主要侧重于生成排序服务节点的创世区块，但是将来预计增加生成新通道的配置以及重新配置已有的通道。

B.3.1　编译生成configtxgen工具

Cryptogen源码在fabric/common/configtx/tool/configtxgen中，是一个独立的可执行程序。v1.0.0之后的版本，源码转到fabric/common/tools/cryptogen/中。

生成cryptogen可执行程序有两种方式。

1）在fabric目录下执行make configtxgen。如果正常执行，则会在fabric/build/bin中生成可执行文件configtxgen。

2）直接在fabric/common/configtx/tool/configtxgen下执行go build。

B.3.2　configtxgen命令说明

执行configtxgen--help命令显示如下信息。

（1）用法

-asOrg string

（2）组织名称

·-channelID string——通道名称，默认是"testchainid"；

·-inspectBlock string——检查和输出创世区块的内容；

·-inspectChannelCreateTx string——检查和输出通道创世区块的内容；

·-outputAnchorPeersUpdate string——创建锚节点配置，锚节点在configtix.yaml中的AnchorPeers中指定；

·-outputBlock string——创世区块生成的目录文件的名称，所用的Profile必须包含Consortiums，否则启动排序服务节点会失败；

·-outputCreateChannelTx string——通道创世区块生成的目录文件的名称，所用的Profile必须包含Application，否则创建通道会失败；

·-profile string——configtx.yaml中的Profiles配置项，它描述了如何生成组织信息，默认的名称是"SampleInsecureSolo"；

·-version——显示版本信息。

下面我们来看看命令configtxgen的常用用法。

（1）生成创世区块

生成创世区块的命令如下：

configtxgen -profile TwoOrgsOrdererGenesis
-outputBlock ./channel-artifacts/genesis.block

其中：

·TwoOrgsOrdererGenesis为configtx.yaml中的Profiles之一；

·./channel-artifacts/genesis.block为生成的创世区块文件名及保存路径。

生成的创世区块用于指定启动排序服务，还必须指定启动参数环境变量ORDERER_GENERAL_GENESISMETHOD=file和ORDERER_GENERAL_GENESISFILE=$PWD/genesis.block，或者修改配置文件orderer.yaml编辑这些属性值。

（2）生成通道创世区块

生成通道创世区块的命令如下：

configtxgen -profile TwoOrgsChannel
-outputCreateChannelTx ./channel-artifacts/channel.tx -channelID
$CHANNEL_NAME

其中：

·TwoOrgsChannel为configtx.yaml中的Profiles之一；

·./channel-artifacts/channel.tx为生成的交易文件名及保存路径；

·$CHANNEL_NAME为通道名称。

（3）生成组织锚节点

生成通道中的组织锚节点，如下所示：

configtxgen -profile TwoOrgsChannel
-outputAnchorPeersUpdate ./channel-artifacts/Org1MSPanchors.tx
-channelID CHANNEL_NAME -asOrg Org1MSP

其中：

·TwoOrgsChannel为configtx.yaml中的Profiles之一；

·./channel-artifacts/Org1MSPanchors.tx为生成锚节点的配置文件名及保存路径；

·$CHANNEL_NAME为通道名称；

·Org1MSP为组织名称。

（4）查看区块信息

查看生成的区块信息的示例如下所示：

configtxgen -profile TwoOrgsOrdererGenesis
-inspectBlock ./channel-artifacts/genesis.block

其中：./channel-artifacts/genesis.block为指定的区块文件，该命令会将指定的区块文件解析成可读JSON格式信息展示出来。

（5）查看通道配置信息

检查和查看通道创世区块的内容如下所示：

configtxgen -profile TwoOrgsOrdererGenesis
-inspectBlock ./channel-artifacts/genesis.block

其中：./channel-artifacts/genesis.block为指定的区块文件，该命令会将指定的区块文件解析成可读JSON格式信息展示出来。

B.3.3　configtx.yaml文件解析
configtxgen工具的配置参数主要由configtx.yaml文件提供。在fabric库中，配置文件在fabric/sampleconfig/configtx.yaml中。这个配置文件可以编辑，或者通过设置环境变量来重写属性值，如CONFIGTX_ORDERER_ORDERERTYPE=kafka。

此配置文件主要分为3部分。

1）Profiles部分。它是默认的，这部分包含一些用于开发或测试场景的示例配置，这些配置涉及fabric目录中的加密部分。configtxgen工具允许通过-profile标签来指定配置文件。Profiles部分可以显式声明所有配置，但是通常都是从默认配置中继承。

2）Organizations部分。它是默认的，这部分包含示例配置MSP定义的单一引用。对于生产部署，应该删除这部分配置，并以新网络成员的MSP定义来替代它。组织中每一个元素都必须带有锚标签，如&orgName，这些标签可以在Profiles中部分引用。

3）默认部分。此部分是Orderer和Application的配置，包括一些属性配置，如BatchTimeout和一般用作继承的基础值。

下面我们分析一下该文件的内容：

#######################################################################
# Profiles
#
# 可以编写不同的 profile 配置，作为参数给 configtxgen 工具使用
# 指定了Consortium（组合、集团、联盟）profile用来生成orderer的创世区块
# 带有正确联盟成员定义的orderer创世区块，其通道的创建请求必须带有组织成员名和联盟名
#
################################################################################
Profiles:
TwoOrgsOrdererGenesis:
Orderer:
<<: *OrdererDefaults
OrdererType: kafka
Organizations:
- *OrdererOrg
Consortiums:
SampleConsortium:
Organizations:
- *Org1
- *Org2
TwoOrgsChannel:
Consortium: SampleConsortium
Application:
<<: *ApplicationDefaults
Organizations:
- *Org1
- *Org2

#######################################################################
# Section: Organizations
#
# - 该部分定义了在这个配置文件中被引用的不同的组织标识
#
################################################################################
Organizations:

# SampleOrg 使用sampleconfig定义一个MSP。这个MSP不会在生产中使用，但可以临时使用
- &SampleOrg
# DefaultOrg 定义一个在开发环境中sampleconfig使用过的组织
Name: SampleOrg

# load MSP 使用的ID
ID: DEFAULT

# MSPDir 是MSP配置文件路径，由cryptogen工具生成的加密材料路径
MSPDir: msp

# AdminPrincipal 指定用于组织的管理员策略的主体类型
# 目前只能使用 Role.ADMIN 和 Role.MEMBER，分别代表主体类型为ADMIN和MEMBER
AdminPrincipal: Role.ADMIN

AnchorPeers:
# AnchorPeers 定义了可用于跨组织gossip通信的peer的位置
# 注意，这个值只有在Application下使用时才会编码进创世区块（即profile中
Application下引用这个组织）
- Host: 127.0.0.1
Port: 7051

- &OrdererOrg
Name: OrdererMSP
ID: OrdererMSP
MSPDir: crypto-config/ordererOrganizations/example.com/msp

- &Org1
Name: Org1MSP
ID: Org1MSP
MSPDir: crypto-config/peerOrganizations/org1.example.com/msp
AnchorPeers:
- Host: peer0.org1.example.com
Port: 7051

- &Org2
Name: Org2MSP
ID: Org2MSP
MSPDir: crypto-config/peerOrganizations/org2.example.com/msp
AnchorPeers:
- Host: peer0.org2.example.com
Port: 7051

################################################################################
#
# SECTION: Orderer
#
# - 该部分定义了编码到配置交易或创世区块中的与orderer相关的参数值
#
################################################################################
Orderer: &OrdererDefaults

# Orderer Type: orderer 的启动方式
# 可选类型是 "solo" 和 "kafka"
OrdererType: solo

Addresses:
#- 127.0.0.1:7050
- orderer.example.com:7050

# Batch Timeout: 创建batch的超时时间
BatchTimeout: 2s

# Batch Size: 控制batch到块中的消息数
BatchSize:

# Max Message Count: batch中允许的最大消息数量
MaxMessageCount: 10

# Absolute Max Bytes: batch中允许的绝对最大序列化消息字节数
# 如果OrdererType 是kafka，则要设置Kafka brokers的'message.max.bytes'和
'replica.fetch.max.bytes'的值大于该值
AbsoluteMaxBytes: 10 MB

# Preferred Max Bytes: batch中允许的首选绝对最大序列化消息字节数
# 如果一个消息大于该值，会导致batch 大于该值
PreferredMaxBytes: 512 KB

# Max Channels 是order网络中所允许的最大通道数
# 设为0表示无最大通道数的限制
MaxChannels: 0

Kafka:
# Brokers: orderer连接的Kafka broker列表
# 注意: 使用 IP:port 格式
Brokers:
- 127.0.0.1:9092

# Organizations 是作为网络中orderer侧参与者的组织列表
Organizations:

################################################################################
#
# SECTION: Application
#
# - 该部分定义了编码到配置交易或创世区块中的与APP相关的参数值
# 注意，在创建channel tx时才会用Application，否则是创建区块
#
################################################################################
Application: &ApplicationDefaults

# Organizations 是作为网络中APP侧参与者的组织列表
Organizations:

参考文献

[1] 　Satoshi Nakamoto.Bitcoin：A Peer-to-Peer Electronic Cash System[EB/OL].https://bitcoin.org/bitcoin.pdf .

[2] 　Gartner.Hype Cycle for EmergingTechnologies[EB/OL].http://www.gartner.com/document/3383817 .

[3] 　梅兰妮·斯万.区块链：新经济蓝图及导读[M].北京：新星出版社，2016.

[4] 　Wikipedia.Byzantine fault tolerance[EB/OL].https://en.wikipedia.org/wiki/Byzantine_fault_tolerance .

[5] 　Hyperledger.超级账本源代码[EB/OL].https://github.com/hyperledger/fabric .

[6]　Hyperledger.超级账本源代码[EB/OL].https://github.com/hyperledger/fabric-ca .

[7]　Hyperledger.超级账本源代码[EB/OL].https://github.com/hyperledger/fabric-baseimage .

[8]　Hyperledger.超级账本源代码[EB/OL].https://github.com/hyperledger/fabric-samples .

[9]　Hyperledger.超级账本项目管理[EB/OL].https://jira.hyperledger.org .

[10]　Hyperledger.超级账本文档[EB/OL].http://hyperledger-fabric.readthedocs.io .

[11]　智库.票据[EB/OL].http://wiki.mbalib.com/wiki/ 票据.

[12]　亿欧.以京东金融为例解析区块链数字票据[EB/OL].http://www.iyiou.com/p/36203 .