Hyperledger Fabric 2.x 自定義智慧合約

zlt2000發表於2022-02-16

一、說明

為了持續地進行資訊的更新,以及對賬本進行管理(寫入交易,進行查詢等),區塊鏈網路引入了智慧合約來實現對賬本的訪問和控制;智慧合約在 Fabric 中稱之為 鏈碼,是區塊鏈應用的業務邏輯。

本文分享如何使用 Java 語言開發智慧合約,以及合約的安裝與使用。

 

二、環境準備

1、部署好 Fabric 的測試網路,按照上一篇文章《Hyperledger Fabric 2.x 環境搭建》的內容執行第1至5步

- 啟動好兩個 peer 節點和一個 orderer 節點
- 建立好 mychannel 通道

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路徑按實際進行修改。

 

三、下載合約程式碼

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"]}'

相關文章