4.4 Hyperledger Fabric - 应用程序开发 - 智能合约处理

应用程序开发 - 智能合约处理

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

我们将介绍:

  • 什么是智能合约及其重要性
  • 如何定义智能合约
  • 如何定义交易
  • 如何进行交易
  • 如何在智能合约中表示业务对象
  • 如何在帐本中存储和检索对象

如果需要,你可以 下载示例,甚至可以在 本地运行。它是用 JavaScript 和 Java 编写的,但是逻辑是完全独立于语言的,因此你可以轻松地看到发生了什么!(该示例也将适用于 Go。)

1. 智能合约

智能合约定义业务对象的不同状态,并管理在这些不同状态之间移动对象的流程。智能合约之所以重要,是因为它们使架构师和智能合约开发人员能够定义关键业务流程和数据,这些关键业务流程和数据是在区块链网络中进行协作的不同组织之间共享的。

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

2. 实现语言

支持两个运行时,即 Java 虚拟机和 Node.js。这使你有机会使用 JavaScript,TypeScript,Java 或可以在这些受支持的运行时之一中运行的任何其他语言之一。

在 Java 和 TypeScript 中,注释或修饰符用于提供有关智能合约及其结构的信息。这样可以提供更丰富的开发经验-例如,可以强制执行作者信息或返回类型。在 JavaScript 中,必须遵循约定,因此,围绕自动确定的内容存在限制。

JavaScript 和 Java 都给出了示例。

3. 合约类

PaperNet 商业票据智能合约的副本包含在一个文件中。使用浏览器查看它,如果已下载,则在你喜欢的编辑器中将其打开。

你可能会从文件路径中注意到,这是 MagnetoCorp 的智能合约副本。 MagnetoCorp 和 DigiBank 必须就他们将要使用的智能合约的版本达成协议。目前,使用哪个组织的副本都没关系,它们都是一样的。

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

JavaScript
class CommercialPaperContract extends Contract {...}
Java
@Contract(...)
@Default
public class CommercialPaperContract implements ContractInterface {...}

CommercialPaperContract 类包含商业票据的交易定义 - 发行,购买和赎回。正是这些交易使商业票据得以存在并在其生命周期中移动。我们将很快检查这些交易,但现在就 JavaScript 而言,CommericalPaperContract 扩展了Hyperledger Fabric Contract

对于 Java,该类必须使用 @Contract(...) 注释进行修饰。这提供了提供有关合同的其他信息的机会,例如许可证和作者。@Default() 批注指示此合约类是默认合约类。能够将合约类别标记为默认合约类别在某些具有多个合约类别的智能合约中很有用。

如果你使用的是 TypeScript 实现,则有类似的 @Contract(...) 批注可以实现与 Java 中相同的目的。

有关可用注释的更多信息,请查阅可用的 API 文档:

这些类,批注和 Context 类在之前已引入范围:

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

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

还请注意,JavaScript 类构造函数如何使用其 超类 使用显式 合约名称 进行初始化:

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

对于 Java 类,构造函数为空白,因为可以在 @Contract() 注解中指定显式合约名称。如果不存在,则使用类名称。

最重要的是,org.papernet.commercialpaper 的描述性非常强 – 该智能合约是所有 PaperNet 组织对商业票据的公认定义。

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

4. 交易定义

在该类中,找到 issue 方法。

JavaScript
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}
Java
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx,
                             String issuer,
                             String paperNumber,
                             String issueDateTime,
                             String maturityDateTime,
                             int faceValue) {...}

Java 注解 @Transaction 用于将该方法标记为交易定义。 TypeScript 具有等效的注释。

每当调用此合约以发行商业票据时,便会控制此功能。回想一下如何通过以下交易创建商业票据 00001:

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

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

每当应用程序请求签发商业票据时,合约都会自动授予 issue 方法控制权。交易属性值通过相应的变量可供方法使用。请参阅 应用程序主题 中的示例应用程序,了解应用程序如何使用 Hyperledger Fabric SDK 提交交易。

你可能已经注意到 issue 定义中有一个额外的变量 ctx。它称为 交易上下文,并且始终是第一位。默认情况下,它维护与 交易逻辑 相关的按合约和按交易的信息。例如,它将包含 MagnetoCorp 的指定交易标识符,MagnetoCorp 颁发用户的数字证书以及对帐本 API 的访问。

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

JavaScript
createContext() {
  return new CommercialPaperContext()
}
Java
@Override
public Context createContext(ChaincodeStub stub) {
     return new CommercialPaperContext(stub);
}

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

JavaScript
class CommercialPaperContext extends Context {

  constructor() {
    super();
    // All papers are held in a list of papers
    this.paperList = new PaperList(this);
}
Java
class CommercialPaperContext extends Context {
    public CommercialPaperContext(ChaincodeStub stub) {
        super(stub);
        this.paperList = new PaperList(this);
    }
    public PaperList paperList;
}

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

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

购买交易:

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
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...}
Java
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx,
                           String issuer,
                           String paperNumber,
                           String currentOwner,
                           String newOwner,
                           int price,
                           String purchaseDateTime) {...}

赎回交易

Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Redeemer = DigiBank
Redeem time = 31 Dec 2020 12:00:00 EST
JavaScript
async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...}
Java
@Transaction
public CommercialPaper redeem(CommercialPaperContext ctx,
                              String issuer,
                              String paperNumber,
                              String redeemingOwner,
                              String redeemDateTime) {...}

在这两种情况下,请观察商业票据交易与智能合约方法定义之间的 1:1 对应关系。

所有 JavaScript 函数都使用 async 和 await 关键字,这些关键字使 JavaScript 函数可以被视为同步函数调用。

5. 交易逻辑

既然你已经了解了合约的结构和定义的交易方式,那么让我们关注智能合约中的逻辑。

回顾第一笔 issue 交易:

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

导致 issue 方法被调用:

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

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

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

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

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

  // Must return a serialized paper to caller of smart contract
  return paper.toBuffer();
}
Java
@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;
}

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

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

购买交易的逻辑更加复杂:

JavaScript
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
@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) 更改所有者之前,请查看交易如何检查 currentOwnerpaper 是否 TRADING。但是基本流程很简单 – 检查一些前提条件,设置新所有者,更新帐本上的商业票据,并将更新后的商业票据 (序列化为缓冲区) 作为交易响应返回。

你为什么不看看能否理解赎回交易的逻辑?

6. 表示一个对象

我们已经了解了如何使用 CommercialPaper 和 PaperList 类来定义和实现发行,购买和赎回交易。通过查看这些类的工作原理来结束本主题。

找到 CommercialPaper 类:

JavaScript

在文件 paper.js 中:

class CommercialPaper extends State {...}
Java

在文件 CommercialPaper.java 中:

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

此类包含商业票据状态的内存表示形式。查看 createInstance 方法如何使用提供的参数初始化新的商业票据:

JavaScript
static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
  return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue });
}
Java
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
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
Java
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
        faceValue,issuer,"");

了解每次调用发行交易的方式,都会创建一个包含交易数据的新的商业票据内存实例。

需要注意的几个要点:

  • 这是内存中的表示形式;稍后我们将看到它如何显示在帐本中。
  • CommercialPaper 类扩展了 State 类。 State 是一个应用程序定义的类,它为状态创建通用抽象。所有状态都有一个它们表示的业务对象类,一个复合键,可以序列化和反序列化等等。当我们在账本上存储多个业务对象类型时,State 可以使我们的代码更易读。检查 state.js 文件 中的 State 类。

票据在创建时会计算自己的键 - 访问帐本时将使用此键。键由 issuerpaperNumber 组合而成。

constructor(obj) {
  super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]);
  Object.assign(this, obj);
}

通过交易而不是票据类将票据移动到 ISSUED 状态。这是因为,智能合约支配着票据的生命周期状态。例如,import 交易可能会立即在 TRADING 状态下创建一组新文件。

CommercialPaper 类的其余部分包含简单的帮助程序方法:

getOwner() {
    return this.owner;
}

回忆一下智能合约如何使用这种方法在商业票据的生命周期中移动。例如,在赎回交易中,我们看到:

if (paper.getOwner() === redeemingOwner) {
  paper.setOwner(paper.getIssuer());
  paper.setRedeemed();
}

7. 访问账本

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

class PaperList extends StateList {

该通用程序类用于管理 Hyperledger Fabric 状态数据库中的所有 PaperNet 商业票据。 PaperList 数据结构在 体系结构主题 中有更详细的描述。

与 CommercialPaper 类类似,该类扩展了应用程序定义的 StateList 类,该类为状态列表创建通用抽象 - 在这种情况下,是 PaperNet 中的所有商业票据。

addPaper() 方法是对 StateList.addState() 方法的简单修饰:

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

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

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

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

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

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

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

async getState(key) {
  let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key));
  let data = await this.ctx.stub.getState(ledgerKey);
  let state = State.deserialize(data, this.supportedClasses);
  return state;
}
async updateState(state) {
  let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
  let data = State.serialize(state);
  await this.ctx.stub.putState(key, data);
}

了解他们如何使用 Fabric API 的 putState(),getState() 和 createCompositeKey() 来访问帐本。我们稍后将扩展此智能合约,以在 paperNet 中列出所有商业票据 - 实现此账本检索的方法将是什么样?

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

Reference

项目源代码

项目源代码会逐步上传到 Github,地址为 https://github.com/windstamp

Contributor

  1. Windstamp, https://github.com/windstamp
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容