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

file

一、說明

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

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

 

二、環境準備

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

- 啓動好兩個 peer 節點和一個 orderer 節點
- 創建好 mychannel 通道

file

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

file

 

三、下載合約代碼

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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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"]}'

file

 

掃碼關注有驚喜!

file

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章