Hyperledger Fabric 2.x 自定义智能合约

Hyperledger Fabric 2.x 自定义智能合约

一、说明

为了持续的进行信息的更新,以及对账本进行管理(写入交易,进行查询等),区块链网络引入了智能合约来实现对账本的访问和控制;智能合约在 Fabric 中称之为 链码,是区块链应用的业务逻辑。

本文分享如何使用 Java 语言开发智能合约,以及合约的安装与使用。

 

二、环境准备

1、部署好 Fabric 的测试网络,按照上一篇文章《Hyperledger Fabric 2.x 环境搭建》的内容执行第1至5步

- 启动好两个 peer 节点和一个 orderer 节点
- 创建好 mychannel 通道

Hyperledger Fabric 2.x 自定义智能合约

2、在环境变量中配置好执行命令(bin)、配置(config)与MSP文件夹的路径:
执行 vim /etc/profile 添加以下内容:

export FABRIC_PATH=/opt/gopath/src/github.com/hyperledger/fabric-samples
export FABRIC_CFG_PATH=${FABRIC_PATH}/config/
export MSP_PATH=${FABRIC_PATH}/test-network/organizations
export CORE_PEER_TLS_ENABLED=true
export PATH=${FABRIC_PATH}/bin:$PATH
FABRIC_PATH路径按实际进行修改。

Hyperledger Fabric 2.x 自定义智能合约

 

三、下载合约代码

gitee:https://gitee.com/zlt2000_admin/my-fabric-chaincode-java

github:https://github.com/zlt2000/my-fabric-chaincode-java

 

四、代码解析

Fabric 2.x 版本后的合约编写方式与旧版本略有不同,需要实现 ContractInterface 接口,下面是官方的一段说明:

All chaincode implementations must extend the abstract class ChaincodeBase. It is possible to implement chaincode by extending ChaincodeBase directly however new projects should implement org.hyperledger.fabric.contract.ContractInterface and use the contract programming model instead.

4.1. pom.xml文件

配置远程仓库

<repositories>
        <repository>
                <id>central</id>
                <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
                <releases>
                        <enabled>true</enabled>
                </releases>
                <snapshots>
                        <enabled>false</enabled>
                </snapshots>
        </repository>
        <repository>
                <id>jitpack.io</id>
                <url>https://www.jitpack.io</url>
        </repository>
        <repository>
                <id>artifactory</id>
                <url>https://hyperledger.jfrog.io/hyperledger/fabric-maven</url>
        </repository>
</repositories>

 

依赖合约sdk

<dependency>
        <groupId>org.hyperledger.fabric-chaincode-java</groupId>
        <artifactId>fabric-chaincode-shim</artifactId>
        <version>${fabric-chaincode-java.version}</version>
</dependency>

 

通过插件 maven-shade-plugin 指定 mainClassorg.hyperledger.fabric.contract.ContractRouter

新版本所有合约的 mainClass 都为 org.hyperledger.fabric.contract.ContractRouter
<build>
        <sourceDirectory>src/main/java</sourceDirectory>
        <plugins>
                <plugin>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>3.1</version>
                        <configuration>
                                <source>${java.version}</source>
                                <target>${java.version}</target>
                        </configuration>
                </plugin>
                <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-shade-plugin</artifactId>
                        <version>3.1.0</version>
                        <executions>
                                <execution>
                                        <phase>package</phase>
                                        <goals>
                                                <goal>shade</goal>
                                        </goals>
                                        <configuration>
                                                <finalName>chaincode</finalName>
                                                <transformers>
                                                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                                                <mainClass>org.hyperledger.fabric.contract.ContractRouter</mainClass>
                                                        </transformer>
                                                </transformers>
                                                <filters>
                                                        <filter>
                                                                <!-- filter out signature files from signed dependencies, else repackaging fails with security ex -->
                                                                <artifact>*:*</artifact>
                                                                <excludes>
                                                                        <exclude>META-INF/*.SF</exclude>
                                                                        <exclude>META-INF/*.DSA</exclude>
                                                                        <exclude>META-INF/*.RSA</exclude>
                                                                </excludes>
                                                        </filter>
                                                </filters>
                                        </configuration>
                                </execution>
                        </executions>
                </plugin>
        </plugins>
</build>

 

4.2. model

创建合约的数据对象 User 使用 @DataType 注解标识,定义三个字段 userIdnamemoney 使用 @Property 注解标识:

@DataType
public class User {
    @Property
    private final String userId;

    @Property
    private final String name;

    @Property
    private final double money;

    public User(final String userId, final String name, final double money) {
        this.userId = userId;
        this.name = name;
        this.money = money;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if ((obj == null) || (getClass() != obj.getClass())) {
            return false;
        }
        User other = (User) obj;
        return Objects.deepEquals(
                new String[] {getUserId(), getName()},
                new String[] {other.getUserId(), other.getName()})
                &&
                Objects.deepEquals(
                        new double[] {getMoney()},
                        new double[] {other.getMoney()});
    }

    @Override
    public int hashCode() {
        return Objects.hash(getUserId(), getName(), getMoney());
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }

    public String getUserId() {
        return userId;
    }

    public String getName() {
        return name;
    }

    public double getMoney() {
        return money;
    }
}

 

4.3. 合约逻辑

  1. 合约类使用 @Contract@Default 注解标识,并实现 ContractInterface 接口
  2. 合约方法使用 @Transaction 注解标识

    Transaction.TYPE.SUBMIT 为 写入交易
    Transaction.TYPE.EVALUATE 为 查询
  3. 包含3个交易方法:initaddUsertransfer
  4. 包含2个查询方法:getUserqueryAll
@Contract(name = "mycc")
@Default
public class MyAssetChaincode implements ContractInterface {
    public  MyAssetChaincode() {}

    /**
     * 初始化3条记录
     */
    @Transaction(intent = Transaction.TYPE.SUBMIT)
    public void init(final Context ctx) {
        addUser(ctx, "1", "zlt",100D);
        addUser(ctx, "2", "admin",200D);
        addUser(ctx, "3", "guest",300D);
    }

    /**
     * 新增用户
     */
    @Transaction(intent = Transaction.TYPE.SUBMIT)
    public User addUser(final Context ctx, final String userId, final String name, final double money) {
        ChaincodeStub stub = ctx.getStub();
        User user = new User(userId, name, money);
        String userJson = JSON.toJSONString(user);
        stub.putStringState(userId, userJson);
        return user;
    }

    /**
     * 查询某个用户
     */
    @Transaction(intent = Transaction.TYPE.EVALUATE)
    public User getUser(final Context ctx, final String userId) {
        ChaincodeStub stub = ctx.getStub();
        String userJSON = stub.getStringState(userId);
        if (userJSON == null || userJSON.isEmpty()) {
            String errorMessage = String.format("User %s does not exist", userId);
            throw new ChaincodeException(errorMessage);
        }
        User user = JSON.parseObject(userJSON, User.class);
        return user;
    }

    /**
     * 查询所有用户
     */
    @Transaction(intent = Transaction.TYPE.EVALUATE)
    public String queryAll(final Context ctx) {
        ChaincodeStub stub = ctx.getStub();
        List<User> userList = new ArrayList<>();
        QueryResultsIterator<KeyValue> results = stub.getStateByRange("", "");
        for (KeyValue result: results) {
            User user = JSON.parseObject(result.getStringValue(), User.class);
            System.out.println(user);
            userList.add(user);
        }
        return JSON.toJSONString(userList);
    }

    /**
     * 转账
     * @param sourceId 源用户id
     * @param targetId 目标用户id
     * @param money 金额
     */
    @Transaction(intent = Transaction.TYPE.SUBMIT)
    public void transfer(final Context ctx, final String sourceId, final String targetId, final double money) {
        ChaincodeStub stub = ctx.getStub();
        User sourceUser = getUser(ctx, sourceId);
        User targetUser = getUser(ctx, targetId);
        if (sourceUser.getMoney() < money) {
            String errorMessage = String.format("The balance of user %s is insufficient", sourceId);
            throw new ChaincodeException(errorMessage);
        }
        User newSourceUser = new User(sourceUser.getUserId(), sourceUser.getName(), sourceUser.getMoney() - money);
        User newTargetUser = new User(targetUser.getUserId(), targetUser.getName(), targetUser.getMoney() + money);
        stub.putStringState(sourceId, JSON.toJSONString(newSourceUser));
        stub.putStringState(targetId, JSON.toJSONString(newTargetUser));
    }
}

 

五、打包合约代码

把合约源代码打包成压缩文件,用于后续安装:

peer lifecycle chaincode package mycc.tar.gz --path /opt/app/my-fabric-chaincode-java --lang java --label mycc

 

六、安装合约

在指定 peer 节点上安装链码,下面分别为两个机构安装。

6.1. 为机构peer0.org1安装合约

执行以下命令,设置 peer0.org1 环境:

export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${MSP_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${MSP_PATH}/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051

执行以下命令安装:

peer lifecycle chaincode install mycc.tar.gz

成功后返回:

2022-02-09 22:09:13.498 EST 0001 INFO [cli.lifecycle.chaincode] submitInstallProposal -> Installed remotely: response:<status:200 payload:"\nEmycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae\022\004mycc" > 
2022-02-09 22:09:13.498 EST 0002 INFO [cli.lifecycle.chaincode] submitInstallProposal -> Chaincode code package identifier: mycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae

 

6.2. 为机构peer0.org2安装合约

执行以下命令,设置 peer0.org2 环境:

export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${MSP_PATH}/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${MSP_PATH}/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

执行以下命令安装:

peer lifecycle chaincode install mycc.tar.gz

成功后返回:

2022-02-09 22:14:14.862 EST 0001 INFO [cli.lifecycle.chaincode] submitInstallProposal -> Installed remotely: response:<status:200 payload:"\nEmycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae\022\004mycc" > 
2022-02-09 22:14:14.862 EST 0002 INFO [cli.lifecycle.chaincode] submitInstallProposal -> Chaincode code package identifier: mycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae

查看安装的合约清单:

peer lifecycle chaincode queryinstalled

返回合约的 Package IDLabel

Installed chaincodes on peer:
Package ID: mycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae, Label: mycc

 

七、审批合约

当合约安装后,需经过机构的审批达成一致后才允许使用。
 

7.1. 为机构peer0.org1审批合约定义

执行以下命令,设置 peer0.org1 环境:

export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${MSP_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${MSP_PATH}/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051

执行以下命令审批合约:

peer lifecycle chaincode approveformyorg \
  -o localhost:7050 \
  --ordererTLSHostnameOverride orderer.example.com \
  --tls 
  --cafile ${MSP_PATH}/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \
  --channelID mychannel \
  --name mycc \
  --version 1.0 \
  --package-id mycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae \
  --sequence 1
package-id 的值按实际进行修改。

成功后返回:

2022-02-09 22:22:38.403 EST 0001 INFO [chaincodeCmd] ClientWait -> txid [2531db2811945a641947000cb15cfd19e0b72da594dfba994f5f79b6bc51bce2] committed with status (VALID) at localhost:7051

 

7.2. 为机构peer0.org2审批合约定义

执行以下命令,设置 peer0.org2 环境:

export CORE_PEER_LOCALMSPID="Org2MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${MSP_PATH}/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${MSP_PATH}/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp
export CORE_PEER_ADDRESS=localhost:9051

执行以下命令审批合约:

peer lifecycle chaincode approveformyorg \
  -o localhost:7050 \
  --ordererTLSHostnameOverride orderer.example.com \
  --tls \
  --cafile ${MSP_PATH}/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \
  --channelID mychannel \
  --name mycc \
  --version 1.0 \
  --package-id mycc:4c8dce2c7f746d26293ca8f27a3ccdec8d6438090f873f40f8ac9508c01973ae \
  --sequence 1
package-id 的值按实际进行修改。

成功后返回:

2022-02-09 22:22:47.711 EST 0001 INFO [chaincodeCmd] ClientWait -> txid [796a1e0a735e69425bcd5911bdf4b2a8003bbac977c5e60c769f84da6b86ef86] committed with status (VALID) at localhost:9051

 

7.3. 合约提交检查

检查合约的审批情况,是否可以向通道进行提交:

peer lifecycle chaincode checkcommitreadiness --channelID mychannel --name mycc --version 1.0 --sequence 1 --output json

返回:

{
    "approvals": {
        "Org1MSP": true,
        "Org2MSP": true
    }
}
代表 Org1 和 Org2 都审批通过

 

八、提交合约

执行以下命令,向通道提交合约:

peer lifecycle chaincode commit \
  -o localhost:7050 \
  --ordererTLSHostnameOverride orderer.example.com \
  --tls \
  --cafile ${MSP_PATH}/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \
  --channelID mychannel \
  --name mycc \
  --peerAddresses localhost:7051 \
  --tlsRootCertFiles ${MSP_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \
  --peerAddresses localhost:9051 \
  --tlsRootCertFiles ${MSP_PATH}/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \
  --version 1.0 \
  --sequence 1

成功后返回:

2022-02-09 22:22:57.445 EST 0001 INFO [chaincodeCmd] ClientWait -> txid [97ded758675113b9339dc9b378a13c0790ea3780855bb8f651758bfb007fc1ec] committed with status (VALID) at localhost:7051
2022-02-09 22:22:57.456 EST 0002 INFO [chaincodeCmd] ClientWait -> txid [97ded758675113b9339dc9b378a13c0790ea3780855bb8f651758bfb007fc1ec] committed with status (VALID) at localhost:9051

查看通道上已经提交的合约:

peer lifecycle chaincode querycommitted --channelID mychannel --name mycc --output json

返回:

{
    "sequence": 1,
    "version": "1.0",
    "endorsement_plugin": "escc",
    "validation_plugin": "vscc",
    "validation_parameter": "EiAvQ2hhbm5lbC9BcHBsaWNhdGlvbi9FbmRvcnNlbWVudA==",
    "collections": {},
    "approvals": {
        "Org1MSP": true,
        "Org2MSP": true
    }
}

 

九、测试智能合约

  1. 交易数据使用 peer chaincode invoke [flags] 命令,该命令将尝试向网络提交背书过的交易。
  2. 查询数据使用 peer chaincode query [flags],该命令不会生成交易。

由于 invoke 命令所需要的参数较多,所以我们先创建一个脚本命令。
执行 vim invoke.sh 添加以下内容:

peer chaincode invoke -o localhost:7050 \
  --ordererTLSHostnameOverride orderer.example.com \
  --tls \
  --cafile ${MSP_PATH}/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \
  -C mychannel \
  -n mycc \
  --peerAddresses localhost:7051 \
  --tlsRootCertFiles ${MSP_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \
  --peerAddresses localhost:9051 \
  --tlsRootCertFiles ${MSP_PATH}/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \
  -c ${1}

9.1. 初始化账本

执行以下命令,调用合约的 init 方法初始化3条账本记录:

sh invoke.sh '{"function":"init","Args":[]}'

 

9.2. 查询数据

需要连接其中一个 peer 节点进行数据查询

执行以下命令,设置 peer0.org1 环境:

export CORE_PEER_LOCALMSPID="Org1MSP"
export CORE_PEER_TLS_ROOTCERT_FILE=${MSP_PATH}/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
export CORE_PEER_MSPCONFIGPATH=${MSP_PATH}/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp
export CORE_PEER_ADDRESS=localhost:7051

执行下面命令,调用 queryAll 方法,查询所有数据:

peer chaincode query -C mychannel -n mycc -c '{"Args":["queryAll"]}'

执行后返回3条数据的数组:

[{"money":100.0,"name":"zlt","userId":"1"},{"money":200.0,"name":"admin","userId":"2"},{"money":300.0,"name":"guest","userId":"3"}]

 

执行下面命令,调用 getUser 方法传入 1 参数,查询单个数据:

peer chaincode query -C mychannel -n mycc -c '{"Args":["getUser", "1"]}'

执行后返回id为1的数据:

{"money":100,"name":"zlt","userId":"1"}

 

9.3. 新增数据

执行以下命令,调用 addUser 方法,新增一条id为4的记录:

sh invoke.sh '{"function":"addUser","Args":["4","test","400"]}'

 

9.4. 转账

执行以下命令,调用 transfer 方法,进行转账操作:

sh invoke.sh '{"function":"transfer","Args":["4","1","400"]}'

转账成功后,使用查询命令进行查看:

peer chaincode query -C mychannel -n mycc -c '{"Args":["queryAll"]}'

Hyperledger Fabric 2.x 自定义智能合约

上一篇:这才是下一代企业级数据库系统该有的样子!


下一篇:日志排查问题困难?分布式日志链路跟踪来帮你