通过Java Chaincode实例学习交易系统中基于Hyperledger Fabric帐户的钱包模型

这是Hyperledger Fabric Java Chaincode的教程。还有Go和NodeJS Chaincodes,我们将来可能会讨论它们。此外,我采用基于帐户的钱包模型作为示例,我们将使用Hyperledger Fabric构建你的第一个网络(BYFN,build your first network),作为我们的测试环境(v1.4网络)。

如果你不知道如何启动BYFN网络,我也会提供步骤,不用担心。但是如果你想了解BYFN是如何工作的,你可以参考:

基于帐户的钱包模型

基于帐户的钱包模型是交易系统的实现之一,一般而言系统中存在一些钱包,并且每个钱包包含可用于转账的一些值或代币。当然,实现的细节有很多变化,例如代币交换和多代币功能。

特征

在Java Chaincode中,我们将实现:

  • 1.用户钱包创建。
  • 2.用户钱包查询。
  • 3.钱包代币转账(即从一个钱包发送钱或代币到另一个钱包)。
  • [可选] Hyperledger Fabric中有两个数据库选项用于保存超级账本的world state:LevelDB和CouchDB,在本教程中,为了简单起见,我将使用LevelDB。
  • [可选]在上面,world state(或全局状态)不是超级账本本身,而是与超级账本分开,虽然它是派生的,可以随时从超级账本中恢复。此外,世界状态存储在数据库中以表示超级账本,使得这些节点不需要总是搜索超级账本以进行数据检索,并且可以更快地从数据库中检索数据(状态)。
  • [可选]在我的负载测试和一些关于Hyperledger Fabric性能的论文中,LevelDB的性能优于CouchDB。但与LevelDB中的简单键值查询相比,CouchDB可以支持可用于构建更复杂查询的丰富查询。

IDE——Java Chaincode开发工具

我们使用IntelliJ IDEA。此外,你需要JDK。请安装它们。

当然,如果你有自己的Java IDE,也可以使用它。但在本教程中,我们使用IntelliJ。

第1步,准备开发

让我们打开IntelliJ。

在IntelliJ中创建新项目Create New Project

选择左边的Gradle,然后:

输入你的GroupIdArtifactId。就我而言,我使用java_account_model_cc。接下来:

然后现在,你应该配置自己的项目位置。就我而言,我使用/Desktop/java_account_model_cc。单击完成Finish

1.1 settings.gradle

在左侧的项目文件中,你应该看到settings.gradle。让我们双击打开它:

输入以下内容:

1
rootProject.name = 'fabric-chaincode-gradle'
1.2 build.gradle

在左侧的项目文件面板中,可以看到build.gradle。让我们双击打开它:

然后,输入以下内容:

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
plugins {
id 'com.github.johnrengelman.shadow' version '2.0.3'
id 'java'
}

group 'org.hyperledger.fabric-chaincode-java'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
mavenLocal()
mavenCentral()
}

dependencies {
compile group: 'org.hyperledger.fabric-chaincode-java', name: 'fabric-chaincode-shim', version: '1.+'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

shadowJar {
baseName = 'chaincode'
version = null
classifier = null

manifest {
attributes 'Main-Class': 'org.hyperledger.fabric.chaincode.AccountBasedChaincode'
}
}

如果IntelliJ右下角有一个需要导入的Gradle项目Gradle project needs to be imported的弹出窗口,请选择导入更改Import Changes

1.3 Chaincode文件

在左侧的项目文件面板中,在src>main>java下,右键单击它并选择New>Java Class

在Name字段中输入org.hyperledger.fabric.chaincode.AccountBasedChaincode

然后,应该看到以下内容:

上面的AccountBasedChaincode(.java)是我们在Java中编写Chaincode的地方。

第2步——需求分析

在我们开始编码之前,让我们组织我们需要编码的内容。

特性部分所述,在Java Chaincode中,我们将实现:

  • 1.用户钱包创建
  • 2.用户钱包查询
  • 3.钱包代币转账(即从一个钱包向另一个钱包汇款)

从上面这个简单的要求,我们需要有以下类:

  • 1.钱包类
  • 2.Chaincode类——拥有Chaincode

我们的Chaincode应该提供以下功能:

  • 1.创建一个钱包。
  • 2.将代币从一个钱包转账到另一个钱包。
  • 3.获取(查询)钱包。
  • 4.Init函数,在实例化Chaincode时调用。
  • 5.Invoke函数,当用户想要调用函数(1)(2)或(3)时调用。

Init函数必须在Chaincode中实现,并且每当我们实例化或升级Chaincode时都会自动调用。通常,它用于初始化区块链中的一些数据。 Invoke函数用于接收所有用户函数调用,然后根据Invoke调用(调用)相应的函数(1)(2)或(3)。它就像一个路由器 ,将传入的请求路由到不同的路径。

第3步——钱包类

现在,我们创建并编写Wallet类。

org.hyperledger.fabric.chaincode下创建一个新包:

调用模型包Models,然后确定:

应该可以看到:

Models包下,创建一个新的Java类并将其命名为Wallet。(这次我没有详细说明,让大家自己尝试)

现在我们为wallet类编码:

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
package org.hyperledger.fabric.chaincode.Models;

public class Wallet {
private String walletId;
private Double tokenAmount;

public Wallet(String walletId, Double tokenAmount) {
this.walletId = walletId;
this.tokenAmount = tokenAmount;
}

private Wallet() {}

public String getWalletId() {
return walletId;
}

public Double getTokenAmount() {
return tokenAmount;
}

public void setWalletId(String walletId) {
this.walletId = walletId;
}

public void setTokenAmount(Double tokenAmount) {
this.tokenAmount = tokenAmount;
}
}

钱包具有用于识别特定钱包的wallet id和用于指定钱包拥有多少代币的数量。

请注意,在生产案例中,Wallet类应该更复杂。例如,你可能对tokenAmount使用BigDecimal数据类型而不是Double。此外,在我们的例子中,我们在整个交易系统中只支持一种代币类型(即只有一种代币)。

第4步——Chaincode类

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
package org.hyperledger.fabric.chaincode;
import java.util.List;
import org.hyperledger.fabric.chaincode.Models.Wallet;
import org.hyperledger.fabric.shim.ChaincodeBase;
import org.hyperledger.fabric.shim.ChaincodeStub;
import com.fasterxml.jackson.databind.ObjectMapper;

public class AccountBasedChaincode extends ChaincodeBase {
private class ChaincodeResponse {
public String message;
public String code;
public boolean OK;

public ChaincodeResponse(String message, String code, boolean OK) {
this.code = code;
this.message = message;
this.OK = OK;
}
}

private String responseError(String errorMessage, String code) {
try {
return (new ObjectMapper()).writeValueAsString(new ChaincodeResponse(errorMessage, code, false));
} catch (Throwable e) {
return "{\"code\":'" + code + "', \"message\":'" + e.getMessage() + " AND " + errorMessage + "', \"OK\":" + false + "}";
}
}

private String responseSuccess(String successMessage) {
try {
return (new ObjectMapper()).writeValueAsString(new ChaincodeResponse(successMessage, "", true));
} catch (Throwable e) {
return "{\"message\":'" + e.getMessage() + " BUT " + successMessage + " (NO COMMIT)', \"OK\":" + false + "}";
}
}

private String responseSuccessObject(String object) {
return "{\"message\":" + object + ", \"OK\":" + true + "}";
}

private boolean checkString(String str) {
if (str.trim().length() <= 0 || str == null)
return false;
return true;
}

@Override
public Response init(ChaincodeStub stub) {
return newSuccessResponse(responseSuccess("Init"));
}

@Override
public Response invoke(ChaincodeStub stub) {
String func = stub.getFunction();
List<String> params = stub.getParameters();
if (func.equals("createWallet"))
return createWallet(stub, params);
else if (func.equals("getWallet"))
return getWallet(stub, params);
else if (func.equals("transfer"))
return transfer(stub, params);
return newErrorResponse(responseError("Unsupported method", ""));
}

private Response createWallet(ChaincodeStub stub, List<String> args) {
if (args.size() != 2)
return newErrorResponse(responseError("Incorrect number of arguments, expecting 2", ""));
String walletId = args.get(0);
String tokenAmount = args.get(1);
if (!checkString(walletId) || !checkString(tokenAmount))
return newErrorResponse(responseError("Invalid argument(s)", ""));

double tokenAmountDouble = 0.0;
try {
tokenAmountDouble = Double.parseDouble(tokenAmount);
if(tokenAmountDouble < 0.0)
return newErrorResponse(responseError("Invalid token amount", ""));
} catch (NumberFormatException e) {
return newErrorResponse(responseError("parseInt error", ""));
}

Wallet wallet = new Wallet(walletId, tokenAmountDouble);
try {
if(checkString(stub.getStringState(walletId)))
return newErrorResponse(responseError("Existent wallet", ""));
stub.putState(walletId, (new ObjectMapper()).writeValueAsBytes(wallet));
return newSuccessResponse(responseSuccess("Wallet created"));
} catch (Throwable e) {
return newErrorResponse(responseError(e.getMessage(), ""));
}
}

private Response getWallet(ChaincodeStub stub, List<String> args) {
if (args.size() != 1)
return newErrorResponse(responseError("Incorrect number of arguments, expecting 1", ""));
String walletId = args.get(0);
if (!checkString(walletId))
return newErrorResponse(responseError("Invalid argument", ""));
try {
String walletString = stub.getStringState(walletId);
if(!checkString(walletString))
return newErrorResponse(responseError("Nonexistent wallet", ""));
return newSuccessResponse((new ObjectMapper()).writeValueAsBytes(responseSuccessObject(walletString)));
} catch(Throwable e){
return newErrorResponse(responseError(e.getMessage(), ""));
}
}

private Response transfer(ChaincodeStub stub, List<String> args) {
if (args.size() != 3)
return newErrorResponse(responseError("Incorrect number of arguments, expecting 3", ""));
String fromWalletId = args.get(0);
String toWalletId = args.get(1);
String tokenAmount = args.get(2);
if (!checkString(fromWalletId) || !checkString(toWalletId) || !checkString(tokenAmount))
return newErrorResponse(responseError("Invalid argument(s)", ""));
if(fromWalletId.equals(toWalletId))
return newErrorResponse(responseError("From-wallet is same as to-wallet", ""));

double tokenAmountDouble = 0.0;
try {
tokenAmountDouble = Double.parseDouble(tokenAmount);
if(tokenAmountDouble < 0.0)
return newErrorResponse(responseError("Invalid token amount", ""));
} catch (NumberFormatException e) {
return newErrorResponse(responseError("parseDouble error", ""));
}

try {
String fromWalletString = stub.getStringState(fromWalletId);
if(!checkString(fromWalletString))
return newErrorResponse(responseError("Nonexistent from-wallet", ""));
String toWalletString = stub.getStringState(toWalletId);
if(!checkString(toWalletString))
return newErrorResponse(responseError("Nonexistent to-wallet", ""));

ObjectMapper objectMapper = new ObjectMapper();
Wallet fromWallet = objectMapper.readValue(fromWalletString, Wallet.class);
Wallet toWallet = objectMapper.readValue(toWalletString, Wallet.class);

if(fromWallet.getTokenAmount() < tokenAmountDouble)
return newErrorResponse(responseError("Token amount not enough", ""));

fromWallet.setTokenAmount(fromWallet.getTokenAmount() - tokenAmountDouble);
toWallet.setTokenAmount(toWallet.getTokenAmount() + tokenAmountDouble);
stub.putState(fromWalletId, objectMapper.writeValueAsBytes(fromWallet));
stub.putState(toWalletId, objectMapper.writeValueAsBytes(toWallet));

return newSuccessResponse(responseSuccess("Transferred"));
} catch(Throwable e){
return newErrorResponse(responseError(e.getMessage(), ""));
}
}

public static void main(String[] args) {
new AccountBasedChaincode().start(args);
}
}

请注意,在生产案例中,代码应与我的不同。这些代码主要用于演示或教程目的。

第5步——在BYFN中运行Chaincode

5.1 安装相关项目

我们在本教程中使用Hyperledger Fabric v1.4。

首先,你可以按照官方说明安装相关项目:

5.2 Chaincode准备

然后,让我们切换到这个目录(假设你完成了上面安装相关项目部分,你应该拥有所有需要的文件和目录):

1
2
3
cd fabric-samples/chaincode/chaincode_example02/
mv java java_01
mkdir java

现在,将项目目录中的以下突出显示的文件复制到fabric-samples/chaincode/chaincode_example02/java/

5.3 建立网络
1
2
cd ../../first-network
./byfn.sh up -l java

运行此脚本后,可能需要等待片刻……

如果你看到以下错误(而不是其他错误),那就OKAY,继续执行下一步骤

1
2
!!!!!!!!!!!!!!! Query result on peer0.org1 is INVALID !!!!!!!!!!!!!!!!
================== ERROR !!! FAILED to execute End-2-End Scenario ==================

另外,在运行上面的命令之前,请记住启动Docker。如果你做错了什么,可以运行以下命令关闭网络,然后重新启动:

1
2
./byfn.sh down
./byfn.sh up -l java

现在,我们测试我们的Chaincode是否有效。

5.4 访问Cli

有一个自动创建的cli Docker容器,它是一个控制节点的命令行界面。

让我们访问cli:

1
docker exec -it cli bash

然后,设置某些程序使用的环境变量:

1
2
3
4
5
export CHANNEL_NAME=mychannel
export CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=peer0.org1.example.com:7051
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

你可以复制所有这些,然后粘贴到你的终端并按enter。这些环境变量用于让一些Hyperledger Fabric程序知道我们需要使用peer0.org1.example.com:7051来调用Chaincode函数。现在,我们创建了两个带有钱包ID的钱包,tomsam

1
2
peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["createWallet","tom","100"]}'
peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses peer0.org2.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"Args":["createWallet","sam","100"]}'

运行上面的每个命令后,你应该在终端中看到类似的内容:

1
2019-02-09 16:56:55.617 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 message:"{\"message\":\"Wallet created\",\"code\":\"\",\"OK\":true}"

现在,我们获得上面创建的两个钱包来验证它们是否存在于区块链中:

1
2
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["getWallet","tom"]}'
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["getWallet","sam"]}'

运行上面的每个命令后,您应该在终端中看到类似的内容:

1
2
"{\"message\":{\"walletId\":\"tom\",\"tokenAmount\":100.0}, \"OK\":true}"
"{\"message\":{\"walletId\":\"sam\",\"tokenAmount\":100.0}, \"OK\":true}"

在上面,我们可以看到之前创建的两个钱包可以被查询。他们都有100个代币。

接下来,我们进行转账交易,让我们将10个代币从tom钱包转移到sam钱包:

1
peer chaincode invoke -o orderer.example.com:7050 --tls true --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem -C $CHANNEL_NAME -n mycc --peerAddresses peer0.org1.example.com:7051 --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt -c '{"Args":["transfer","tom","sam","10"]}'

最后,让我们再次验证两个钱包:

1
2
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["getWallet","tom"]}'
peer chaincode query -C $CHANNEL_NAME -n mycc -c '{"Args":["getWallet","sam"]}'

你能看到下面的结果:

1
2
"{\"message\":{\"walletId\":\"tom\",\"tokenAmount\":90.0}, \"OK\":true}"
"{\"message\":{\"walletId\":\"sam\",\"tokenAmount\":110.0}, \"OK\":true}"

请注意,现在Tom的钱包仍然是90个令牌,而Sam的钱包有110个令牌,交易已完成并写入区块链超级账本中。

第6步——清理
1
2
exit
./byfn.sh down

然后,删除目录fabric-samples/chaincode/chaincode_example02/java并将目录java_01重命名为java

谢谢!希望尽快学习的请访问Fabric区块链开发详解,本课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、通道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、nodejs链码与应用开发的操作实践,是Nodejs工程师学习Fabric区块链开发的最佳选择。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、帐户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括帐户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文通过Java Chaincode实例学习交易系统中基于Hyperledger Fabric帐户的钱包模型