基於Fisco-Bcos的區塊鏈智慧合約-簡單案例實踐
一、智慧合約介紹
智慧合約是指把合同/協議條款以程式碼的形式電子化地放到區塊鏈網路上。FISCO BCOS平臺支援兩種智慧合約型別:Solidity智慧合約與預編譯智慧合約
Solidity與Java類似。程式碼寫好後,都需要通過編譯器將程式碼轉換成二進位制,在Java中,編譯器是Javac,而對於Solidity,是solc。生成後的二進位制程式碼,會放到虛擬機器裡執行。Java程式碼在Java虛擬機器(JVM)中執行,在Solidity中,是一個區塊鏈上的虛擬機器EVM。目的,是給區塊鏈提供一套統一的邏輯,讓相同的程式碼跑在區塊鏈的每個節點上,藉助共識演算法,讓區塊鏈的資料以統一的方式進行改變,達到全域性一致的結果
設計目的:
為區塊鏈提供一套統一的邏輯,讓相同的程式碼跑在區塊鏈的每個節點上,藉助共識演算法,讓區塊鏈的資料以統一的方式進行改變,達到全域性一致的結果
Solidity 侷限與改進
- Solidity不夠靈活
受自身堆疊深度的限制,函式傳參和區域性引數的個數總和不能超過16個,Solidity是一種強型別的語言,但其型別轉換較為麻煩
- 效能差
底層儲存單位是32位元組(256 bits),對硬碟的讀寫要求較高,浪費了大量的儲存資源
針對上述兩點,FISCO BCOS提供了一種用C++寫合約方式:預編譯合約。開發者可以用C++編寫智慧合約邏輯,並將其內建在節點中,
預編譯合約突破了Solidity語言的限制,藉助強大的C++語言,可以靈活的實現各種邏輯,靈活性大大提高。同時,C++的效能優勢也得到了很好的利用,通過預編譯合約編寫的邏輯,相比於Solidity語言來說,效能得到提升
合約編寫
開發工具:remix-ide的使用,開發編譯過程選擇線上remix
Remix是功能強大的開源工具,可幫助您直接從瀏覽器編寫Solidity合同。Remix用JavaScript編寫,支援在瀏覽器和本地使用。
Remix還支援智慧合約的測試,除錯和部署等等。
優點:
1. 動態編譯、可調控編譯版本
2. 即時錯誤提醒
3. 程式碼自動補全
4. 釋出階段,程式碼問題提醒
5. 對設計方法的簡單呼叫
認識合約
例:
pragma solidity ^ 0.4.26; constant Sample{ //變數 address表示賬戶地址 address private _admin; uint private _state; //修飾符 ,為函式提供一些額外的功能,例如檢查、清理等工作 // 檢測函式的呼叫者是否為函式部署時設定的那個管理員(即合約的部署人) modifier onlyAdmin(){ require(msg.sender==_admin,"You are not admin"); _; } //事件 // 記錄事件定義的引數,儲存到區塊鏈交易的日誌中,提供廉價的儲存。 // 提供一種回撥機制,在事件執行成功後,由節點向註冊監聽的SDK傳送回撥通知,觸發回撥函式被執行。 // 提供一個過濾器,支援引數的檢索和過濾。 event SetState(unit valule); //構造方法 建構函式用於初始化合約 constructor() public { _admin=msg.sender; } //函式 方法 function setSate(unit value) public onlyAdmin(){ _state=value; emit SetState(value); } function getValue() public view return (uint){ return _state; } }
二、案列合約設計
邏輯如下:
定義:
- 定義事件方法AddEqu(string equnum, string data)
- 建構函式中建立t_equipment表
- 查詢方法:select(string equnum),根據裝置編號查詢裝置備案資訊,或使用記錄。( 成功返回0, 裝置不存在返回-1)
- addEqu(string equnum, string data),新增資料前校驗資料唯一性,已存在不在插入
Eqump合約類圖
Contract:Java與智慧合約進行互動的實體合約型別抽象
ManagedTransaction: 交易管理
Eqump合約核心程式碼
Eqump.sol
pragma solidity ^ 0.4.25; import "./Table.sol"; contract Eqump{ // event event AddEqu(string equnum, string data); // constructor() public { // 建構函式中建立t_equipment表 createTable(); } function createTable() private { TableFactory tf = TableFactory(0x1001); // 建立表 tf.createTable("t_equipment", "equnum", "data"); } function openTable() private view returns(Table) { TableFactory tf = TableFactory(0x1001); Table table = tf.openTable("t_equipment"); return table; } /* 描述 : 根據裝置管理資訊查詢裝置資訊 引數 : equ_num : 裝置編號 返回值: 引數一: 成功返回0, 裝置不存在返回-1 */ function select(string equnum) public view returns(int256, string) { // 開啟表 Table table = openTable(); // 查詢 Entries entries = table.select(equnum, table.newCondition()); if (0 == uint256(entries.size())) { return (-1, ""); } else { Entry entry = entries.get(0); return (0, entry.getString("data")); } } /* 描述 : 新增資訊 引數 : equnum : 案資訊主鍵 data : 資訊 返回值: 0 備案成功 -1 備案資訊已存在 -2 其他錯誤 */ function addEqu(string equnum, string data) public returns(int256){ int256 ret_code = 0; Table table = openTable(); Entries entries = table.select(equnum, table.newCondition()); if(0 == uint256(entries.size())) { Entry entry = table.newEntry(); entry.set("equnum", equnum); entry.set("data", data); // 插入 int count = table.insert(equnum, entry); if (count == 1) { // 成功 ret_code = 0; } else { // 失敗? 無許可權或者其他錯誤 ret_code = -2; } } else { // 備案資訊 ret_code = -1; } emit AddEqu(equnum, data); return ret_code; } }
pragma solidity ^0.4.24; contract TableFactory { function openTable(string) public constant returns (Table); // 開啟表 function createTable(string,string,string) public returns(int); // 建立表 } // 查詢條件 contract Condition { //等於 function EQ(string, int) public; function EQ(string, string) public; //不等於 function NE(string, int) public; function NE(string, string) public; //大於 function GT(string, int) public; //大於或等於 function GE(string, int) public; //小於 function LT(string, int) public; //小於或等於 function LE(string, int) public; //限制返回記錄條數 function limit(int) public; function limit(int, int) public; } // 單條資料記錄 contract Entry { function getInt(string) public constant returns(int); function getAddress(string) public constant returns(address); function getBytes64(string) public constant returns(byte[64]); function getBytes32(string) public constant returns(bytes32); function getString(string) public constant returns(string); function set(string, int) public; function set(string, string) public; function set(string, address) public; } // 資料記錄集 contract Entries { function get(int) public constant returns(Entry); function size() public constant returns(int); } // Table主類 contract Table { // 查詢介面 function select(string, Condition) public constant returns(Entries); // 插入介面 function insert(string, Entry) public returns(int); // 更新介面 function update(string, Entry, Condition) public returns(int); // 刪除介面 function remove(string, Condition) public returns(int); function newEntry() public constant returns(Entry); function newCondition() public constant returns(Condition); }
編譯釋出
WeBASE簡介:
WeBASE(WeBank Blockchain Application Software Extension) 是在區塊鏈應用和FISCO-BCOS節點之間搭建的一套通用元件。圍繞交易、合約、金鑰管理,資料,視覺化管理來設計各個模組,開發者可以根據業務所需,選擇子系統進行部署。WeBASE遮蔽了區塊鏈底層的複雜度,降低開發者的門檻,大幅提高區塊鏈應用的開發效率,包含節點前置、節點管理、交易鏈路,資料匯出,Web管理平臺等子系統。
過程
-
- 編譯釋出
- 測試驗證 發交易-->addEqu
{ equnum : y1 data:y1 }
發交易-->select
{ equnum:y1 }
基於web3sdk 除錯Eqump
1. 在IDE⾥編寫智慧合約。
2. 合約編寫完成後,拿到fisco ckient 命令⾏⼯具內進⾏編譯和⽣成java SDK的操作。 在/home/FISCO-BCOS/generator/console⽬錄下執⾏⼀下命令,將合約解析成java SDK⽂件。
sh sol2java.sh com.wg.service
特點
- - 輕量化配置,即可連線區塊鏈節點
- - 根據.sol 合約檔案,一鍵生成.abi 和 .bin檔案
- - 一鍵生成java 合約檔案
基於springboot-demo專案
applycation.yml
encrypt-type: # 0:普通, 1:國密 encrypt-type: 0 group-channel-connections-config: caCert: ca.crt sslCert: sdk.crt sslKey: sdk.key all-channel-connections: - group-id: 1 #group ID connections-str: # 若節點小於v2.3.0版本,檢視配置項listen_ip:channel_listen_port - 127.0.0.1:20200 # node channel_listen_ip:channel_listen_port - 127.0.0.1:20201 - group-id: 2 connections-str: # 若節點小於v2.3.0版本,檢視配置項listen_ip:channel_listen_port - 127.0.0.1:20202 # node channel_listen_ip:channel_listen_port - 127.0.0.1:20203 channel-service: group-id: 1 # sdk實際連線的群組 agency-name: fisco # 機構名稱
SSL連線配置
國密區塊鏈和非國密區塊鏈環境下,節點與SDK之間均可以建立SSL的連線,將節點所在目錄nodes/${ip}/sdk/目錄下的ca.crt、sdk.crt和sdk.key檔案拷貝到專案的資源目錄。(低於2.1版本的FISCO BCOS節點目錄下只有node.crt和node.key,需將其重新命名為sdk.crt和sdk.key以相容最新的SDK)
啟動
無異常,看到區塊鏈的版本、java環境地址、埠為正常啟動。
2020-07-17 09:13:21,417 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.handler.ConnectionCallback] ConnectionCallback.java:267 - support channel handshake node: Version [buildTime=20200602 03:35:56, buildType=Linux/clang/Release, chainID=1, version=2.4.1, gitBranch=HEAD, gitCommit=f6f2b4f12d5441e24c81a7c862691636c9cb3263, supportedVersion=2.4.1], content: {"id":0,"jsonrpc":"2.0","result":{"Build Time":"20200602 03:35:56","Build Type":"Linux/clang/Release","Chain Id":"1","FISCO-BCOS Version":"2.4.1","Git Branch":"HEAD","Git Commit Hash":"f6f2b4f12d5441e24c81a7c862691636c9cb3263","Supported Version":"2.4.1"}} 2020-07-17 09:13:21,422 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.handler.ConnectionCallback] ConnectionCallback.java:167 - channel protocol handshake success, set socket channel protocol, host: 10.2.23.16:20200, channel protocol: ChannelProtocol [protocol=3, nodeVersion=2.4.1, EnumProtocol=VERSION_3] 2020-07-17 09:13:21,424 [restartedMain] INFO [org.fisco.bcos.channel.client.Service] Service.java:373 - Connect to nodes: [10.2.23.16:20200] ,groupId: 1 ,caCert: class path resource [ca.crt] ,sslKey: class path resource [sdk.key] ,sslCert: class path resource [sdk.crt] ,java version: 1.8.0_151 ,java vendor: Oracle Corporation 2020-07-17 09:13:21,432 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.handler.ConnectionCallback] ConnectionCallback.java:338 - send update topic message request, seq: 89300763da2a4279bcb49b4b8187e477, content: ["_block_notify_1"] 2020-07-17 09:13:21,434 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.handler.ConnectionCallback] ConnectionCallback.java:370 - query block number host: 10.2.23.16:20200, seq: 0db7f13819ec425c8d9494cb68cd98cd, content: {"jsonrpc":"2.0","method":"getBlockNumber","params":[1],"id":1} 2020-07-17 09:13:21,440 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.handler.ConnectionCallback] ConnectionCallback.java:395 - query blocknumer, host:10.2.23.16:20200, blockNumber: 336
驗證
編寫單元測試
核心程式碼
package org.fisco.bcos; import org.fisco.bcos.constants.GasConstants; import org.fisco.bcos.solidity.Eqump; import org.fisco.bcos.web3j.crypto.Credentials; import org.fisco.bcos.web3j.protocol.Web3j; import org.fisco.bcos.web3j.tuples.generated.Tuple2; import org.fisco.bcos.web3j.tx.gas.StaticGasProvider; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.math.BigInteger; import static org.junit.Assert.assertTrue; /** * 財政部大型裝置合約單元測試 */ public class EqumpTest extends BaseTest { @Autowired private Web3j web3j; @Autowired private Credentials credentials; /** * 部署呼叫合約 * @throws Exception */ @Test public void deployAndCall() throws Exception { // deploy contract Eqump eqump = Eqump.deploy(web3j, credentials, new StaticGasProvider(GasConstants.GAS_PRICE, GasConstants.GAS_LIMIT)).send(); if (eqump != null) { System.out.println("Eqump address is: " + eqump.getContractAddress()); // call set function eqump.addEqu("1A2B","12312").send(); // call get function Tuple2<BigInteger, String> send = eqump.select("1A2B").send(); System.out.println(send.getValue1()); System.out.println(send.getValue2()); assertTrue("Eqump!".equals(send)); } } /** * 查詢 * @throws Exception */ @Test public void queryAndCall() throws Exception { // deploy contract Eqump eqump = Eqump.deploy(web3j, credentials, new StaticGasProvider(GasConstants.GAS_PRICE, GasConstants.GAS_LIMIT)).send(); if (eqump != null) { System.out.println("Eqump address is: " + eqump.getContractAddress()); // call set function // call get function Tuple2<BigInteger, String> send = eqump.select("y6").send(); System.out.println(send.getValue1()); System.out.println(send.getValue2()); } } }
核心業務程式碼
/** * 新增裝置資訊 * * @param dataArray * @throws InterruptedException */ private void addIpassItem(JSONArray dataArray) { System.out.println("===========================addIpassItem 新增裝置資訊業務開始================================"); try { Eqump eqump = Eqump.deploy(web3j, credentials, new StaticGasProvider(GasConstants.GAS_PRICE, GasConstants.GAS_LIMIT)).send(); for (int i = 0; i < dataArray.size(); i++) { List list = (List) dataArray.getJSONObject(i).get("equipmentInfor"); long startime = System.currentTimeMillis(); for (int j = 0; j < list.size(); j++) { JSONObject jobj = (JSONObject) list.get(j); String sbbh = StringUtil.validator(jobj.get("裝置編號")); String jsonStr = StringUtil.validator(jobj); if (eqump != null) { System.out.println("Eqump address is: " + eqump.getContractAddress()); eqump.addEqu(sbbh,jsonStr).send(); } } System.out.println("耗時:" + (System.currentTimeMillis() - startime) + " 毫秒"); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } System.out.println("===========================addIpassItem 新增裝置資訊業務結束================================"); } /** * 新增裝置使用資訊 * * @param dataArray * @throws InterruptedException */ private void addIpassUse(JSONArray dataArray) { System.out.println("===========================addIpassUse 新增裝置資訊業務開始================================"); try { for (int i = 0; i < dataArray.size(); i++) { List list = (List) dataArray.getJSONObject(i).get("equipmentUsageRec"); long startime = System.currentTimeMillis(); for (int j = 0; j < list.size(); j++) { JSONObject jobj = (JSONObject) list.get(j); String sbbh = StringUtil.validator(jobj.get("裝置編號")); String jsonStr = StringUtil.validator(jobj); Eqump eqump = Eqump.deploy(web3j, credentials, new StaticGasProvider(GasConstants.GAS_PRICE, GasConstants.GAS_LIMIT)).send(); if (eqump != null) { System.out.println("Eqump address is: " + eqump.getContractAddress()); eqump.addEqu(sbbh,jsonStr).send(); } } System.out.println("耗時:" + (System.currentTimeMillis() - startime) + " 毫秒"); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } System.out.println("===========================addIpassUse 新增裝置資訊業務結束================================"); }
日誌資訊
===========================addIpassItem 新增裝置資訊業務開始================================ 2020-07-23 17:20:57,261 [http-nio-8080-exec-1] INFO [org.fisco.bcos.web3j.utils.Async] Async.java:19 - default set setExeutor , pool size is 50 2020-07-23 17:20:57,262 [http-nio-8080-exec-1] INFO [org.fisco.bcos.web3j.utils.Async] Async.java:81 - set setExeutor because executor null, executor is java.util.concurrent.ThreadPoolExecutor@3ac27c97[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0] 2020-07-23 17:20:57,458 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.client.Service] Service.java:1388 - Receive block notify: {"blockNumber":353,"groupID":1} Eqump address is: 0x0066699656ac8bc09ec364858680f2357f899ae0 2020-07-23 17:21:01,012 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.client.Service] Service.java:1388 - Receive block notify: {"blockNumber":354,"groupID":1} Eqump address is: 0x0066699656ac8bc09ec364858680f2357f899ae0 2020-07-23 17:21:02,807 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.client.Service] Service.java:1388 - Receive block notify: {"blockNumber":355,"groupID":1} Eqump address is: 0x0066699656ac8bc09ec364858680f2357f899ae0 2020-07-23 17:21:04,341 [nioEventLoopGroup-2-1] INFO [org.fisco.bcos.channel.client.Service] Service.java:1388 - Receive block notify: {"blockNumber":356,"groupID":1} 耗時:6593 毫秒 ===========================addIpassItem 新增裝置資訊業務結束================================
webase驗證
{"序號":"3","儀器名稱":"600MHz超導核磁共振儀","儀器型號":"Avance III 600","裝置編號":"Avance III 600","所屬單位":"昆明植物研究所 ","所屬區域中心":"昆明生物多樣性大型儀器區域中心","製造商名稱":"瑞士布魯克公司 ","國別":"瑞士","購置時間":"20100109","放置地點":"分析測試中心101","預約稽核人":"李波 ","操作人員":"李波 ","儀器工作狀態":"正常 ","預約形式":"必須預約 ","預約型別":"專案預約","儀器大類":"室內分析測試裝置","儀器中類":"波譜儀器 ","儀器小類":"核磁共振波譜儀器"}
資料及參考
spring-boot-starter
Solidity官方文件
FISCO BCOS學習資料索引
線上remix
remix-ide的