返回

Hyperledger Fabric文档v2.2(四)

入门及开发客户端应用程序

入门

参考博文:搭建 Fabric 测试网络

在我们开始之前,如果您还没有这样做,您可能希望检查您是否已经在将要开发区块链应用程序或运行 Hyperledger Fabric 的平台上安装了所有 准备阶段

安装必备组件后,即可下载并安装 HyperLedger Fabric。在我们为Fabric 二进制文件开发真正的安装程序的同时,我们提供了一个脚本,可以将 安装示例、二进制和 Docker 镜像 到您的系统中。该脚本还将 Docker 镜像下载到本地注册表。

在你下载完 Fabric 示例以及 Docker 镜像到你本机之后,您就可以跟着 使用Fabric的测试网络 教程开始使用 Fabric 了。

Hyperledger Fabric 智能合约(链码) API

Hyperledger Fabric 提供了不同编程语言的 API 来支持开发智能合约(链码)。智能合约 API 可以使用 Go、Node.js 和 Java:

Hyperledge Fabric 应用程序 SDK

Hyperledger Fabric 提供了许多 SDK 来支持各种编程语言开发应用程序。SDK 有支持 Node.js 和 Java 语言的:

此外,还有两个尚未正式发布的 SDK(Python 和 Go),但它们仍可供下载和测试:

目前,Node.js、Java 和 Go 支持 Hyperledge Fabric 1.4 提供的新的应用程序编程模型。

Hyperledger Fabric CA

Hyperledger Fabric 提供一个可选的 证书授权服务 ,您可以选择使用该服务生成证书和密钥材料,以配置和管理区块链网络中的身份。然而,任何可以生成 ECDSA 证书的 CA 都是可以使用的。

开发应用

本主题介绍如何使用 Hyperledger Fabric 开发客户端应用程序智能合约来解决业务问题。在现实世界的 商业票据 场景中,涉及多个组织,您将了解实现此目标所需的所有概念和任务。我们假设区块链网络已经可用。

该主题遵循典型的软件开发生命周期。它以业务需求开始,然后覆盖了为满足业务需求而开发一个应用和智能合约所需要的所有主要的技术活动。

可以通过运行商业票据 教程,按照简短的解释立即试用商业票据场景。当对教程中介绍的概念进行更全面的解释时,可以返回此主题。

场景

在本主题中,我们将会描述一个涉及六个组织的业务场景,这些组织使用基于 Hyperledger Fabric 构建的商业票据网络 PaperNet 来发行,购买和兑换商业票据。我们将使用该场景概述参与组织使用的商业票据应用程序和智能合约的开发要求。

PaperNet网络

PaperNet 是一个商业票据网络,允许适当授权的参与者发行,交易,兑换和估价商业票据。

PaperNet 商业票据网络。六个组织目前使用 PaperNet 网络发行购买出售兑换估价商业票据。MagentoCorp 发行和兑换商业票据。 DigiBank, BigFund,BrokerHouse 和 HedgeMatic 互相交易商业票据。RateM 为商业票据提供各种风险衡量标准。

让我们来看一下 MagnetoCorp 如何使用 PaperNet 和商业票据来帮助其业务。

演员介绍

MagnetoCorp 是一家备受推崇的公司,生产自动驾驶电动车。在 2020 年 4 月初,MagnetoCorp 公司赢得了大量订单,为 Daintree 公司制造 10,000 辆 D 型车,Daintree 是个人运输市场的新进入者。尽管该订单代表了 MagnetoCorp 公司的重大胜利,但在 MagnetoCorp 和 Daintree 正式达成协议后六个月,Daintree 将于 11 月 1 日开始交付之前不需要支付车辆费用。

为了制造这些车辆,MagnetoCorp 公司将需要雇佣 1000 名工人至少 6 个月。这对它的财务状况造成了短期压力,– 每月需要额外 500 万美元来支付这些新员工。商业票据是为帮助 MagnetoCorp 公司克服短期融资需求而设计的,– 每月基于 Daintree 公司开始支付 D 型车辆费用时就会有充足现金的预期来满足工资单。

在五月底,MagnetoCorp 公司需要 500 万美元才能满足 5 月 1 日雇佣的额外工人。要做到这个,它会发行一张面值 500 万美元的商业票据,未来6个月到期 – 当预计看到 Daintree 现金流时。DigiBank 认为 MagnetoCorp 公司是值得信赖的,因此,不需要高于中央银行 2% 基准利率的溢价,这将会使得今天价值 495 万美元,在6个月时间后价值 500 万美元。所以它以 494 万美元的价格购买了MagnetoCorp 公司 6 个月到期的商业票据 – 与 495 万美元的价值相比略有折扣。DigiBank 完全预计它将能够在 6 个月内从 MagnetoCorp 赎回 500 万美元,因此承担与此商业票据相关的风险增加,使其获利 1 万美元。

在六月底,MagnetoCorp 公司发行一个 500 万美元的商业票据来支付六月份的工资单时,被 BigFund 以 494 万美元购买。这是因为六月的商业条件与五月大致相同,导致 BigFund 以与 DigiBank 五月份相同的价格对 MagnetoCorp 商业票据进行估值。

接下来的每个月,MagnetoCorp 公司可以发行新的商业票据来满足它的工资义务,这些票据可以被 DigiBank 或其他任何在 PaperNet 商业票据网络的参与者购买 – BigFund, HedgeMatic 或 BrokerHouse。这些组织可能会根据两个因素为商业票据支付更多或更少的费用 – 央行基准利率和与 MagnetoCorp 相关的风险。后者取决于各种因素,如 D 型车的生产,以及评级机构 RateM 评估的 MagnetoCorp 公司的信誉度。

在 PaperNet 中的组织具有不同的角色,MagnetoCorp 发行票据,DigiBank、BigFund、HedgeMatic 和 BrokerHouse 交易票据,并且 RateM 评估票据。具有相同角色的组织,比如 DigiBank、Bigfund、HedgeMatic 和 BrokerHouse 是竞争对手。不同角色的组织没有必要是竞争对手,但是可能还是具有不同的商业利益,比如 MagentoCorp 想要给它的票据一个高的评估来卖出高价,然而 DigiBank 会从一个低的评估来获利,因此他们可以以一个地的价钱买入。我们能够看到,像 PaperNet 这样一个非常简单的网络,也可以有非常复杂的关系。一个区块链能够在彼此是竞争对手或者具有相反的商业利益并且可能造成争议的组织间帮助创建信任。其中 Fabric 能够创建更细粒度的信任关系。

让我们暂停 MagnetoCorp 的故事,开发 PaperNet 用于发行,购买,出售和兑换商业票据的客户应用程序和智能合约以获取组织之间的信任关系。稍后我们将回到评级机构 RateM 的角色。

分析

让我们更详细的分析商业票据。MagnetoCorp 和 DigiBank 等 PaperNet 参与者使用商业票据交易来实现其业务目标 – 让我们检查商业票据的结构以及随着时间推移影响票据结构的交易。我们还将根据网络中组织之间的信任关系,考虑 PaperNet 中的哪些组织需要签署交易。稍后我们将关注买家和卖家之间的资金流动情况; 现在,让我们关注 MagnetoCorp 发行的第一个票据。

商业票据生命周期

票据 00001 是 5 月 31 号由 MagnetoCorp 发行的。花点时间来看看该票据的第一个状态,它具有不同的属性和值:

1
2
3
4
5
6
7
Issuer = MagnetoCorp
Paper = 00001
Owner = MagnetoCorp
Issue date = 31 May 2020
Maturity = 30 November 2020
Face value = 5M USD
Current state = issued

该票据的状态是 发行 交易的结果,它使得 MagnetoCorp 公司的第一张商业票据面世!注意该票据在今年晚些时候如何兑换面值 500 万美元。当票据 00001 发行后 IssuerOwner 具有相同的值。该票据有唯一标识 MagnetoCorp00001——它是 Issuer 属性和 Paper 属性的组合。最后,属性 Current state = issued 快速识别了 MagnetoCorp 票据 00001 在它生命周期中的阶段。

发行后不久,该票据被 DigiBank 购买。花点时间来看看由于购买交易,同一个商业票据如何发生变化:

1
2
3
4
5
6
7
Issuer = MagnetoCorp
Paper = 00001
Owner = DigiBank
Issue date = 31 May 2020
Maturity date = 30 November 2020
Face value = 5M USD
Current state = trading

最重要的变化是 Owner 的改变——票据初始拥有者是 MagnetoCorp 而现在是 DigiBank。我们可以想象该票据后来如何被出售给 BrokerHouse 或 HedgeMatic,以及相应的变更为相应的 Owner。注意 Current state 允许我们轻松的识别该票据目前状态是 trading

6 个月后,如果 DigiBank 仍然持有商业票据,它就可以从 MagnetoCorp 那里兑换:

1
2
3
4
5
6
7
Issuer = MagnetoCorp
Paper = 00001
Owner = MagnetoCorp
Issue date = 31 May 2020
Maturity date = 30 November 2020
Face value = 5M USD
Current state = redeemed

最终的兑换交易结束了这个商业票据的生命周期——它可以被认为票据已经终止。通常必须保留已兑换的商业票据的记录,并且 redeemed 状态允许我们快速识别这些。通过将 Owner 跟交易创建者的身份进行比较,一个票据的 Owner 值可以被用来在兑换交易上进行访问控制。Fabric 通过 getCreator() chaincode API 来对此提供支持。如果使用 Go 语言的链码, 客户端身份链码库 来获取交易创建者额外的属性。

交易

我们已经看到票据 00001 的生命周期相对简单——由于发行购买兑换交易,它在 issued, tradingredeemed 状态之间转移。

这三笔交易由 MagnetoCorp 和 DigiBank(两次)发起,并推动了 00001 票据的状态变化。让我们更详细地看一下影响票据的交易:

发行

检查 MagnetoCorp 发起的第一笔交易:

1
2
3
4
5
6
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD

观察发行交易是如何具有属性和值的结构的。这个交易的结构跟票据 00001 的结构是不同的但是非常匹配的。那是因为他们是不同的事情——票据 00001 反应了 PaperNet state,是作为发行交易的结果。这是在发行交易背后的逻辑(我们是看不到的),它带着这些属性并且创建了票据。因为交易创建了票据,着意味着在这些结构之间具有着非常密切的关系。

在这个发行的交易中唯一被引入的组织是 MagnetoCorp。很自然的,MagnetoCorp 需要对交易进行签名。通常,一个票据的发行者会被要求在发行一个新的票据的交易上提供批准。

购买

接下来,检查购买交易,将票据 00001 的所有权从 MagnetoCorp 转移到 DigiBank:

1
2
3
4
5
6
7
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = MagnetoCorp
New owner = DigiBank
Purchase time = 31 May 2020 10:00:00 EST
Price = 4.94M USD

看一下购买交易如何减少票据中最终的属性。因为交易只能修改该票据。只有 New owner = DigiBank 改变了;其他所有的都是相同的。这没关系——关于购买交易最重要的是所有权的变更,事实上,在这次交易中,有一份对该票据当前所有者 MagnetoCorp 的认可。

你可能会奇怪为什么 Purchase timePrice 属性没有在票据 00001 中体现? 这要回到交易和票据之间的差异。494 万美元的价格标签实际上是交易的属性,而不是票据的属性。花点时间来思考一下这两者的不同;它并不像它看上去的那么明显。稍后我们会看到账本会记录这些信息片段——影响票据的所有交易历史,和它最新的状态。清楚这些信息分离非常重要。

同样值得注意的是票据 00001 可能会被买卖多次。尽管在我们的场景中略微跳过了一点,我们来检查一下如果票据 00001 变更了所有者我们可能会看到什么。

如果 BigFund 购买:

1
2
3
4
5
6
7
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = DigiBank
New owner = BigFund
Purchase time = 2 June 2020 12:20:00 EST
Price = 4.93M USD

接着由 HedgeMatic 购买:

1
2
3
4
5
6
7
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = BigFund
New owner = HedgeMatic
Purchase time = 3 June 2020 15:59:00 EST
Price = 4.90M USD

看看票据所有者如何变化,以及在我们的例子中,价格如何变化。你能想到 MagnetoCorp 商业票据价格会降低的原因吗?

很显然,购买交易要求售卖组织和购买组织都要对该交易进行离线签名,这是证明参与交易的双方共同同意该交易的证据。

兑换

票据 00001 兑换交易代表了它生命周期的结束。在我们相对简单的例子中,HedgeMatic 启动将商业票据转回 MagnetoCorp 的交易:

1
2
3
4
5
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Current owner = HedgeMatic
Redeem time = 30 Nov 2020 12:00:00 EST

再次注意兑换交易有更少的属性;票据 00001 所有更改都可以通过兑换交易逻辑计算数据:Issuer 将成为新的所有者,Current state 将变成 redeemed。在我们的例子中指定了 Current owner 属性,以便可以针对当前的票据持有者进行检查。

从信任的角度来说,跟购买交易一样,也可以应用到兑换交易中:参与交易的双方组织都需要对它离线签名。

账本

在本主题中,我们已经看到交易产生的票据状态是 PaperNet 中两个重要的概念。的确,我们将会在任何一个 Hyperledger Fabric 分布式账本中看到这些基本元素——包含了当前所有对象最新状态的世界状态和记录了所有交易历史并能归集出最新世界状态的区块链

在交易上必须的批准通过规则被强制执行,这个在一个交易被附加到账本之前被评估。只有当需要的签名被展示的时候,Fabric 才会接受一个交易作为有效的交易。

你现在处在一个很棒的地方,将这些想法转化为智能合约。如果您的编程有点生疏,请不要担心,我们将提供了解程序代码的提示和指示。掌握商业票据智能合约是设计自己的应用程序的第一个重要步骤。或者,你是一个有一点编程经验的业务分析师,不要害怕继续深入挖掘!

流程和数据设计

本主题主要讨论在 PaperNet 中如何设计商业票据流程和与它相关的数据结构。我们在 分析章节 中已经强调使用状态和交易对 PaperNet 建模提供了一种精确的方法来了解正在发生的事情。我们现在将详细阐述这两个强烈相关的概念,以帮助我们随后设计 PaperNet 的智能合约和应用程序。

生命周期

正如我们所见,在处理商业票据时有两个重要的概念:状态交易。实际上,所有区块链用例都是如此;状态建模是重要的概念对象,其生命周期转换交易描述。对状态和交易的有效分析是成功实施的重要起点。

我们可以用状态转移表来表示商业票据的生命周期:

商业票据的状态转移表。商业票据通过发行购买兑换交易在已发行交易中已兑换之间进行状态转移。

了解状态图如何描述商业票据随着时间如何变化,以及特定交易如何控制生命周期转换。在 Hypledger Fabric 中,智能合约实现了在不同状态之间转换商业票据的交易逻辑。商业票据状态实际上是保存在帐本的世界状态中; 让我们来深入了解一下。

账本状态

回想一下商业票据的结构:

商业票据可以被表示为属性集,每个属性都对应一个值。通常,这些属性的组合会为每个票据提供一个唯一键

商业票据的 Paper 属性的值是 00001Face value 属性的值是 5M USD。更重要的是, Current state 属性表示了商业票据是 issued 状态,trading 状态还是 redeemed 状态。 结合来看,属性的完整集合构成了商业票据的状态。此外,这些商业票据的全部集合构成了账本的 世界状态

所有的账本状态都是这种形式;每个状态都是一个属性集,每个都有不同的值。状态的多属性是一个强大的特性——允许我们把 Fabric 的状态看做是一个向量而不是一个简单的标量。然后,我们把整个实际的对象当做独立的状态,随后由交易逻辑控制状态转换。Fabric 的状态是由键值对实现的,其中值以捕获对象的多个属性的格式编码对象属性,通常是 JSON 格式。根据这些属性,账本数据库 可以支持高级的查询操作,这对于复杂的对象检索非常有帮助。

查看 MagnetoCorp 的票据 00001 如何表示为一个状态向量,根据不同的交易刺激进行转换:

商业票据状态是由于不同的交易而产生和过渡的。Hyperledger Fabric 状态拥有多个属性,使他们成为向量而不是标量。

注意每个独立的票据都起于空状态,技术上被称作 nil,来表示票据不存在!通过发行交易,票据 00001 问世,然后由于购买兑换交易而更新状态。

注意每个状态是如何自描述的;每个属性都有一个名字和值。尽管目前所有的商业票据都有相同的属性,这种情况不一定总是如此,而 Hyperledger Fabric 支持不同的状态有不同的属性。这允许相同的帐本世界状态包含相同资产的不同形式以及不同类型的资产。同样使得更新状态结构成为可能;试想有一个新的规则需要一个额外的数据字段。灵活的状态属性集支持数据演化的基本需求。

状态键值

大多数的实际应用中,状态会有一个属性组合在给定的上下文中唯一识别它——它就是主键。PaperNet 商业票据的主键是通过 Issuer 属性和 paper 属性拼接得到的;所以 MagnetoCorp 的第一个票据的主键就是 MagnetoCorp00001

状态的主键允许我们唯一识别一个票据;它是通过发行交易创建的,然后由购买兑换更新。Hyperledger Fabric 需要账本中的每个状态都有唯一的主键。

当唯一主键在可用的属性集中不能获得,应用决定的唯一键会被指定为交易的输入来创建状态。这个唯一键的形式一般是 UUID,尽管可读性不好,但是是一个很好的实践。最重要的是账本中每个独立的状态对象都必须有一个唯一键

Note: 在主键中你应该避免使用 U+0000 (nil byte)。

多个状态

正如我们所见,PaperNet 中的商业票据作为状态向量被存储在账本中。能够从账本中查询不同的商业票据是合理的需求;比如,查询所有由 MagnetoCorp 发行的的票据,或者查询所有由 MagnetoCorp 发行且处在 redeemed 状态的票据。

为了满足不同类型的查询任务,把所有相关的商业票据按逻辑顺序排列在一起是很有帮助的。PaperNet 的设计包含了商业票据列表的思想——一个逻辑容器,每当商业票据发行或发生其他更改时,该容器都会更新。

逻辑表示

把所有的 PaperNet 商业票据放在一个商业票据列表中是有帮助的

MagnetoCorp 新增加的票据 00004 被加入到已有的商业票据列表中。

新票据由于发行交易被加入到列表中,然后列表中已存在的票据因为购买交易和兑换交易可以被更新状态。列表有一个描述性的名称:org.papernet.papers;使用这种 DNS 名真的是一个好主意,因为适当的名称会让你的区块链设计对其他人来说是直观的。这种想法同样也适用于智能合约的名字

物理表现

我们可以正确地想到 PaperNet 中的单个票据列表—— org.papernet.papers ——列表最好作为一组单独的 Fabric 状态来实现,其复合键将状态与其列表关联起来。这样,每个状态的复合键都是惟一的,并支持有效的列表查询

将 PaperNet 商业票据列表表示为一组不同的 Hyperledger Fabric 状态

注意列表中的每个票据都是如何用向量状态表示的,其中唯一的组合键是由 org.papernet.paper 的属性 IssuerPaper 连接而成的。这种结构有两个好处:

  • 允许我们检查账本中的任意状态向量在哪个列表中,不用引用到不同的列表。这类似于观察一群体育迷,通过他们穿的衬衫的颜色来判断他们支持哪支球队。体育迷们自我宣布他们的忠诚;我们不需要粉丝名单。
  • Hyperlegder Fabric 内部使用了一个并发控制机制来更新账本,所以把票据保存在不同的状态向量中大大减少了共享状态碰撞的机会。这种碰撞需要交易重新提交,复杂化了应用设计,而且降低了性能。

第二点是 Hyperledger Fabric 的关键;状态向量的物理设计对于优化性能和行为非常重要。保持状态的独立!

信任关系

我们已经讨论了网络中的不同角色,如发行者,交易员或评级机构,以及不同的商业利益如何决定谁需要签署交易。在 Fabric 中,这些规则由所谓的背书策略捕获。这些规则可以在链码粒度上设置,也可以为单个状态键设置。

这意味着在 PaperNet 中,我们可以为整个命名空间设置一个规则,以确定哪些组织可以发行新票据。然后,可以为单个票据设置和更新规则,以捕获购买和兑换交易的信任关系。

在下一个主题中,我们将向您展示如何结合这些设计概念来实现 PaperNet 商业票据智能合约,然后是应用程序来使用它!

智能合约处理

这部分有点老旧,没有使用Go语言,大概了解下流程就行

区块链网络的核心是智能合约。在 PaperNet 中,商业票据智能合约中的代码定义了商业票据的有效状态,以及将票据从一种状态状态转变为另一种状态的交易逻辑。在本主题中,我们将向您展示如何实现一个真实世界的智能合约,该合约管理发行、购买和兑换商业票据的过程。

如果您愿意,可以下载示例,甚至可以在本地运行。它是用 JavaScript 和 Java 编写的,但逻辑与语言无关,因此您可以轻松地查看正在发生的事情!(该示例也可用于 Go。)

智能合约

智能合约定义业务对象的不同状态,并管理对象在不同状态之间变化的过程。智能合约很重要,因为它们允许架构师和智能合约开发人员定义在区块链网络中协作的不同组织之间共享的关键业务流程和数据

在 PaperNet 网络中,智能合约由不同的网络参与者共享,例如 MagnetoCorp 和 DigiBank。 连接到网络的所有应用程序必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。

实现语言

支持两种运行时,Java 虚拟机和 Node.js。支持使用 JavaScript、TypeScript、Java 或其他可以运行在支持的运行时上其中一种语言。

在 Java 和 TypeScript 中,标注或者装饰器用来为智能合约和它的结构提供信息。这就更加丰富了开发体验——比如,作者信息或者强调返回类型。使用 JavaScript 的话就必须遵守一些规范,同时,对于什么可以自动执行也有一些限制。

这里给出的示例包括 JavaScript 和 Java 两种语言。(也可用 Go 实现)

合约类

PaperNet 商业票据智能合约的副本包含在单个文件中。如果您已下载,请使用浏览器或在您喜欢的编辑器中打开它。

您可能会从文件路径中注意到这是 MagnetoCorp 的智能合约副本。 MagnetoCorp 和 DigiBank 必须同意他们将要使用的智能合约版本。现在,你看哪个组织的合约副本无关紧要,它们都是一样的。

花一些时间看一下智能合约的整体结构; 注意,它很短!在文件的顶部,您将看到商业票据智能合约的定义

JavaScript

1
class CommercialPaperContract extends Contract {...}

Java

1
2
3
@Contract(...)
@Default
public class CommercialPaperContract implements ContractInterface {...}

CommercialPaperContract 类中包含商业票据中交易的定义——发行购买兑换。这些交易带给了商业票据创建和在它们的生命周期中流动的能力。我们马上会查看这些交易,但是现在我们需要关注一下 JavaScript, CommericalPaperContract 扩展的 Hyperledger Fabric Contract

在 Java 中,类必须使用 @Contract(...) 标注进行包装。它支持额外的智能合约信息,比如许可和作者。 @Default() 标注表明该智能合约是默认合约类。在智能合约中标记默认合约类在一些有多个合约类的智能合约中会很有用。

如果你使用 TypeScript 实现,也有类似 @Contract(...) 的标注,和 Java 中功能相似。

关于可用的标注的更多信息,请查看 API 文档:

我们先导入这些类、标注和 Context 类:

JavaScript

1
const { Contract, Context } = require('fabric-contract-api');

Java

1
2
3
4
5
6
7
8
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.Contact;
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Info;
import org.hyperledger.fabric.contract.annotation.License;
import org.hyperledger.fabric.contract.annotation.Transaction;

我们的商业票据合约将使用这些类的内置功能,例如自动方法调用,每个交易上下文交易处理器,和类共享状态。

还要注意 JavaScript 类构造函数如何使用其超类通过一个命名空间来初始化自身:

1
2
3
constructor() {
    super('org.papernet.commercialpaper');
}

在 Java 类中,构造器是空的,合约名会通过 @Contract() 注解进行识别。如果不是就会使用类名。

最重要的是,org.papernet.commercialpaper 非常具有描述性——这份智能合约是所有 PaperNet 组织关于商业票据商定的定义。

通常每个文件只有一个智能合约(合约往往有不同的生命周期,这使得将它们分开是明智的)。但是,在某些情况下,多个智能合约可能会为应用程序提供语法帮助,例如 EuroBondDollarBondYenBond 但基本上提供相同的功能。在这种情况下,智能合约和交易可以消除歧义。

交易定义

在类中定位 issue 方法。

JavaScript

1
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}

Java

1
2
3
4
5
6
7
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx,
                             String issuer,
                             String paperNumber,
                             String issueDateTime,
                             String maturityDateTime,
                             int faceValue) {...}

Java 标注 @Transaction 用于标记该方法为交易定义;TypeScript 中也有等价的标注。

无论何时调用此合约来发行商业票据,都会调用该方法。回想一下如何使用以下交易创建商业票据 00001:

1
2
3
4
5
6
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD

我们已经更改了编程样式的变量名称,但是看看这些属性几乎直接映射到 issue 方法变量。

只要应用程序请求发行商业票据,合约就会自动调用 issue 方法。交易属性值通过相应的变量提供给方法。使用示例应用程序,了解应用程序如何使用应用主题中的 Hyperledger Fabric SDK 提交一笔交易。

您可能已经注意到 issue 方法中定义的一个额外变量 ctx。它被称为交易上下文,它始终是第一个参数。默认情况下,它维护与交易逻辑相关的每个合约和每个交易的信息。例如,它将包含 MagnetoCorp 指定的交易标识符,MagnetoCorp 可以发行用户的数字证书,也可以调用账本 API。

通过实现自己的 createContext() 方法而不是接受默认实现,了解智能合约如何扩展默认交易上下文:

JavaScript

1
2
3
createContext() {
  return new CommercialPaperContext()
}

Java

1
2
3
4
@Override
public Context createContext(ChaincodeStub stub) {
     return new CommercialPaperContext(stub);
}

此扩展上下文将自定义属性 paperList 添加到默认值:

JavaScript

1
2
3
4
5
6
7
class CommercialPaperContext extends Context {

  constructor() {
    super();
    // All papers are held in a list of papers
    this.paperList = new PaperList(this);
}

Java

1
2
3
4
5
6
7
class CommercialPaperContext extends Context {
    public CommercialPaperContext(ChaincodeStub stub) {
        super(stub);
        this.paperList = new PaperList(this);
    }
    public PaperList paperList;
}

我们很快就会看到 ctx.paperList 如何随后用于帮助存储和检索所有 PaperNet 商业票据。

为了巩固您对智能合约交易结构的理解,找到购买兑换交易定义,看看您是否可以理解它们如何映射到相应的商业票据交易。

购买交易:

1
2
3
4
5
6
7
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = MagnetoCorp
New owner = DigiBank
Purchase time = 31 May 2020 10:00:00 EST
Price = 4.94M USD

JavaScript

1
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...}

Java

1
2
3
4
5
6
7
8
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx,
                           String issuer,
                           String paperNumber,
                           String currentOwner,
                           String newOwner,
                           int price,
                           String purchaseDateTime) {...}

兑换交易:

1
2
3
4
5
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Redeemer = DigiBank
Redeem time = 31 Dec 2020 12:00:00 EST

JavaScript

1
async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...}

Java

1
2
3
4
5
6
@Transaction
public CommercialPaper redeem(CommercialPaperContext ctx,
                              String issuer,
                              String paperNumber,
                              String redeemingOwner,
                              String redeemDateTime) {...}

在两个案例中,注意商业票据交易和智能合约方法调用之间 1:1 的关系。

所有 JavaScript 方法都使用 asyncawait 关键字

交易逻辑

现在您已经了解了合约的结构和交易的定义,下面让我们关注智能合约中的逻辑。

回想一下第一个发行交易:

1
2
3
4
5
6
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD

它导致 issue 方法被传递调用:

JavaScript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {

   // create an instance of the paper
  let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);

  // Smart contract, rather than paper, moves paper into ISSUED state
  paper.setIssued();

  // Newly issued paper is owned by the issuer
  paper.setOwner(issuer);

  // Add the paper to the list of all similar commercial papers in the ledger world state
  await ctx.paperList.addPaper(paper);

  // Must return a serialized paper to caller of smart contract
  return paper.toBuffer();
}

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx,
                              String issuer,
                              String paperNumber,
                              String issueDateTime,
                              String maturityDateTime,
                              int faceValue) {

    System.out.println(ctx);

    // create an instance of the paper
    CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
            faceValue,issuer,"");

    // Smart contract, rather than paper, moves paper into ISSUED state
    paper.setIssued();

    // Newly issued paper is owned by the issuer
    paper.setOwner(issuer);

    System.out.println(paper);
    // Add the paper to the list of all similar commercial papers in the ledger
    // world state
    ctx.paperList.addPaper(paper);

    // Must return a serialized paper to caller of smart contract
    return paper;
}

逻辑很简单:获取交易输入变量,创建新的商业票据 paper,使用 paperList 将其添加到所有商业票据的列表中,并将新的商业票据(序列化为buffer)作为交易响应返回。

了解如何从交易上下文中检索 paperList 以提供对商业票据列表的访问。issue()buy()redeem() 不断重新访问 ctx.paperList 以使商业票据列表保持最新。

购买交易的逻辑更详细描述:

JavaScript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) {

  // Retrieve the current paper using key fields provided
  let paperKey = CommercialPaper.makeKey([issuer, paperNumber]);
  let paper = await ctx.paperList.getPaper(paperKey);

  // Validate current owner
  if (paper.getOwner() !== currentOwner) {
      throw new Error('Paper ' + issuer + paperNumber + ' is not owned by ' + currentOwner);
  }

  // First buy moves state from ISSUED to TRADING
  if (paper.isIssued()) {
      paper.setTrading();
  }

  // Check paper is not already REDEEMED
  if (paper.isTrading()) {
      paper.setOwner(newOwner);
  } else {
      throw new Error('Paper ' + issuer + paperNumber + ' is not trading. Current state = ' +paper.getCurrentState());
  }

  // Update the paper
  await ctx.paperList.updatePaper(paper);
  return paper.toBuffer();
}

Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx,
                           String issuer,
                           String paperNumber,
                           String currentOwner,
                           String newOwner,
                           int price,
                           String purchaseDateTime) {

    // Retrieve the current paper using key fields provided
    String paperKey = State.makeKey(new String[] { paperNumber });
    CommercialPaper paper = ctx.paperList.getPaper(paperKey);

    // Validate current owner
    if (!paper.getOwner().equals(currentOwner)) {
        throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner);
    }

    // First buy moves state from ISSUED to TRADING
    if (paper.isIssued()) {
        paper.setTrading();
    }

    // Check paper is not already REDEEMED
    if (paper.isTrading()) {
        paper.setOwner(newOwner);
    } else {
        throw new RuntimeException(
                "Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState());
    }

    // Update the paper
    ctx.paperList.updatePaper(paper);
    return paper;
}

在使用 paper.setOwner(newOwner) 更改拥有者之前,理解交易如何检查 currentOwner 并检查该 paper 应该是 TRADING 状态的。基本流程很简单:检查一些前提条件,设置新拥有者,更新账本上的商业票据,并将更新的商业票据(序列化为 buffer )作为交易响应返回。

为什么不看一下你否能理解兑换交易的逻辑?

对象的表示

我们已经了解了如何使用 CommercialPaperPaperList 类定义和实现发行购买兑换交易。让我们通过查看这些类如何工作来结束这个主题。

定位到 CommercialPaper 类:

JavaScript

In the paper.js file:

1
class CommercialPaper extends State {...}

Java

In the CommercialPaper.java file:

1
2
@DataType()
public class CommercialPaper extends State {...}

该类包含商业票据状态的内存表示。了解 createInstance 方法如何使用提供的参数初始化一个新的商业票据:

JavaScript

1
2
3
static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
  return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue });
}

Java

1
2
3
4
5
public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime,
        String maturityDateTime, int faceValue, String owner, String state) {
    return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime)
            .setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(owner).setState(state);
}

回想一下发行交易如何使用这个类:

JavaScript

1
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);

Java

1
2
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
        faceValue,issuer,"");

查看每次调用发行交易时,如何创建包含交易数据的商业票据的新内存实例。

需要注意的几个要点:

  • 这是一个内存中的表示; 我们稍后会看到它如何在帐本上显示。
  • CommercialPaper 类扩展了 State 类。 State 是一个应用程序定义的类,它为状态创建一个公共抽象。所有状态都有一个它们代表的业务对象类、一个复合键,可以被序列化和反序列化,等等。当我们在帐本上存储多个业务对象类型时, State 可以帮助我们的代码更清晰。检查 state.js 文件中的 State 类。
  • 票据在创建时会计算自己的密钥,在访问帐本时将使用此密钥。密钥由 issuerpaperNumber 的组合形成:
1
2
3
4
constructor(obj) {
  super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]);
  Object.assign(this, obj);
}
  • 票据通过交易而不是票据类变更到 ISSUED 状态。那是因为智能合约控制票据的状态生命周期。例如,import 交易可能会立即创建一组新的 TRADING 状态的票据。

CommercialPaper 类的其余部分包含简单的辅助方法:

1
2
3
getOwner() {
    return this.owner;
}

回想一下智能合约如何使用这样的方法来维护商业票据的整个生命周期。例如,在兑换交易中,我们看到:

1
2
3
4
if (paper.getOwner() === redeemingOwner) {
  paper.setOwner(paper.getIssuer());
  paper.setRedeemed();
}

访问账本

现在在 paperlist.js 文件中找到 PaperList 类:

1
class PaperList extends StateList {

此工具类用于管理 Hyperledger Fabric 状态数据库中的所有 PaperNet 商业票据。PaperList 数据结构在架构主题中有更详细的描述。

CommercialPaper 类一样,此类扩展了应用程序定义的 StateList 类,该类为一系列状态创建了一个通用抽象——在本例中是 PaperNet 中的所有商业票据。

addPaper() 方法是对 StateList.addState() 方法的简单封装:

1
2
3
async addPaper(paper) {
  return this.addState(paper);
}

您可以在 StateList.js 文件中看到 StateList 类如何使用 Fabric API putState() 将商业票据作为状态数据写在帐本中:

1
2
3
4
5
async addState(state) {
  let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
  let data = State.serialize(state);
  await this.ctx.stub.putState(key, data);
}

帐本中的每个状态数据都需要以下两个基本要素:

  • 键(Key): createCompositeKey() 使用固定名称和 state 密钥形成。在构造 PaperList 对象时分配了名称,state.getSplitKey() 确定每个状态的唯一键。
  • 数据(Data): 数据 只是商业票据状态的序列化形式,使用 State.serialize() 方法创建。State 类使用 JSON 对数据进行序列化和反序列化,并根据需要使用 State 的业务对象类,在我们的例子中为 CommercialPaper,在构造 PaperList 对象时再次设置。

注意 StateList 不存储有关单个状态或状态总列表的任何内容——它将所有这些状态委托给 Fabric 状态数据库。这是一个重要的设计模式 – 它减少了 Hyperledger Fabric 中账本 MVCC 冲突的机会。

StateList getState()updateState() 方法以类似的方式工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async getState(key) {
  let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key));
  let data = await this.ctx.stub.getState(ledgerKey);
  let state = State.deserialize(data, this.supportedClasses);
  return state;
}
async updateState(state) {
  let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
  let data = State.serialize(state);
  await this.ctx.stub.putState(key, data);
}

了解他们如何使用 Fabric APIs putState()getState()createCompositeKey() 来存取账本。我们稍后将扩展这份智能合约,以列出 paperNet 中的所有商业票据。实现账本检索的方法可能是什么样的?

是的!在本主题中,您已了解如何为 PaperNet 实现智能合约。您可以转到下一个子主题,以查看应用程序如何使用 Fabric SDK 调用智能合约。

应用程序

这部分有点老旧,没有使用Go语言,大概了解下流程就行

应用程序可以通过将交易提交到帐本或查询帐本内容来与区块链网络进行交互。本主题介绍了应用程序如何执行此操作的机制; 在我们的场景中,组织使用应用程序访问 PaperNet,这些应用程序调用定义在商业票据智能合约中的发行购买兑换交易。尽管 MagnetoCorp 的应用发行商业票据是基础功能,但它涵盖了所有主要的理解点。

在本主题中,我们将介绍:

为了帮助您理解,我们将参考 Hyperledger Fabric 提供的商业票据示例应用程序。您可以下载在本地运行它。它是用 JavaScript 和 Java 编写的,但逻辑与语言无关,因此您可以轻松地查看正在发生的事情!(该示例也适用于 Go。)

基本流程

应用程序使用 Fabric SDK 与区块链网络交互。 以下是应用程序如何调用商业票据智能合约的简化图表:

develop.application PaperNet 应用程序调用商业票据智能合约来提交发行交易请求。

应用程序必须遵循六个基本步骤来提交交易:

  • 从钱包中选择一个身份
  • 连接到网关
  • 访问所需的网络
  • 构建智能合约的交易请求
  • 将交易提交到网络
  • 处理响应

您将看到典型应用程序如何使用 Fabric SDK 执行这六个步骤。 您可以在 issue.js 文件中找到应用程序代码。请在浏览器中查看,如果您已下载,请在您喜欢的编辑器中打开它。 花一些时间看一下应用程序的整体结构; 尽管有注释和空白,但是它只有100行代码!

钱包

issue.js 的顶部,您将看到两个 Fabric 类导入代码域:

1
const { Wallets, Gateway } = require('fabric-network');

您可以在node SDK 文档中了解 fabric-network 类,但是现在,让我们看看如何使用它们将 MagnetoCorp 的应用程序连接到 PaperNet。该应用程序使用 Fabric Wallet 类,如下所示:

1
const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet');

了解 wallet 如何在本地文件系统中找到钱包。从钱包中检索到的身份显然适用于使用 issue 应用程序的 Isabella 用户。钱包拥有一组身份——X.509 数字证书——可用于访问 PaperNet 或任何其他 Fabric 网络。如果您运行该教程,并查看此目录,您将看到 Isabella 的身份凭证。

想想一个钱包里面装着政府身份证,驾照或 ATM 卡的数字等价物。其中的 X.509 数字证书将持有者与组织相关联,从而使他们有权在网络通道中获得权利。例如, Isabella 可能是 MagnetoCorp 的管理员,这可能比其他用户更有特权——来自 DigiBank 的 Balaji。 此外,智能合约可以在使用交易上下文的智能合约处理期间检索此身份。

另请注意,钱包不持有任何形式的现金或代币——它们持有身份

网关

第二个关键类是 Fabric Gateway。最重要的是,网关识别一个或多个提供网络访问的 Peer 节点——在我们的例子中是 PaperNet。了解 issue.js 如何连接到其网关:

1
await gateway.connect(connectionProfile, connectionOptions);

gateway.connect() 有两个重要参数:

  • connectionProfile连接配置文件的文件系统位置,用于将一组 Peer 节点标识为 PaperNet 的网关
  • connectionOptions:一组用于控制 issue.js 与 PaperNet 交互的选项

了解客户端应用程序如何使用网关将自身与可能发生变化的网络拓扑隔离开来。网关负责使用连接配置文件连接选项将交易提案发送到网络中的正确 Peer 节点。

花一些时间检查连接配置文件 ./gateway/connectionProfile.yaml。它使用YAML,易于阅读。

它被加载并转换为 JSON 对象:

1
let connectionProfile = yaml.safeLoad(file.readFileSync('./gateway/connectionProfile.yaml', 'utf8'));

现在,我们只关注 channels:peers: 配置部分:(我们稍微修改了细节,以便更好地解释发生了什么。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
channels:
  papernet:
    peers:
      peer1.magnetocorp.com:
        endorsingPeer: true
        eventSource: true

      peer2.digibank.com:
        endorsingPeer: true
        eventSource: true

peers:
  peer1.magnetocorp.com:
    url: grpcs://localhost:7051
    grpcOptions:
      ssl-target-name-override: peer1.magnetocorp.com
      request-timeout: 120
    tlsCACerts:
      path: certificates/magnetocorp/magnetocorp.com-cert.pem

  peer2.digibank.com:
    url: grpcs://localhost:8051
    grpcOptions:
      ssl-target-name-override: peer1.digibank.com
    tlsCACerts:
      path: certificates/digibank/digibank.com-cert.pem

看一下 channel: 如何识别 PaperNet: 网络通道及其两个 Peer 节点。MagnetoCorp 拥有 peer1.magenetocorp.com,DigiBank 拥有 peer2.digibank.com,两者都有背书节点的角色。通过 peers: 键链接到这些 Peer 节点,其中包含有关如何连接它们的详细信息,包括它们各自的网络地址。

连接配置文件包含大量信息——不仅仅是 Peer 节点——而是网络通道,网络排序节点,组织和 CA,因此如果您不了解所有信息,请不要担心!

现在让我们将注意力转向 connectionOptions 对象:

1
2
3
4
5
let connectionOptions = {
    identity: userName,
    wallet: wallet,
    discovery: { enabled:true, asLocalhost: true }
};

了解它如何指定应使用 identity、userName 和 wallet、wallet 连接到网关。 这些是在代码中分配值较早的。

应用程序可以使用其他连接选项来指示 SDK 代表它智能地执行操作。 例如:

1
2
3
4
5
6
7
8
let connectionOptions = {
  identity: userName,
  wallet: wallet,
  eventHandlerOptions: {
    commitTimeout: 100,
    strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
  },
}

这里, commitTimeout 告诉 SDK 等待100秒以监听是否已提交交易。 strategy:EventStrategies.MSPID_SCOPE_ANYFORTX 指定 SDK 可以在单个 MagnetoCorp Peer 节点确认交易后通知应用程序,与 strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX 相反,strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX 要求 MagnetoCorp 和 DigiBank 的所有 Peer 节点确认交易。

如果您愿意,请阅读更多 有关连接选项如何允许应用程序指定面向目标的行为而不必担心如何实现的信息。

网络通道

在网关 connectionProfile.yaml 中定义的 Peer 节点提供 issue.js 来访问 PaperNet。 由于这些 Peer 节点可以连接到多个网络通道,因此网关实际上为应用程序提供了对多个网络通道的访问!

了解应用程序如何选择特定通道:

1
const network = await gateway.getNetwork('PaperNet');

从这一点开始, network 将提供对 PaperNet 的访问。 此外,如果应用程序想要访问另一个网络,BondNet,同时,它很容易:

1
const network2 = await gateway.getNetwork('BondNet');

现在,我们的应用程序可以访问第二个网络 BondNet,同时可以访问 PaperNet

我们在这里可以看到 Hyperledger Fabric 的一个强大功能——应用程序可以通过连接到多个网关 Peer 节点来加入网络中的网络,每个网关 Peer 节点都连接到多个网络通道。 根据 gateway.connect() 提供的钱包标识,应用程序将在不同的通道中拥有不同的权限。

构造请求

该应用程序现在准备发行商业票据。要做到这一点,它将再次使用 CommercialPaperContract,它可以非常直接地访问这个智能合约:

1
const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper');

请注意应用程序如何提供名称——papercontract——以及可选的合约命名空间: org.papernet.commercialpaper! 我们看到如何从包含许多合约的 papercontract.js 链码文件中选出一个合约名称。在 PaperNet 中,papercontract.js 已安装并使用名称 papercontract 部署到了通道,如果您有兴趣,可以看如何部署包含多个智能合约的链代码。

如果我们的应用程序同时需要访问 PaperNet 或 BondNet 中的另一个合约,这将很容易:

1
2
3
const euroContract = await network.getContract('EuroCommercialPaperContract');

const bondContract = await network2.getContract('BondContract');

在这些例子中,注意我们是如何不使用一个有效的合约名字——每个文件我们只有一个智能合约,并且 getContract() 将会使用它找到的第一个合约。

回想一下 MagnetoCorp 用于发行其第一份商业票据的交易:

1
2
3
4
5
6
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD

我们现在将此交易提交给 PaperNet!

提交交易

提交一个交易是对 SDK 的单个方法调用:

1
const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');

了解 submitTransaction() 参数如何与交易请求匹配。它们的值将传递给智能合约中的 issue() 方法,并用于创建新的商业票据。回想一下它的签名:

1
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}

那可能会显示一个智能合约会在应用程序触发了 submitTransaction() 之后很快地收到控制,但是并不是那样的。在外表下,SDK 使用了 connectionOptionsconnectionProfile 来将交易提案发送给网络中正确的节点,从那里可以得到所需的背书。但是应用程序并不用担心——它仅仅是触发了 submitTransaction 然后 SDK 会处理接下来所有的事情!

我们注意到,submitTransaction API 包含了监听交易提交的一个流程。监听提交是必须的,因为如果没有它的话,你将不会知道你的交易是否被成功地排序了,验证了并且提交到了账本上。

现在让我们将注意力转向应用程序如何处理响应!

处理响应

回想一下 papercontract.js 如何发行交易返回一个商业票据响应:

1
return paper.toBuffer();

您会注意到一个轻微的怪癖——新票据需要在返回到应用程序之前转换为缓冲区。请注意 issue.js 如何使用类方法 CommercialPaper.fromBuffer() 将响应缓冲区重新转换为商业票据:

1
let paper = CommercialPaper.fromBuffer(issueResponse);

这样可以在描述性完成消息中以自然的方式使用票据

1
console.log(`${paper.issuer} commercial paper : ${paper.paperNumber} successfully issued for value ${paper.faceValue}`);

了解如何在应用程序和智能合约中使用相同的 paper 类——如果您像这样构建代码,它将真正有助于可读性和重用。

与交易提案一样,智能合约完成后,应用程序可能会很快收到控制权,但事实并非如此。SDK 负责管理整个共识流程,并根据策略连接选项在应用程序完成时通知应用程序。 如果您对 SDK 的内容感兴趣,请阅读详细的交易流程

就是这样!在本主题中,您已了解如何通过检查 MagnetoCorp 的应用程序如何在 PaperNet 中发行新的商业票据,从示例应用程序调用智能合约。现在检查关键账本和智能合约数据结构是由它们背后的架构主题设计的。

应用程序设计元素

本节详细介绍了 Hyperledger Fabric 中的客户端应用程序和智能合约开发的主要功能。对功能的充分理解将帮助您设计和实现高效且有效的解决方案。

合约名称

链码(Chaincode)是一种用于部署代码到 Hyperledger Fabric 区块链网络中的通用容器。链码中定义一个或多个相关联的智能合约。每个智能合约在链码中有一个唯一的标识名。应用程序通过合约名称去访问已经实例化的链码内指定的智能合约。

在本主题中,我们将会讲到:

  • 一个链码容器如何包括多个智能合约
  • 如何分配一个智能合约的名字
  • 如何使用应用程序中的智能合约
  • 默认的智能合约

链码

开发应用主题中,我们能够看到 Fabric SDK 如何提供了高级的编程抽象,从而能够帮助那些应用程序和智能合约的开发者们聚焦在他们的业务问题上,而不是将目光放在如何与 Fabric 网络交互的这些细节中。

智能合约(Smart Contract)是一种高级编程抽象的例子,可以在链码容器中定义智能合约。当一个链码被安装到 Peer 节点并部署到通道后,链码内所有的智能合约对你的应用来说都是可用的。

多个智能合约能够被定义在同一个链码内。每一个智能合约都通过链码内的名字而被唯一标识。

上图中,链码 A 中定义了三个智能合约,然而链码 B 中有四个智能合约。看一下链码名称如何被用于限定一个特定的智能合约。

账本结构由一组已经部署的智能合约所定义。那是因为账本包括了一些有关网络所感兴趣的业务对象(例如 PaperNet 内的商业票据),并且这些业务对象通过智能合约内定义的交易功能在其生命周期(例如:发行,购买,赎回)中移动。

在大多数情况下,一个链码内仅仅只定义了一个智能合约。然而,将相关联的智能合约放在一个链码中是有意义的。比如,以不同货币计价的商业票据中,可能有 EuroPaperContract, DollarPaperContract, YenPaperContract,这些智能合约在部署他们的通道内可能要互相保持同步。

命名

链码里的每一个智能合约都通过合约名称被唯一标识。当智能合约可以在构造类时显示分配这个名称,或者让 Contract隐式分配一个默认名称。

查看 papercontract.js 链码文件

1
2
3
4
5
6
class CommercialPaperContract extends Contract {

    constructor() {
        // Unique name when multiple contracts per chaincode file
        super('org.papernet.commercialpaper');
    }

看一下 CommercialPaperContract 构造函数是如何将合约名称指定为 org.papernet.commercialpaper 的。结果是:在 papercontract 链码内,这个智能合约现在与合约名 org.papernet.commercialpaper 相关联。

如果没有明确指定一个合约名,则会分配一个默认的名字——类名。在我们的例子中,默认的合约名是 CommercialPaperContract

细心地选择你的名字。不仅仅每一个智能合约必须有一个唯一的名字,而且一个精心挑选的名字也是很有启发性的。特别地,建议使用显式的 DNS 样式命名方法,对组织清晰、有意义的名称有帮助; org.papernet.commercialpaper 表达了 PaperNet 网络已经定义了一个标准的商业票据智能合约。

在一个给定链码内,合约名称也有利于消除具有相同名字的不同合约方法交易函数之间的歧义。当智能合约紧密联系的时候,这种歧义就容易发生;他们的交易名称往往倾向于一致。我们能够看到,通过链码和智能合约的名字的组合,一个交易被唯一地定义在通道内。

合约名称在链码文件内必须是唯一的。在部署前,一些代码编辑器将会检测是否存在具有相同类名的多个定义的情况。如果存在多个类使用了相同的合约名,无论是显式还是隐式指定,链码都将会返回错误。

应用程序

一旦链码安装在一个 Peer 节点而且部署在一个通道上,链码里的智能合约对于应用程序来说是可访问的:

1
2
3
4
5
const network = await gateway.getNetwork(`papernet`);

const contract = await network.getContract('papercontract', 'org.papernet.commercialpaper');

const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');

看一下应用程序如何通过 network.getContract() 方法访问智能合约。papercontract 链码的名 org.papernet.commercialpaper 返回了一个引用,此引用使用 contract.submitTransaction() 接口去提交发布商业票据的交易。

默认合约

被定义在链码内的第一个智能合约被成为默认合约。这个默认是有用的,因为链码内往往有一个被定义的智能合约;这个默认的智能合约允许应用程序直接地访问这些交易,而不需要特殊指定合约名称。

一个默认地智能合约是第一个被定义在链码的智能合约。

在这个图表中,CommercialPaperContract 就是那个默认的智能合约。即使我们有两个智能合约,默认的智能合约让我们当前的例子更加容易去编写。

1
2
3
4
5
const network = await gateway.getNetwork(`papernet`);

const contract = await network.getContract('papercontract');

const issueResponse = await contract.submitTransaction('issue', 'MagnetoCorp', '00001', '2020-05-31', '2020-11-30', '5000000');

papercontract 链码内,CommercialPaperContract 就是那个默认的智能合约,同时它有一个 issue 交易。注意,在 BondContract 内发布的交易事务仅仅能通过显示地寻址指来调用。同样地,即使 cancel 交易是唯一的(因为 BondContract 不是默认的智能合约),它也必须要显示地寻址。

在大多数情况下,一个链码仅仅只包括一个单一的智能合约,所以对链码仔细命名,能够降低开发者将链码视为概念来关注的需求。在上述代码例子中,感觉 papercontract 像是一个智能合约。

总而言之,在一个给定的链码内,合约名称是一些简单的机制去标识独自的智能合约。合约名称让应用程序更加容易的发现特定的智能合约而且更方便地使用它访问账本。

链码命名空间

链码命名空间允许其保持自己的世界状态与其他链码的不同。具体来说,同链码的智能合约对相同的世界状态都拥有直接的访问权,而不同链码的智能合约无法直接访问对方的世界状态。如果一个智能合约需要访问其他链码的世界状态,可通过执行链码-对-链码的调用来完成。最后,区块链可以包含涉及了不同世界状态的交易。

本文我们将讲述以下内容:

  • 命名空间的重要性
  • 什么是链码命名空间
  • 通道和链码命名空间
  • 如何使用链码命名空间
  • 如何使用智能合约来访问世界状态
  • 链码命名空间的设计考虑

动机

命名空间是一个常见的概念。我们知道虽然公园街道,纽约和帕克街道,西雅图这些属于不同街道,但是他们有共同的名字。城市为公园街提供了一个命名空间,同时赋予了它自由度和清晰度。

在电脑系统中也是如此。命名空间实现了让不同用户在不影响其他方操作的情况下对一个共享系统的不同部分进行编程和操作。许多编程语言都有命名空间,这就使得程序可以轻松地分配独特的识别符,如变量名,而无须担心其他程序也在进行相同操作。下文中会介绍到Hyperledger Fabric通过命名空间使得智能合约可以保持自己的账本世界状态与其他智能合约的不同

情景

让我们通过下面的图表来看一下账本世界状态如何管理对通道组织很重要的商业对象的相关事实。无论对象是商业票据,债券,还是车牌号码,无论这些对象位于生命周期的什么位置,它们都会被作为状态维持在账本数据状态数据库中。智能合约通过与账本(世界状态和区块链)交互来管理这些商业对象,而多数情况下这一过程会涉及到智能合约查询或更新账本世界状态。

账本世界状态是通过访问该状态的智能合约的链码来进行划分的,理解这一点至关重要 。这种划分或者命名空间对架构师、管理员和程序员来说是一点非常重要的设计考虑。

账本世界状态根据访问其状态的链码被分成不同命名空间。在一个给定通道上,相同链码的智能合约共享相同世界状态,不同链码的智能合约无法直接访问对方的世界状态。同样的道理,区块链可以包含与不同链码世界状态相关的交易。

在我们的示例中可以看到两种不同的链码中定义了四种智能合约,每个都位于它们自己的链码容器里。 euroPaperyenPaper 智能合约被定义在 papers 链码中。 euroBondyenBond 智能合约的情况也类似——它们被定义在bonds链码中。该设计可帮助应用程序员们理解他们的工作对象,是商业票据还是欧元或日元的债券,因为各金融产品的规则不会受货币种类影响,这就使得用相同链码管理这些金融产品的部署成为可能。

上图 还展示了这一部署选择的结果。数据库管理系统为 papersbonds 链码以及各自其中包含的智能合约创建了不同的世界状态数据库。 World state Aworld state B 被分别放在不同的数据库中;其中的数据也被分隔开,从而使得一个单独的世界状态查询(比如)不会同时访问两个世界状态。据说世界状态根据其链码进行了命名空间设定

看看 world state A 是如何包含两个列表的商业票据 paperListEuropaperListYen 。状态 PAP11PAP21 分别是由 euroPaperyenPaper 智能合约管理的每个票据的示例。由于这些智能合约共享同样的链码命名空间,因此它们的key(PAPxyz) 必须在 papers 链码的命名空间里是唯一的。要注意怎样能在papers 链码上写一个能对所有商业票据(无论是欧元还是日元)执行汇总计算的智能合约,因为这些商业票据共享相同的命名空间。债券的情况也类似——债券保存在 world state B 中, world state B 与另一个bonds 数据库形成映射,它们的key必须都是唯一的。

同样重要的一点是,命名空间意味着 euroPaperyenPaper 无法直接访问 world state B ,也意味着 euroBondyenBond 无法直接访问 world state A 。这样的分隔是有帮助的,因为商业票据和债券是相差很大的两种金融工具,它们各有不同的属性和规定。同时命名空间还意味着 papersbonds 可以有相同的key,因为它们在不同的命名空间。这是有帮助的,因为它为命名提供了足够大的自由空间。以此来为不同的商业对象命名。

最重要的一点是,我们可以看到区块链是与在特定通道上运行的节点相关的,它包括了影响 world state Aworld state B 的交易。这就解释了为什么区块链是节点中最基础的数据结构。因为世界状态集是区块链交易的汇总结构,所以它总是能从区块链中重新创建。世界状态通常仅要求该状态的当前值,所以它可以简化智能合约,提升智能合约的效率。通过命名空间来让世界状态彼此各不相同,这能帮助智能合约将各自逻辑从其他智能合约中隔离出来,而不需要担心那些与不同世界状态通信的交易。例如, bonds 合约不需要担心 paper 交易,因为该合约无法看到由它所造成的世界状态。

同样值得注意的是,节点,链码容器和数据库管理系统(DBMS)在逻辑上来说都是不同的过程。节点和其所有的链码容器始终位于不同的操作系统进程中,但是 DBMS 可被配置为嵌入或分离,这取决于它的类型。对于 LevelDB 来说,DBMS 整体被包含在节点中,但是对于 CouchDB 来说,这是另外一个操作系统进程。

要记住,该示例中命名空间的选择是商业上要求共享不同币种商业票据的结果,但是要将商业票据与债券区别开,这一点很重要。想想如何对命名空间的结果进行修改以满足保持每个金融资产等级互不相同这一商业需求,或者来共享所有商业票据和债券。

通道

如果一个节点加入了多个通道,那么将会为每个通道创建、管理一条新的区块链。而且每当有链码部署在一条新通道上,就会为其创建一个新的世界状态数据库。这就意味着通道还可以随着链码命名空间来为世界状态生成一种命名空间。

然而,相同的节点和链码容器流程可同时加入多个通道,与区块链和世界状态数据库不同,这些流程不会随着加入的通道数而增加。

例如,如果链码 papersbonds 在一个新的通道上被部署了,将会有一个完全不同的区块链和两个新的世界状态数据库被创建出来。但是,节点和链码容器不会随之增加;它们只会各自连接到更多相应的通道上。

使用

让我们来用商业票据的例子讲解一个应用程序是如何使用带有命名空间的智能合约的。值得注意的是应用程序与节点交互,该节点将请求发送到对应的链码容器,随后,该链码容器访问DBMS。请求的传输是由下图中的节点核心组件完成的。

以下是使用商业票据和债券的应用程序代码,以欧元和日元定价。该代码可以说是不言自明的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const euroPaper = network.getContract(papers, euroPaper);
paper1 = euroPaper.submit(issue, PAP11);

const yenPaper = network.getContract(papers, yenPaper);
paper2 = yenPaper.submit(redeem, PAP21);

const euroBond = network.getContract(bonds, euroBond);
bond1 = euroBond.submit(buy, BON31);

const yenBond = network.getContract(bonds, yenBond);
bond2 = yenBond.submit(sell, BON41);

看一下应用程序是怎么:

  • 访问euroPaperyenPaper 合约,用 getContract() API指明paper 的链码。看交互点1a2a
  • 访问 euroBondyenBond 合约,用 getContract() API指明bonds链码。看交互点3a4a
  • 向网络提交一个 issue 交易,以使 PAP11 票据使用 euroPaper 合约。看交互点1a。这就产生了由 world state A 中的 PAP11 状态代表的商业票据;交互点1b。这一操作在交互点1c上被捕获为区块链的一项交易。
  • 向网络发送一个 redeem 交易,让商业票据 PAP21 来使用 yenPaper 合约。看交互点2a。这就产生了由 world state APAP21 代表的一个商业票据;交互点2b。该操作在交互点2c被捕获为区块链的一项交易。
  • 向网络发送一个 buy 交易,让债券 BON31 来使用 euroBond 合约。看交互点3a。这就产生了由 world state BBON31 代表的一个商业票据;交互点3b。该操作在交互点3c被捕获为区块链的一项交易。
  • 向网络发送一个 sell 交易,让债券 BON41 来使用 yenBond 合约。看交互点4a。这就产生了由 world state BBON41 代表的一个商业票据;交互点4b。该操作在交互点4c被捕获为区块链的一项交易。

我们来看看智能合约是如何与世界状态交互的:

  • euroPaperyenPaper 合约可直接访问 world state A,但无法直接访问 world state Bworld state A储存在与papers链码相对应的数据库管理系统中的papers数据库里面。
  • euroBondyenBond 合约可以直接访问world state B,但是无法直接访问world state Aworld state B储存在与bonds链码相对应的数据库管理系统中的bonds数据库里面。

看看区块链是如何为所有世界状态捕获交易的:

  • 与交易相对应的交互点1c2c分别创建和更新商业票据 PAP11PAP21。它们都包含在 world state A 中。
  • 与交易相对应的交互点3c4c都会对 BON31BON41 进行更新。它们都包含在world state B 中。
  • 如果 world state Aworld state B 因任何原因被破坏,可通过重新进行区块链上的所有交易来重新创建world state Aworld state B

跨链码访问

情景 中的示例可见, euroPaperyenPaper 无法直接访问world state B。这是因为我们对链码和智能合约进行了设置,使得这些链码和世界状态各自之间保持不同。不过,我们来想象一下 euroPaper 需要访问 world state B

为什么会发生这种情况呢?想象一下当发行商业票据时,智能合约想根据到期日相似的债券的当前收益情况来对票据进行定价。这种情况时,需要 euroPaper 合约能够在 world state B中查询债券价格。看以下图表来研究一下应该如何设计交互的结构。

链码和智能合约如何能够直接访问其他世界状态——通过自己的链码。

注意如何:

  • 应用程序在 euroPaper 智能合约中发送 issue 交易来颁布 PAP11。看交互点 1a
  • euroPaper 智能合约中的 issue 交易在 euroBond 智能合约中调用 query 交易。看交互点 1b
  • euroBond 中的 queryworld state B 中索取信息。看交互点 1c
  • 当控制返回到 issue 交易,它可利用响应中的信息对票据定价,并使用信息来更新 world state A。看交互点 1d
  • 发行以日元定价的商务票据的控制流程也是一样的。看交互点 2a2b2c2d

链码间的控制传递是利用 invokeChaincode() API来实现的。

该 API 将控制权从一个链码传递到另一个链码。

虽然我们在示例中只谈论了查询交易,但是用户也能够调用一个将更新所谓链码世界状态的智能合约。请参考下面的 思考 部分。

思考

  • 大体上来说,每个链码都包含一个智能合约。

  • 若多个智能合约之间关系紧密,应该把它们部署在同一链码中。通常,只有当这些智能合约共享相同的世界状态时才需要这样做。

  • 链码命名空间将不同的世界状态分隔开来。这大体上会将彼此不相关的数据区分开。注意,命名空间是由 Hyperledger Fabric 分配并直接映射到链码名字的,用户无法选择。

  • 对于使用 invokeChaincode()API 的链码间交互,必须将这两个链码安装在相同节点上。

    • 对于仅需要查询被调用链码的世界状态的交互,该调用可发生在与调用发起者链码不同的通道上。

    • 对于需要更新被调用链码世界状态的交互,调用必须发生在与调用发起者链码相同的通道上。

交易上下文

交易上下文执行两个功能。 首先,它允许开发人员在智能合约中跨交易调用定义和维护用户变量。 其次,它提供了对范围广泛的 Fabric API 的访问,允许智能合约开发人员执行与详细交易处理相关的操作。 这些范围从查询或更新账本(包括不可变的区块链和可修改的世界状态)到检索提交交易的应用程序的数字身份。

当智能合约部署到通道并可供每个后续交易调用使用时,将创建交易上下文。 交易上下文帮助智能合约开发人员编写功能强大、高效且易于推理的程序。

  • 为什么交易上下文很重要
  • 如何使用交易上下文
  • 交易上下文中有什么
  • 使用上下文stub
  • 使用上下文 clientIdentity

设想

在商业票据示例中,papercontract 最初定义了它负责的商业票据列表的名称。 随后的每笔交易均参考此列表; 发行交易向其添加新文件,购买交易更改其所有者,赎回交易将其标记为已完成。 这是一种常见的模式; 在编写智能合约时,初始化和调用顺序交易中的特定变量通常很有帮助。

智能合约交易上下文允许智能合约定义和维护跨交易调用的用户变量。 请参阅文本以获得详细说明。

编程

在构造智能合约时,开发人员可以选择重写内置的 Context 类的 createContext 方法来创建自定义上下文:

1
2
3
createContext() {
    new CommercialPaperContext();
}

在我们的示例中,CommercialPaperContext 专用于 CommercialPaperContract。 查看自定义上下文如何通过此地址将特定变量 PaperList 添加到自身:

1
2
3
4
5
CommercialPaperContext extends Context {
    constructor () {
        this.paperList = new PaperList(this);
    }
}

当 createContext() 方法在上图中的点 (1) 处返回时,已创建自定义上下文 ctx,其中包含 paperList 作为其变量之一。

随后,每当调用发行、购买或赎回等智能合约交易时,都会将此上下文传递给它。 查看第 (2)、(3) 和 (4) 点如何使用 ctx 变量将相同的商业票据上下文传递到交易方法中。

在第 (5) 点查看上下文是如何使用的:

1
2
ctx.paperList.addPaper(...);
ctx.stub.putState(...);

请注意在 CommercialPaperContext 中创建的 paperList 如何可用于发行交易。 查看 redeem 和 buy 交易如何类似地使用 paperList; ctx 使智能合约高效且易于推理。

您还可以看到上下文中还有另一个元素——ctx.stub——它不是 CommercialPaperContext 明确添加的。 这是因为stub和其他变量是上下文内置的一部分。 现在让我们检查一下这个内置上下文的结构、这些隐式变量以及如何使用它们。

结构

正如我们从示例中看到的,交易上下文可以包含任意数量的用户变量,例如 paperList。

交易上下文还包含两个内置元素,它们提供对范围广泛的 Fabric 功能的访问,从提交事务的客户端应用程序到账本访问。

  • ctx.stub 用于访问提供广泛交易处理操作的 API,从用于访问分类帐的 putState() 和getState() 到用于检索当前事务 ID 的 getTxID()。
  • ctx.clientIdentity 用于获取有关提交交易的用户身份的信息

我们将使用下图向您展示智能合约可以使用可用的 API 使用stub和 clientIdentity 做什么:

智能合约可以通过交易上下文stubclientIdentity 访问智能合约中的一系列功能

stub

stub中的 API 分为以下几类:

  1. 世界状态数据 API。 请参见交互点 (1)。 这些 API 使智能合约能够使用它们的密钥从世界状态中获取、放置和删除与各个对象相对应的状态:

这些基本 API 由查询 API 补充,查询 API 使合约能够检索一组状态,而不是单个状态。 请参见交互点 (2)。 该集合要么由一系列键值定义,使用完整或部分键,要么根据底层世界状态数据库中的值进行查询。 对于大型查询,可以对结果集进行分页以减少存储需求:

  1. 私有数据 API。 请参见交互点 (3)。 这些 API 使智能合约能够与私有数据集合进行交互。 它们类似于用于世界状态交互的 API,但用于私有数据。 有一些 API 可以通过其密钥获取、放置和删除私有数据状态:

该集合由一组用于查询私有数据的 API 进行补充 (4)。 这些 API 允许智能合约根据一系列键值(完整或部分键)或根据底层世界状态数据库中的值进行查询,从私有数据集合中检索一组状态。 目前没有用于私有数据集合的分页 API。

  1. 交易 API。 请参见交互点 (5)。 智能合约使用这些 API 来检索有关智能合约正在处理的当前交易提案的详细信息。 这包括交易标识符和创建交易建议的时间。
    • getTxID() 返回当前交易提案的标识符 (5).
    • getTxTimestamp() :返回当前交易提案创建时的时间戳 (5).
    • getCreator() :返回创建者的原始身份(X.509 或其他交易提案。 如果这是一个 X.509 证书,那么它通常是更适合使用ctx.ClientIdentity.
    • getSignedProposal() :通过智能合约返回正在处理的当前交易提案的签名副本。
    • getBinding() :用于防止交易被恶意或意外重播使用随机数。 (出于实际目的,随机数是一个随机数由客户端应用程序生成并包含在密码中哈希。)例如,此 API 可由智能合约在 (1)检测事务的重播(5)
    • getTransient() 允许智能合约访问应用程序传递的瞬态数据到智能合约。 请参阅交互点 (9)(10)。 瞬态数据对于应用程序-智能合约交互是私有的。它不记录在分类账上,通常与私人数据收集一起使用 (3)
  2. 密钥 API。智能合约使用 Key APIs 来操作世界状态或私有数据集合中的状态密钥。 请参阅交互点 2 和 4

这些 API 中最简单的一个允许智能合约从其各个组件中形成和拆分复合密钥。 稍微高级一点的是 ValidationParameter() API,它为世界状态 (2) 和私有数据 (4) 获取和设置基于状态的背书策略。 最后,getHistoryForKey() 通过返回一组存储值来检索状态的历史记录,包括执行状态更新的交易标识符,从而允许从区块链 (10) 读取交易。

  1. 事件 API。事件 API 用于管理智能合约中的事件处理
  2. 实用程序 API 。是一组有用的 API,它们不容易归入预定义的类别,因此我们将它们组合在一起! 它们包括检索当前通道名称并将控制权传递给同一对等方上的不同链代码。

其中一些实用程序 API 仅在您使用低级链码而不是智能合约时使用。 这些 API 主要用于链代码输入的详细操作; 智能合约 Contract 类为开发人员自动完成所有这些参数编组。

ClientIdentity

ClientIdentity 获取 getCreator() 返回的信息,并在其之上放置一组 X.509 实用程序 API,以使其更易于用于此常见用例。

  • getX509Certificate() 返回交易提交者的完整 X.509 证书,包括其所有属性及其值。 请参见交互点 (6)。
  • getAttributeValue() 返回特定 X.509 属性的值,例如,组织单位 OU 或专有名称 DN。 请参见交互点 (6)。
  • 如果 X.509 属性的指定属性具有指定值,则 assertAttributeValue() 返回 TRUE。 请参见交互点 (6)。
  • getID() 根据交易提交者的专有名称和发证 CA 的专有名称返回交易提交者的唯一身份。 格式为 x509::{subject DN}::{issuer DN}。 请参见交互点 (6)。
  • getMSPID() 返回交易提交者的通道 MSP。 这允许智能合约根据提交者的组织身份做出处理决策。 请参阅交互点 (15) 或 (16)。

在大多数情况下,提交交易的应用程序将使用 X.509 证书。 在示例中,Isabella (8) 在她的应用程序中使用 CA1 (7) 颁发的 X.509 证书 (6) 来签署事务 t6 (5) 中的提案。

交易处理器

交易处理器允许智能合约开发人员在应用程序和智能合约交互期间的关键点上定义通用处理。交易处理器是可选的,但是如果定义了,它们将在调用智能合约中的每个交易之前或之后接收控制权。还有一个特定的处理器,当请求调用未在智能合约中定义的交易时,该处理程序接收控制

这里是一个商业票据智能合约示例的交易处理器的例子:

前置、后置和未知交易处理器。在这个例子中,beforeTransaction()issue, buyredeem 交易之前被调用。afterTransaction()issue, buyredeem 交易之后调用。UnknownFunction() 当请求调用未在智能合约中定义的交易时调用。(通过不为每个交易重复 beforeTransactionafterTransaction 框来简化该图)

处理器类型

有三种类型的交易处理器,它们涵盖应用程序和智能合约之间交互的不同方面:

  • 前置处理器:在每个智能合约交易执行之前调用。该处理器通常用来改变交易使用的交易上下文。处理器可以访问所有 Fabric API;如,可以使用 getState()putState()
  • 后置处理器:在每个智能合约交易执行之后调用。处理器通常会对所有的交易执行通用的后置处理,同样可以访问所有的 Fabric API。
  • 未知处理器:试图执行未在智能合约中定义的交易时被调用。通常,处理器将记录管理员后续处理的失败。处理器可以访问所有的 Fabric API。

定义事务处理程序是可选的; 智能合约将在没有定义处理程序的情况下正确执行。 智能合约最多可以为每种类型定义一个处理程序。

定义处理器

交易处理器作为具有明确定义名称的方法添加到智能合约中。这是一个添加每种类型的处理器的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
CommercialPaperContract extends Contract {

    ...

    async beforeTransaction(ctx) {
        // Write the transaction ID as an informational to the console
        console.info(ctx.stub.getTxID());
    };

    async afterTransaction(ctx, result) {
        // This handler interacts with the ledger
        ctx.stub.cpList.putState(...);
    };

    async unknownTransaction(ctx) {
        // This handler throws an exception
        throw new Error('Unknown transaction function');
    };

}

交易处理器定义的形式对于所有处理程序类型都是类似的,但请注意 afterTransaction(ctx,result) 如何接收交易返回的任何结果。API 文档 展示了这些处理器的准确格式。

处理器处理

一旦处理器添加到智能合约中,它可以在交易处理期间调用。在处理期间,处理器接收 ctx ,即交易上下文,会执行一些处理,完成后返回控制权。继续如下的处理:

  • 前置处理器:如果处理器成功完成,使用更新后的上下文调用交易。如果处理器抛出异常,不会调用交易,智能合约失败并显示异常错误消息。
  • 后置处理器:如果处理器成功完成,则智能合约将按调用的交易确定完成。如果处理程序抛出异常,则交易将失败并显示异常错误消息。
  • 未知处理器:处理器应该通过抛出包含所需错误消息的异常来完成。如果未指定未知处理器,或者未引发异常,则存在合理的默认处理;智能合约将以未知交易错误消息失败。

如果处理器需要访问函数和参数,这很容易做到:

1
2
3
4
5
6
7
8
async beforeTransaction(ctx) {
    // Retrieve details of the transaction
    let txnDetails = ctx.stub.getFunctionAndParameters();


    console.info(`Calling function: ${txnDetails.fcn} `);
    console.info(util.format(`Function arguments : %j ${stub.getArgs()} ``);
}

查看此处理程序如何通过事务上下文使用实用程序 APIgetFunctionAndParameters

多处理器

智能合约的每种类型最多只能定义一个处理程序。 如果智能合约需要在之前、之后或未知处理期间调用多个函数,它应该从适当的函数中进行协调。

背书策略

背书策略定义了要背书一项交易使其生效所需要的最小组织集合。要想对一项交易背书,组织的背书节点需要运行与该交易有关的智能合约,并对结果签名。当排序服务将交易发送给提交节点,节点们将各自检查该交易的背书是否满足背书策略。如果不满足的话,交易被撤销,不会对世界状态产生影响。

背书策略从以下两种不同的维度来发挥作用:既可以被设置为整个命名空间,也可被设置为单个状态键。它们是使用诸如 ANDOR 这样的逻辑表述来构成的。例如,在 PaperNet 中可以这样来用: MagnetoCorp 将一篇论文卖给 DigiBank,其背书策略可被设定为 AND(MagnetoCorp.peer, DigiBank.peer),这就要求任何对该论文做出的改动需被 MagnetoCorp 和 DigiBank 同时背书。

连接配置文件

连接配置文件描述了一组组件,包括 Hyperledger Fabric 区块链网络中的 Peer 节点排序节点以及 CA。它还包含与这些组件相关的通道和组织信息。连接配置文件主要由应用程序用于配置处理所有网络交互的网关,从而使其可以专注于业务逻辑。连接配置文件通常由了解网络拓扑的管理员创建。

在本主题中,我们将介绍:

情景

连接配置文件用于配置网关。网关很重要,很多原因,主要是简化应用程序网络通道的交互。

两个应用程序,发行和购买,使用配置有连接配置文件 1和 2 的网关 1和 2。每个配置文件描述了 MagnetoCorp 和 DigiBank 网络组件的不同子集。每个连接配置文件必须包含足够的信息,以便网关代表发行和购买应用程序与网络进行交互。有关详细说明,请参阅文本。

连接配置文件包含网络视图的描述,以技术语法表示,可以是 JSON 或 YAML。在本主题中,我们使用 YAML 表示,因为它更容易阅读。静态网关需要比动态网关更多的信息,因为后者可以使用服务发现来动态增加连接配置文件中的信息。

连接配置文件不应该是网络通道的详尽描述;它只需要包含足够的信息,足以满足使用它的网关。在上面的网络中,连接配置文件1需要至少包含背书组织和用于 issue 交易的 Peer 节点,以及识别将交易提交到帐本上时会通知网关的 Peer 节点。

最简单的方法是将连接配置文件视为描述网络的视图。这可能是一个综合观点,但由于以下几个原因,这是不现实的:

  • 根据需求添加和删除 Peer 节点、排序节点、CA、通道和组织。
  • 组件可以启动或停止,或意外失败(例如断电).
  • 网关不需要整个网络的视图,只需要成功处理交易提交或事件通知所需的内容。
  • 服务发现可以扩充连接配置文件中的信息。具体来说,动态网关可以配置最少的 Fabric 拓扑信息;其余的都可以被发现。

静态连接配置文件通常由详细了解网络拓扑的管理员创建。这是因为静态配置文件可能包含大量信息,管理员需要在相应的连接配置文件中获取到这些信息。相比之下,动态配置文件最小化所需的定义数量,因此对于想要快速掌握的开发人员或想要创建响应更快的网关的管理员来说,这是更好的选择。使用编辑器以 YAML 或 JSON 格式创建连接配置文件。

用法

我们将看到如何快速定义连接配置文件;让我们首先看看 MagnetoCorp 示例的 issue 程序如何使用它:

1
2
3
4
5
6
7
8
const yaml = require('js-yaml');
const { Gateway } = require('fabric-network');

const connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/paperNet.yaml', 'utf8'));

const gateway = new Gateway();

await gateway.connect(connectionProfile, connectionOptions);

加载一些必需的类后,查看如何从文件系统加载 paperNet.yaml 网关文件,使用 yaml.safeLoad() 方法转换为 JSON 对象,并使用其 connect() 方法配置网关。

通过使用此连接配置文件配置网关,发行应用程序为网关提供应用于处理交易的相关网络拓扑。这是因为连接配置文件包含有关 PaperNet 通道、组织、Peer 节点或排序节点和 CA 的足够信息,以确保可以成功处理交易。

连接配置文件为任何给定的组织定义多个 Peer 节点是一种比较好的做法,它可以防止单点故障。这种做法也适用于动态网关; 为服务发现提供多个起点。

DigiBank 的 buy 程序通常会为其网关配置类似的连接配置文件,但有一些重要的区别。一些元素将是相同的,例如通道;一些元素将重叠,例如背书节点。其他元素将完全不同,例如通知 Peer 节点或 CA。

传递给网关的 connectionOptions 补充了连接配置文件。它们允许应用程序声明网关如何使用连接配置文件。它们由 SDK 解释以控制与网络组件的交互模式,例如选择要连接的标识或用于事件通知的节点。了解可用连接选项列表以及何时使用它们。

结构

为了帮助您了解连接配置文件的结构,我们将逐步介绍上面显示的网络示例。其连接配置文件基于 PaperNet 商业票据样例,并存储在 GitHub 仓库中。为方便起见,我们在下面复制了它。您会发现在现在阅读它时,将它显示在另一个浏览器窗口中会很有帮助:

  • 第9行: name: "papernet.magnetocorp.profile.sample"

    这是连接配置文件的名称。尝试使用 DNS 风格名称;它们是传达意义的一种非常简单的方式。

  • 第16行: x-type: "hlfv1"

    用户可以添加自己的“特定于应用程序”的 x- 属性,就像 HTTP 头一样。它们主要供未来使用。

  • 第20行: description: "Sample connection profile for documentation topic"

    连接配置文件的简短描述。尽量让这对第一次看到这个的读者有所帮助!

  • 第25行: version: "1.0"

    此连接配置文件的架构版本。目前仅支持版本1.0,并且未设想此架构将经常更改。

  • 第32行: channels:

    这是第一个非常重要的行。channels: 标识以下内容是此连接配置文件描述的所有通道。但是,最好将不同的通道保存在不同的连接配置文件中,特别是如果它们彼此独立使用。

  • 第36行: papernet:

    papernet 详细信息将是此连接配置文件中的第一个通道。

  • 第41行: orderers:

    有关 papernet 的所有排序节点的详细信息如下。您可以在第45行看到此通道的排序节点是 orderer1.magnetocorp.example.com。这只是一个逻辑名称;稍后在连接配置文件(第134-147行)中,将会有如何连接到此排序节点的详细信息。请注意 orderer2.digibank.example.com 不在此列表中;应用程序使用自己组织的排序节点,而不是来自不同组织的排序节点,这是有道理的。

  • 第49行: peers:

    下边将介绍 papernet 所有 Peer 节点的详细信息。

    您可以看到 MagnetoCorp 列出的三个 Peer 节点: peer1.magnetocorp.example.compeer2.magnetocorp.example.compeer3.magnetocorp.example.com。没有必要列出 MagnetoCorp 中的所有 Peer 节点,就像这里所做的那样。您只能看到 DigiBank 中列出的一个 Peer 节点: peer9.digibank.example.com; 包括这个 Peer 节点开始隐含背书策略要求 MagnetoCorp和DigiBank 背书交易,正如我们现在要确认的。最好有多个 Peer 节点来避免单点故障。

    在每个 peer 下面,您可以看到四个非独占角色:endorsingPeerchaincodeQueryledgerQueryeventSource。了解一下 peer1peer2 如何在主机 papercontract 中执行所有角色。它们与 peer3 不同,peer3 只能用于通知,或者用于访问帐本的链组件而不是世界状态的帐本查询,因此不需要安装智能合约。请注意 peer9 不应该用于除背书之外的任何其他情况,因为 MagnetoCorp 的节点可以更好地服务于这些角色。

    再次,看看如何根据 Peer 节点的逻辑名称和角色来描述 Peer 节点。稍后在配置文件中,我们将看到这些 Peer 节点的物理信息。

  • 第97行: organizations:

    所有组织的详细信息将适用与所有通道。请注意,这些组织适用于所有通道,即使 papernet 是目前唯一列出的组织。这是因为组织可以在多个通道中,通道可以有多个组织。此外,一些应用程序操作涉及组织而不是通道。例如,应用程序可以使用连接选项从其组织内的一个或所有 Peer 节点或网络中的所有组织请求通知。为此,需要有一个组织到 Peer 节点的映射,本节提供了它。

  • 第101行: MagnetoCorp:

    列出了属于 MagnetoCorp 的所有 Peer 节点:peer1peer2peer3。同样适用于证书颁发机构。再次注意逻辑名称用法,与 channels: 部分相同;物理信息将在后面的配置文件中显示。

  • 第121行 DigiBank:

    只有 peer9 被列为 DigiBank 的一部分,没有证书颁发机构。这是因为这些其他 Peer 节点和 DigiBank CA与此连接配置文件的用户无关。

  • 第134行: orderers:

    现在列出了排序节点的物理信息。由于此连接配置文件仅提到了 papernet 一个排序节点,您会看到列出的 orderer1.magnetocorp.example.com 的详细信息。包括其 IP 地址和端口,以及可以覆盖与排序节点通信时使用的默认值的 gRPC 选项(如有必要)。对于 peers: 为了实现高可用性,可以指定多个排序节点。

  • 第152行: peers:

    现在列出所有先前 Peer 节点的物理信息。此连接配置文件有三个 MagnetoCorp 的 Peer 节点:peer1peer2peer3;对于 DigiBank,单个 peer peer9列出了其信息。对于每个 Peer 节点,与排序节点一样,列出了它们的 IP 地址和端口,以及可以覆盖与特定节点通信时使用的默认值的 gRPC 选项(如有必要)。

  • 第194行: certificateAuthorities:

    现在列出了证书颁发机构的物理信息。连接配置文件为 MagnetoCorp 列出了一个 CA ca1-magnetocorp,然后是其物理信息。除了 IP 详细信息,注册商信息允许此 CA 用于证书签名请求(CSR)。这些都是用本地生成的公钥/私钥对来请求新证书。

现在您已经了解了 MagnetoCorp 的连接配置文件,您可能希望查看 DigiBank 的相关配置文件。找到与 MagnetoCorp 相同的配置文件的位置,查看它的相似之处,并比较最终哪里不同。想想为什么这些差异对 DigiBank 应用程序有作用。

这就是您需要了解的有关连接配置文件的所有信息。总之,连接配置文件为应用程序定义了足够的通道、组织、Peer 节点、排序节点和 CA 以配置网关。网关允许应用程序专注于业务逻辑而不是网络拓扑的细节。

示例

该文件是从 GitHub 商业票据示例中复制的。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
1: ---
2: #
3: # [Required]. A connection profile contains information about a set of network
4: # components. It is typically used to configure gateway, allowing applications
5: # interact with a network channel without worrying about the underlying
6: # topology. A connection profile is normally created by an administrator who
7: # understands this topology.
8: #
9: name: "papernet.magnetocorp.profile.sample"
10: #
11: # [Optional]. Analogous to HTTP, properties with an "x-" prefix are deemed
12: # "application-specific", and ignored by the gateway. For example, property
13: # "x-type" with value "hlfv1" was originally used to identify a connection
14: # profile for Fabric 1.x rather than 0.x.
15: #
16: x-type: "hlfv1"
17: #
18: # [Required]. A short description of the connection profile
19: #
20: description: "Sample connection profile for documentation topic"
21: #
22: # [Required]. Connection profile schema version. Used by the gateway to
23: # interpret these data.
24: #
25: version: "1.0"
26: #
27: # [Optional]. A logical description of each network channel; its peer and
28: # orderer names and their roles within the channel. The physical details of
29: # these components (e.g. peer IP addresses) will be specified later in the
30: # profile; we focus first on the logical, and then the physical.
31: #
32: channels:
33:   #
34:   # [Optional]. papernet is the only channel in this connection profile
35:   #
36:   papernet:
37:     #
38:     # [Optional]. Channel orderers for PaperNet. Details of how to connect to
39:     # them is specified later, under the physical "orderers:" section
40:     #
41:     orderers:
42:     #
43:     # [Required]. Orderer logical name
44:     #
45:       - orderer1.magnetocorp.example.com
46:     #
47:     # [Optional]. Peers and their roles
48:     #
49:     peers:
50:     #
51:     # [Required]. Peer logical name
52:     #
53:       peer1.magnetocorp.example.com:
54:         #
55:         # [Optional]. Is this an endorsing peer? (It must have chaincode
56:         # installed.) Default: true
57:         #
58:         endorsingPeer: true
59:         #
60:         # [Optional]. Is this peer used for query? (It must have chaincode
61:         # installed.) Default: true
62:         #
63:         chaincodeQuery: true
64:         #
65:         # [Optional]. Is this peer used for non-chaincode queries? All peers
66:         # support these types of queries, which include queryBlock(),
67:         # queryTransaction(), etc. Default: true
68:         #
69:         ledgerQuery: true
70:         #
71:         # [Optional]. Is this peer used as an event hub? All peers can produce
72:         # events. Default: true
73:         #
74:         eventSource: true
75:       #
76:       peer2.magnetocorp.example.com:
77:         endorsingPeer: true
78:         chaincodeQuery: true
79:         ledgerQuery: true
80:         eventSource: true
81:       #
82:       peer3.magnetocorp.example.com:
83:         endorsingPeer: false
84:         chaincodeQuery: false
85:         ledgerQuery: true
86:         eventSource: true
87:       #
88:       peer9.digibank.example.com:
89:         endorsingPeer: true
90:         chaincodeQuery: false
91:         ledgerQuery: false
92:         eventSource: false
93: #
94: # [Required]. List of organizations for all channels. At least one organization
95: # is required.
96: #
97: organizations:
98:    #
99:    # [Required]. Organizational information for MagnetoCorp
100:   #
101:   MagnetoCorp:
102:     #
103:     # [Required]. The MSPID used to identify MagnetoCorp
104:     #
105:     mspid: MagnetoCorpMSP
106:     #
107:     # [Required]. The MagnetoCorp peers
108:     #
109:     peers:
110:       - peer1.magnetocorp.example.com
111:       - peer2.magnetocorp.example.com
112:       - peer3.magnetocorp.example.com
113:     #
114:     # [Optional]. Fabric-CA Certificate Authorities.
115:     #
116:     certificateAuthorities:
117:       - ca-magnetocorp
118:   #
119:   # [Optional]. Organizational information for DigiBank
120:   #
121:   DigiBank:
122:     #
123:     # [Required]. The MSPID used to identify DigiBank
124:     #
125:     mspid: DigiBankMSP
126:     #
127:     # [Required]. The DigiBank peers
128:     #
129:     peers:
130:       - peer9.digibank.example.com
131: #
132: # [Optional]. Orderer physical information, by orderer name
133: #
134: orderers:
135:   #
136:   # [Required]. Name of MagnetoCorp orderer
137:   #
138:   orderer1.magnetocorp.example.com:
139:     #
140:     # [Required]. This orderer's IP address
141:     #
142:     url: grpc://localhost:7050
143:     #
144:     # [Optional]. gRPC connection properties used for communication
145:     #
146:     grpcOptions:
147:       ssl-target-name-override: orderer1.magnetocorp.example.com
148: #
149: # [Required]. Peer physical information, by peer name. At least one peer is
150: # required.
151: #
152: peers:
153:   #
154:   # [Required]. First MagetoCorp peer physical properties
155:   #
156:   peer1.magnetocorp.example.com:
157:     #
158:     # [Required]. Peer's IP address
159:     #
160:     url: grpc://localhost:7151
161:     #
162:     # [Optional]. gRPC connection properties used for communication
163:     #
164:     grpcOptions:
165:       ssl-target-name-override: peer1.magnetocorp.example.com
166:       request-timeout: 120001
167:   #
168:   # [Optional]. Other MagnetoCorp peers
169:   #
170:   peer2.magnetocorp.example.com:
171:     url: grpc://localhost:7251
172:     grpcOptions:
173:       ssl-target-name-override: peer2.magnetocorp.example.com
174:       request-timeout: 120001
175:   #
176:   peer3.magnetocorp.example.com:
177:     url: grpc://localhost:7351
178:     grpcOptions:
179:       ssl-target-name-override: peer3.magnetocorp.example.com
180:       request-timeout: 120001
181:   #
182:   # [Required]. Digibank peer physical properties
183:   #
184:   peer9.digibank.example.com:
185:     url: grpc://localhost:7951
186:     grpcOptions:
187:       ssl-target-name-override: peer9.digibank.example.com
188:       request-timeout: 120001
189: #
190: # [Optional]. Fabric-CA Certificate Authority physical information, by name.
191: # This information can be used to (e.g.) enroll new users. Communication is via
192: # REST, hence options relate to HTTP rather than gRPC.
193: #
194: certificateAuthorities:
195:   #
196:   # [Required]. MagnetoCorp CA
197:   #
198:   ca1-magnetocorp:
199:     #
200:     # [Required]. CA IP address
201:     #
202:     url: http://localhost:7054
203:     #
204:     # [Optioanl]. HTTP connection properties used for communication
205:     #
206:     httpOptions:
207:       verify: false
208:     #
209:     # [Optional]. Fabric-CA supports Certificate Signing Requests (CSRs). A
210:     # registrar is needed to enroll new users.
211:     #
212:     registrar:
213:       - enrollId: admin
214:         enrollSecret: adminpw
215:     #
216:     # [Optional]. The name of the CA.
217:     #
218:     caName: ca-magnetocorp

连接选项

连接选项(Connection option)用于与连接配置文件联合使用去精准地控制一个网关如何与网络交互。通过使用网关,允许一个应用开发专注于商业逻辑而不是网络拓扑。

本主题中,我们将涉及:

  • 为什么连接选项很重要
  • 应用程序如何使用连接选项
  • 每个连接选项的作用
  • 何时使用特定的连接选项

情景

连接选项指定网关行为的特定方面。网关对于一些原因是重要,其主要目的是允许应用程序去聚焦在业务逻辑和智能合约,同时管理与网络的一些组件的交互。

连接控制行为的不同交互点。这些选项将会在本文中被充分说明

一个连接选项的例子可能是想说明:被 issue 程序使用的网关应该使用 Isabella 身份将交易提交到 papernet 网络中。另一个可能是网关应该等待来自 MagnetoCorp 的三个节点去确定一个交易被提交并返回控制。连接选项允许应用程序去指定与网络交互的网关的精准行为。如果没有网关,应用程序需要去很多的工作;网关节省了你的时间,让你的应用程序更加可读,不易出错。

用法

我们将马上描述应用程序可用的连接选项的全集;首先让我们看看他们如何被示例 MagnetoCorp issue 应用指定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const userName = 'User1@org1.example.com';
const wallet = new FileSystemWallet('../identity/user/isabella/wallet');

const connectionOptions = {
  identity: userName,
  wallet: wallet,
  eventHandlerOptions: {
    commitTimeout: 100,
    strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
    }
  };

await gateway.connect(connectionProfile, connectionOptions);

看一下 identitywallet 选项是 connectionOptions 对象的简单属性。他们分别有 userName and wallet,他们较早的被设置在代码里。相比于这些属性,eventHandlerOptions 本身就是一个独立的对象。它有两个属性:commitTimeout: 100 (以秒为单位)和 strategy:EventStrategies.MSPID_SCOPE_ANYFORTX

看一下 connectionOptions 如何作为一个 connectionProfile 的补充被传递给网关的;网络被网络配置文件所标识,这些选项精确地指定了网关该如何与它交互。现在让我们看下可用选项。

选项

这里是可用选项的列表以及他们的作用。

  • wallet 标识了将要被应用程序上的网关所使用的钱包。看一下交互1,钱包是由应用程序指定的,但它实际上是检索身份的网关。

    一个钱包必须被指定;最重要的决定是钱包使用的类型,是文件系统、内存、HSM 还是数据库。

  • identity 应用程序从 wallet 中使用的用户身份。看一下交互2a;用户身份被应用程序指定,也代表了应用程序的用户 Isabella,2b。身份实际上被网络检索。

    在我们的例子中,Isabella 的身份将会被不同的 MSP(2c2d)使用,用于确定他为来自 MagnetoCorp 的一员,并且在其中扮演着一个特殊的角色。这两个事实将相应地决定了他在资源上的许可。

    一个用户的身份必须被指定。正如你所看到的,这个身份对于 Hyperledger Fabric 是一个有权限的网络的概念来说是基本的原则——所有的操作者有一个身份,包括应用程序、Peer 节点和排序节点,这些身份决定了他们在资源上的控制。你可以在成员身份服务话题中阅读更过关于这方面的概念。

  • clientTIsIdentity 是可以从钱包(3a)获取到的身份,用于确保网关和不同的t通道组件之间的交流(3b),比如 Peer 节点和排序节点。

    注意:这个身份不同于用户身份。即使 clientTlsIdentity 对于安全通信来说很重要,但它并不像用户身份那样基础,因为它的范围没有超过确保网络的通信。

    clientTlsIdentity 是可选项。建议你把它设置进生产环境中。你应该也使用不同的 clientTlsIdentity 用做 identity,因为这些身份有着非常不同的意义和生命周期。例如,如果 clientTIsIdentity 被破坏,那么你的 identity 也会被破坏;让他们保持分离会更加安全。

  • eventHandlerOptions.commitTimeout 是可选的。它以秒为单位指定网关在将控制权返回给应用程序之前,应该等待任何对等方提交事务的最大时间量(4a)。用于通知的 Peer 节点集合由 eventHandlerOptions 选项决定。如果没有指定 commitTimeout,网关默认将使用300秒的超时。

  • eventHandlerOptions.strategy 是可选的。它定义了网关用来监听交易被提交通知的 Peer 节点集合。例如,是监听组织中的单一的 Peer 节点还是全部的节点。可以采用以下参数之一:

    • EventStrategies.MSPID_SCOPE_ANYFORTX 监听用户组织内的一些 Peer 节点。在我们的例子中,看一下交互点4b;MagnetoCorp 中的 peer1、peer2、peer3 节点能够通知网关。

    • EventStrategies.MSPID_SCOPE_ALLFORTX 这是一个默认值。监听用户组织内的所有 Peer 节点。在我们例子中,看一下交互点4b。所有来自 MagnetoCorp 的节点必须全部通知网关:peer1、peer2 和 peer3 节点。只有已知/被发现和可用的 Peer 节点才会被计入,停止或者失效的节点不包括在内。

    • EventStrategies.NETWORK_SCOPE_ANYFORTX 监听整个网络通道内的一些 Peer 节点。在我们的例子中,看一下交互点 4b4c;MagnetoCorp 中部分 Peer 节点1-3 或者 DigiBank 的部分 Peer 节点 7-9 能够通知网关。

    • EventStrategies.NETWORK_SCOPE_ALLFORTX 监听整个网络通道内所有 Peer 节点。在我们的例子中,看一些交互 4b4c。所有来自 MagnetoCorp 和 DigiBank 的 Peer 节点 1-3 和 Peer 节点 7-9 都必须通知网关。只有已知/被发现和可用的 Peer 节点才会被计入,停止或者失效的节点不包括在内。

    • <PluginEventHandlerFunction> 用户定义的事件处理器的名字。这个允许用户针对事件处理而定义他们自己的逻辑。看一下如何定义一个时间处理器插件,并检验一个示例处理器

      如果你有一个迫切需要事件处理的需求的话,那么用户定义的事件处理器是有必要的;大体上来说,一个内建的事件策略是足够的。一个用户定义的事件处理器的例子可能是等待超过半数的组织内的 Peer 节点,以去确认交易被提交。

    如果你确实指定了一个用户定义的事件处理器,她不会影响你的一个应用程序的逻辑;它是完全独立的。处理器被处理过程中的SDK调用;它决定了什么时候调用它,并且使用它的处理结果去选择哪个 Peer 节点用于事件通知。当SDK完成它的处理的时候,应用程序收到控制。

    如果一个用户定义的事件处理器没有被指定,那么 EventStrategies的默认值将会被使用。

  • discovery.enabled 是可选项,可能的值是 true 还是 false。默认是 ture。它决定了网关是否使用服务发现去增加连接配置文件里指定的网络拓扑。看一下交互点6;peer 节点的 gossip 信息会被网关使用。

    这个值会被 INITIALIIZE-WITH-DISCOVERY 的环境变量覆盖,其值可以被设置成 true 或者 false

  • discovery.asLocalhost 是可选项,可能的值是 true或者 false。默认是true。它决定在服务发现期间发现的 IP 地址是否从 docker 网络传递给本地主机。

    通常,开发人员将为其网络组件(如 Peer 节点、排序节点 和 CA 节点)编写使用 docker 容器的应用程序,但是他们自己本身不会运行在 docker 容器内。这个就是为什么 true 是默认的,这生产环境中,应用程序很可能以网络组件相同的方式运行在 docker 中,因此不需要地址转换。在这种情况下,应用程序应该要不明确指定 false,要不使用环境变量来覆盖。

    这个值会被 DISCOVERY-AS-LOCALHOST 环境变量覆盖,其值可以被设置成 true 或者 false

注意事项

当打算选择连接选项的时候,下面的事项列表是有帮助的。

  • eventHandlerOptions.commitTimeouteventHandlerOptions.strategy 协同工作。例如,commitTimeout: 100strategy:EventStrategies.MSPID_SCOPE_ANYFORTX 意味着网关最多等待100秒,使得一些 Peer 节点确定交易被提交。相反,指定 strategy: EventStrategies.NETWORK_SCOPE_ALLFORTX 意味着网关将会等待所有组织里的所有 Peer 节点100秒

  • eventHandlerOptions.strategy:EventStrategies.MSPID_SCOPE_ALLFORTX 的默认值将等待应用程序内的组织的所有 Peer 节点提交提交交易。这是一个好的默认设置,因为应用程序能够确保所有的节点有一个最新账本的拷贝,最小的并发性问题。

    然而,当组织内的 Peer 节点数量增加,等待所有 Peer 节点则没有必要,在这种情况下,使用一个可插拔的事件处理器能够提供更多有效果的策略。例如,在一个公式能够保持所有帐本同步的安全假设下,相同的 Peer 节点集合能够用于提交交易和监听通知。

  • 服务发现要求 clientTlsIdentity 被设置。因为与应用程序有交换信息的 Peer 节点需要确信他们在与信任的实体交换信息。如果 clientTlsIdentity 没有被设置,那么 discovery 不会生效,不管它是否被设置。

  • 即使应用程序在连接网关时能够设置连接选项,但是管理员可能需要覆盖这些选项。这是因为选项与网络交互有关,而网络交互可能随时间而变化。例如,管理员试图了解使用服务发现对网络性能的影响。

    一个好的方式是在一个配置文件中定义应用程序覆盖,此文件在配置与网关的连接的时候,会被应用程序读取。

    因为服务发现选项 enabledasLocalHost 最容易被管理员频繁的覆盖,环境变量 INITIALIIZE-WITH-DISCOVERYDISCOVERY-AS-LOCALHOST 是因为方便而提供的。管理员应该在应用程序的生产环境中设置他们,其生产环境很可能是一个 docker 容器。

钱包⭐

一个钱包包括了一组用户身份。当与通道连接的时候,应用程序会从这些身份中选择一个用户来运行。对通道资源的访问权限,比如账本,由与 MSP(Membership provider)相关联的这个身份所定义。

在本主题中,我们将涉及:

  • 为什么钱包是重要的
  • 钱包是如何被组织的
  • 钱包的不同类型
  • 钱包的操作

情景

当应用程序连接到一个比如像 PaperNet 的网络通道的时候,它选择一个用户身份去这么做,例如 ID1通道的 MSP 将 ID1 与组织内特定的角色相关联,而且这个角色将最终决定应用程序在通道上的权限。

两个用户, Isabella 和 Balaji 拥有包含能够用于连接不同网络通道的不同身份的钱包,PaperNet 和 BondNet。

思考一下这两个用户的例子;来自于 MagnetoCorp 的 lsabella 和来自 DigiBank 的 Balaji。Isabella 打算使用应用程序1去调用 PaperNet 里的智能合约和一个 BondNet 的智能合约。同理,Balaji 打算使用应用程序2去调用智能合约,但是这是在 PaperNet 中。(对于应用程序来说,访问多个网络和其中多个的智能合约是简单的。)

看一下:

  • MagnetoCorp 使用 CA1 去颁发身份,DigiBank 使用 CA2 去颁发身份。这些身份被存储在用户的钱包中。
  • Balaji 的钱包拥有一个唯一的身份, ID4 由 CA2 发行。Isabella 的钱包拥有多种被 CA1 颁发的身份,ID1ID2ID3钱包能够让一个用户拥有多个身份,并且每一个身份都能由不同的 CA 颁发
  • Isabella 和 Balaji 都连接到 PaperNet,它(PaperNet)的 MSP 确定了 Isabella 是 MagnetoCorp 的成员,且确定了 Balaji 是 DigiBank 组织的一员,因为信任颁发他们身份的 CA。(一个组织使用多个 CA 或者一个 CA 支持多个组织是可能的
  • Isabella 能够使用 ID1 去连接 PaperNet 和 BondNet。在这两种情况下,当 Isabella 使用这个身份时候,她会被是识别为 MangetoCorp 的一员。
  • Isabella 能够使用 ID2 去连接 BondNet,在这种情况下,她被标识为 MagnetoCorp 的管理员。这给了 Isabella 两种不同的权利:ID1 把她标识为能够读写 BondNet 账本的 MagnetoCorp 的普通一员,然而 ID2 标识她为能够给 BondNet 添加组织的 MagnetoCorp 的管理员。
  • Balaji 不能够使用 ID4 连接 BondNet。如果他尝试去连接,ID4 将不会被认可其属于 DigiBank,因为 CA2 不知道 BondNet 的 MSP。

类型

根据他们身份存储的位置,会有不同的钱包的类型

三种不同的钱包类型:文件系统、内存和 CouchDB

  • 文件系统(FileSystem):这是存储钱包最常见的地方;文件系统是无处不在的、容易理解且可以挂载在网络上。对于钱包来说,这是很好的默认选择
  • 内存(In-memory):存储在应用程序里的钱包。当你的应用程序正在运行在一个没有访问文件系统的约束环境的时候,使用这种类型的钱包;有代表性的是 web 浏览器。需要记住的是这种类型的钱包是不稳定的;在应用程序正常结束或者崩溃的时候,身份将失去丢失。
  • CouchDB:存储在 Couch DB 的钱包。这是最罕见的一种钱包存储形式,但是对于想去使用数据备份和恢复机制的用户来说,CouchDB 钱包能够提供一个有用的选择去简化崩溃的恢复。

使用 Wallets 提供的工厂方法来创建钱包。

硬件安全模块

硬件安全模块(Hardware Security Module,HSM)是一种超安全、防篡改的设备, 它存储数字身份信息,特别是私钥,HSM可以本地连接到您的计算机或者可访问的网络。大多数HSM提供使用私钥执行机上加密的能力,正是如此所以私钥从不与HSM分离。

HSM可以用于任何类型的钱包。在本例中身份证书将被存储在钱包中,私钥将被存储于HSM中。

要启用HSM管理身份的功能,必须使用HSM连接信息配置IdentityProvider,并将其注册到钱包。 要了解更多细节, 请参考使用钱包管理身份教程。

结构

一个单一的钱包能够保存多个身份,每一个都被一个被指定的证书机构发放。每一个身份都有一个规范的带有描述性标签的结构,一个包括公钥、私钥与一些 Fabric-specific 元数据的 X.509 证书。不同的钱包类型 将这个结构合理地映射到他们地存储机制上。

Fabric 钱包能够持有多个被不同证书机构颁发的身份,身份包含了证书、私钥和一些 Fabric 元数据

这是几个关键的类方法,使得钱包和身份更容易被管理:

1
2
3
4
5
6
7
8
9
const identity: X509Identity = {
    credentials: {
        certificate: certificatePEM,
        privateKey: privateKeyPEM,
    },
    mspId: 'Org1MSP',
    type: 'X.509',
};
await wallet.put(identityLabel, identity);

看看如何创建一个identity,它有元数据Org1MSP,一个certificate和 一个privateKey。看看wallet.put()如何将此身份添加到带有一个特定的identityLabel的钱包。

在上面的例子中,Gateway类只要求mspIdtype元数据被设置为一个身份——Org1MSPX.509。它当前使用MSP ID值认证 connection profile中的特定peer节点, 例如,当一个特定的通知strategy被请求。 在DigiBank gateway文件networkConnection.yaml中,看看 Org1MSP通知将如何与peer0.org1.example.com关联:

1
2
3
4
5
6
organizations:
  Org1:
    mspid: Org1MSP

    peers:
      - peer0.org1.example.com

你真的不需要担心内部不同的钱包类型的结构,但是如果你感兴趣,导航到商业票据例子里的用户身份文件夹

1
2
3
magnetocorp/identity/user/isabella/
                                  wallet/
                                        User1@org1.example.com.id

你能够检查这些文件,但是正如讨论的,它很容易去使用 SDK 实现这些数据。

选项

不同的钱包类型都实现了一个公共的 Wallet接口, 该接口提供了一组标准api来管理身份标识。这意味着其他应用程序可以独立于底层钱包存储机制;例如,文件系统和HSM钱包都以高度类似的方式进行处理。

钱包遵循一个生命周期:他们能够被创建、被打开,身份能够被读取、添加和删除。

应用程序能够根据一个简单的生命周期去使用钱包。钱包能够被打开、创建,随后可以添加、读取、更新和删除身份。花费一些时间在 JSDOC 中不同的 Wallet 方法上来看一下他们是如何工作的;商业票据的教程在 addToWallet.js 中提供了一个很好的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const wallet = await Wallets.newFileSystemWallet('../identity/user/isabella/wallet');

const cert = fs.readFileSync(path.join(credPath, '.../User1@org1.example.com-cert.pem')).toString();
const key = fs.readFileSync(path.join(credPath, '.../_sk')).toString();

const identityLabel = 'User1@org1.example.com';
const identity = {
    credentials: {
        certificate: cert,
        privateKey: key,
    },
    mspId: 'Org1MSP',
    type: 'X.509',
};

await wallet.put(identityLabel, identity);

注意:

  • 当程序第一次运行的时候,钱包被创建在本地文件系统 .../isabella/wallet
  • 证书密钥从文件系统中下载。
  • 一个新的身份使用证书,密钥Org1MSP 来创建。
  • 新的身份通过 wallet.put() 加入的钱包中,并附带 User1@org1.example.com 标签。

这是关于钱包你所要知道的所有事情。你已经看到用户代表在访问 Fabric 网络资源上,钱包如何持有被应用程序使用的身份的。更具你的应用程序和安全需要,这里有一些有用的不同类型的钱包,和一套简单的 API 去帮助应用程序去管理其内部的钱包和身份。

网关

网关代表应用程序管理与区块链网络的交互,使其能够专注于业务逻辑。应用程序连接到网关,然后使用该网关的配置管理所有后续交互。

本主题,将包括如下内容:

  • 为什么网关很重要
  • 应用程序如何使用网关
  • 如何定义静态网关
  • 如何为服务发现定义动态网关
  • 使用多个网关

情景

Hyperledger Fabric 网络通道可以不断变化。Peer 节点、排序节点和 CA 组件,由网络中的不同组织提供,会经常变化。造成这种情况的原因包括业务需求增加或减少,以及计划内和计划外中断。网关为应用解除这种负担,使其能够专注于它试图解决的业务问题

MagnetoCorp 和 DigiBank 应用程序(发行和购买)将各自的网络交互委托给其网关。每个网关都了解网络信道拓扑,包括 MagnetoCorp 和 DigiBank 两个组织的多个 Peer 节点 和 排序节点,使应用程序专注于业务逻辑。Peer 节点可以使用 gossip 协议在组织内部和组织之间相互通信。

应用程序可以通过两种不同的方式使用网关:

  • 静态:网关配置完全定义在连接配置中。应用程序可用的所有 Peer 节点、排序节点和 CA 在用于配置网关的连接配置文件中静态定义。比如对 Peer 节点来说,这包括他们作为背书节点或事件通知节点的角色配置。你可以在连接配置主题中了解到更多关于角色的内容。

    SDK 将使用此静态拓扑并结合网关连接配置来管理交易提交和通知过程。连接配置必须包括足够多的网络拓扑来允许网关以应用程序的身份和网络进行交互;包括网络通道、组织、排序节点、Peer 节点和他们的角色。

  • 动态:网关配置被最小化的定义在连接配置中。通常,指定应用程序组织中的一个或两个 Peer 节点,然后使用服务发现来发现可用的网络拓扑。这包括 Peer 节点, 排序节点、通道、部署过的智能合约和他们的背书策略。(在生产环境中,网关配置需要至少指定两个可用的 Peer 节点。)

    SDK 将使用静态的配置和发现的拓扑信息,结合网关连接选项来管理交易提交和通知过程。作为其中的一部分,它还将智能地使用发现的拓扑;比如,它会使用为智能合约发现的背书策略来计算需要的最少背书节点。

你可能会问静态动态网关哪一个更好?这取决于对可预测性和响应性的权衡。静态网络将始终以相同的方式运行,因为它们将网络视为不变的。从这个意义上讲,它们是可预测的——如果它们可用,它们将始终使用相同的 Peer 节点和排序节点。动态网络在了解网络如何变化时更具响应性——它们可以使用新添加的 Peer 节点和排序节点,从而带来额外的弹性和可扩展性,可能会带来一定的可预测性成本。一般情况下,使用动态网络比较好,实际上这是网关的默认模式

请注意,静态或动态可以使用相同的连接配置文件。显然,如果配置文件被静态网关使用,它需要是全面的,而动态网关使用只需要很少的配置

这两种类型的网关对应用程序都是透明的;无论是静态网关还是动态网关应用程序设计无序改变。意思就是一些应用可以使用服务发现,另外一些可以不使用。通常,SDK 使用动态发现意味着更少的定义和更加的智能;而这是默认的。

连接

当应用连接到一个网关,需要提供两个配置项。在随后的 SDK 处理程序中会使用到:

1
  await gateway.connect(connectionProfile, connectionOptions);
  • 连接配置:不管是静态网关还是动态网关,connectionProfile 是被 SDK 用来做交易处理的网关配置项,它可以使用 YAML 或 JSON 进行配置,尽管在传给网关后会被转换成 JSON 对象。

    1
    
    let connectionProfile = yaml.safeLoad(fs.readFileSync('../gateway/paperNet.yaml', 'utf8'));
    

    阅读更多关于连接配置的信息,了解如何配置。

  • 连接选项connectionOptions 允许应用程序声明而不是实现所需的交易处理行为。连接选项被 SDK 解释以控制与网络组件的交互模式,比如选择哪个身份进行连接,使用哪个节点做事件通知。这些选项可在不影响功能的情况下显著地降低应用复杂度。这是可能的,因为 SDK 已经实现了许多应用程序所需的低级逻辑; 连接选项控制逻辑流程。

    阅读可用的连接选项列表以及何时使用它们。

静态网关

静态网关定义了一个固定的网络视图。在 MagnetoCorp 情景中,网关可以识别来自 MagnetoCorp 的单个 Peer 节点,来自 DigiBank 的单个 Peer 节点以及 MagentoCorp 的排序节点。或者,网关可以定义来自 MagnetCorp 和 DigiBank 的所有 Peer 节点和排序节点。在两个案例中,网关必须定义充分的网络视图来获取和分发商业票据的交易背书。

应用可以通过在 gateway.connect() API 上明确指定连接选项 discovery: { enabled:false } 来使用静态网关。或者,在环境变量中设置 FABRIC_SDK_DISCOVERY=false,将始终覆盖应用程序的选择。

检查被 MagnetoCorp 发行票据应用使用的连接选项。了解一下所有的 Peer 节点、排序节点和 CA 是如何配置的,包括他们的角色。

值得注意的是,静态网关代表这一刻的网络视图。随着网络的变化,在网关文件中反映变更可能很重要。当重新加载网关配置文件时,应用程序将会自动生效这些变更。

动态网关

动态网关为网络定义一个小的固定起点。在 MagnetoCorp 场景中,动态网关可能只识别来自 MagnetoCorp 的单个 Peer 节点; 其他一切都将被发现!(为了提供弹性,最好定义两个这样的引导节点。)

如果应用程序选择了服务发现,则网关文件中定义的拓扑将使用此进程生成的拓扑进行扩充。服务发现从网关定义开始,使用 gossip 协议查找 MagnetoCorp 组织内的所有连接的 Peer 节点和排序节点。如果已为某个通道定义了锚节点,则服务发现将使用跨组织的 goosip 协议来发现已连接组织的组件。此过程还将发现安装在 Peer 节点上的智能合约及其在通道级别定义的背书策略。与静态网关一样,发现的网络必须足以获取和分发商业票据交易背书。

动态网关是 Fabric 应用的默认设置。可在 gateway.connect() API 上明确的指定连接选项配置 discovery: { enabled:true }。或者,设置环境变量 FABRIC_SDK_DISCOVERY=true,将会覆盖应用程序的选择。

动态网络代表了一个与时俱进的网络视图。随着网络的改变,服务发现将会确保网络视图精确反映应用程序可见的拓扑。应用程序将会自动生效这些变更;甚至不需要重载网关配置文件。

多网关

最后,应用程序可以直接为相同或不同的网络定义多个网关。此外,应用程序可以静态和动态地使用命名网关。

拥有多个网关可能会有所帮助。 原因如下:

  • 代表不同用户处理请求。
  • 同时连接不同的网络。
  • 通过同时将其行为与现有配置进行比较来测试网络配置。
Built with Hugo
Theme Stack designed by Jimmy