FISCO-BCOS:Java SDK 部署和调用智能合约
由于FISCO-BCOS官方文档的应用开发手册已过于老旧,很多变量名、API随着版本更新发生改变,因此光凭手册难以高效地,迅速上手地使用FISCO-BCOS和Java SDK部署合约、开发应用。因此作者决定结合自身应用开发经验,对原手册的开发过程进行一次更新。
工作环境
LINUX:Linux桌面发行版,Ubuntu22.04
JAVA:JDK 11
IDE:IntelliJ IDEA 社区版
搭建一条FISCO BCOS链
参考https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/installation.html
搭建单群组4节点联盟链完成后,即使关闭命令行工具或者退出console,进程仍然处于启动状态(占据了对应端口),意味着四个节点仍然在正常工作中。
部署智能合约开发环境
准备智能合约
将需要用到的Solidity智能合约放入~/fisco/console/contracts/solidity
的目录中,本人以工作中需要用到的审计合约AuditHashContract.sol为例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.0;
contract AuditHashContract {
mapping(uint64 => mapping(uint64 => string)) private AuditHash;
function saveAuditHash(string memory hash, uint64 ctID, uint64 flowStartSec) public {
AuditHash[ctID][flowStartSec] = hash;
}
function getAuditHash(uint64 ctID, uint64 flowStartSec) view public returns (string memory){
return AuditHash[ctID][flowStartSec];
}
function verifyAuditHash(string memory hash, uint64 ctID, uint64 flowStartSec) view public returns (bool) {
return keccak256(abi.encodePacked(hash)) == keccak256(abi.encodePacked(AuditHash[ctID][flowStartSec]));
}
}
cd ~/fisco/console/
# 若控制台版本大于等于2.8.0,编译合约方法如下:(可通过bash sol2java.sh -h命令查看该脚本使用方法)
# 以下命令中参数“org.com.fisco”是指定产生的java类所属的包名,可自定义
bash sol2java.sh -p org.com.fisco
使用sol2java.sh将contracts/solidity下的所有合约编译产生bin,abi,java工具类。运行成功之后,将会在console/contracts/sdk
目录下生成java、abi和bin目录,
查看编译结果:
在Idea创建项目以及创建各种配置文件
创建一个Gradle工程
该项目我命名为FiscoAPP_teach
在build.gradle中加入以下依赖
def spring_version = "4.3.27.RELEASE"
List spring = [
"org.springframework:spring-core:$spring_version",
"org.springframework:spring-beans:$spring_version",
"org.springframework:spring-context:$spring_version",
"org.springframework:spring-tx:$spring_version",
]
dependencies {
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation ('org.fisco-bcos.java-sdk:fisco-bcos-java-sdk:2.9.1')
implementation spring
implementation ('org.slf4j:slf4j-log4j12:1.7.36')
runtimeOnly('org.slf4j:slf4j-log4j12:1.7.36')
}
将sol2java.sh生成的java目标文件放入工程中,目录结构(包名)与前面参数一致
将节点证书,以及生成的abi、bin目录放在resources目录下(似乎不需要?)
节点证书就是fisco/nodes/127.0.0.1/sdk
目录下的所有文件。证书和某FISCO BCOS链是一一对应的,生成一条链就有唯一的一个证书。在控制台(console)或者idea中,若想在该链上工作,就需要拷贝证书到conf文件夹里。当FISCO BCOS链重新生成时,证书也需要更新。
创建配置文件config-example.toml,放在resources目录下
配置文件如下:
[cryptoMaterial]
certPath = "conf" # The certification path
# The following configurations take the certPath by default if commented
# caCert = "conf/ca.crt" # CA cert file path
# If connect to the GM node, default CA cert path is ${certPath}/gm/gmca.crt
# sslCert = "conf/sdk.crt" # SSL cert file path
# If connect to the GM node, the default SDK cert path is ${certPath}/gm/gmsdk.crt
# sslKey = "conf/sdk.key" # SSL key file path
# If connect to the GM node, the default SDK privateKey path is ${certPath}/gm/gmsdk.key
# enSslCert = "conf/gm/gmensdk.crt" # GM encryption cert file path
# default load the GM SSL encryption cert from ${certPath}/gm/gmensdk.crt
# enSslKey = "conf/gm/gmensdk.key" # GM ssl cert file path
# default load the GM SSL encryption privateKey from ${certPath}/gm/gmensdk.key
[network]
peers=["127.0.0.1:20200", "127.0.0.1:20201", "127.0.0.1:20202", "127.0.0.1:20203"] # The peer list to connect
# AMOP configuration
# You can use following two methods to configure as a private topic message sender or subscriber.
# Usually, the public key and private key is generated by subscriber.
# Message sender receive public key from topic subscriber then make configuration.
# But, please do not config as both the message sender and the subscriber of one private topic, or you may send the message to yourself.
# Configure a private topic as a topic message sender.
# [[amop]]
# topicName = "PrivateTopic"
# publicKeys = [ "conf/amop/consumer_public_key_1.pem" ] # Public keys of the nodes that you want to send AMOP message of this topic to.
# Configure a private topic as a topic subscriber.
# [[amop]]
# topicName = "PrivateTopic"
# privateKey = "conf/amop/consumer_private_key.p12" # Your private key that used to subscriber verification.
# password = "123456"
[account]
keyStoreDir = "account" # The directory to load/store the account file, default is "account"
# accountFilePath = "" # The account file path (default load from the path specified by the keyStoreDir)
accountFileFormat = "pem" # The storage format of account file (Default is "pem", "p12" as an option)
# accountAddress = "" # The transactions sending account address
# Default is a randomly generated account
# The randomly generated account is stored in the path specified by the keyStoreDir
# password = "" # The password used to load the account file
[threadPool]
# channelProcessorThreadSize = "16" # The size of the thread pool to process channel callback
# Default is the number of cpu cores
# receiptProcessorThreadSize = "16" # The size of the thread pool to process transaction receipt notification
# Default is the number of cpu cores
maxBlockingQueueSize = "102400" # The max blocking queue size of the thread pool
内容基本不用修改,主要是network
下的节点peers
,我是运行在本机的,保留不变即可,端口同理。但是注意原样例文件,只给了两个端口:
"127.0.0.1:20200", "127.0.0.1:20201"
我建立的是四节点的群组,因此在后面又加上了另外两个节点的ip : port。
而若运行在虚拟机上,可能需要进行更改,自行调整即可。例如某虚拟机IP地址和端口可能是192.168.160.66:20000,192.168.160.66:20001。
查询节点可以查看~/nodes/127.0.0.1/node0/config.ini文件,一般是rpc的channel_listen_port(Channel端口,对应到Java SDK配置中的channel_listen_port)。
节点与网络之间的连接信息 SDK与节点间通过 ChannelServer 进行通信,SDK需要连接 ChannelServer 的监听端口,该端口可通过节点 config.ini 的 rpc.channel_listen_port 获取,具体请参考https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/manual/configuration.html。
创建一个log4j.properties文件,放在resources目录下
内容如下:
### set log levels ###
log4j.rootLogger=DEBUG, file
### output the log information to the file ###
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern='_'yyyyMMddHH'.log'
log4j.appender.file.File=./log/sdk.log
log4j.appender.file.Append=true
log4j.appender.file.filter.traceFilter=org.apache.log4j.varia.LevelRangeFilter
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p] [%-d{yyyy-MM-dd HH:mm:ss}] %C{1}.%M(%L) | %m%n
###output the log information to the console ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] [%-d{yyyy-MM-dd HH:mm:ss}] %C{1}.%M(%L) | %m%n
最终目录结构
在Idea中尝试调用合约以及查看块的信息(试验性质)
在Test中创建一个BcosSDKTest.java文件,其在包org.com.fisco下。(测试用代码,之后会删除)
代码如下,运行即可:
package org.com.fisco;
import org.fisco.bcos.sdk.BcosSDK;
import org.fisco.bcos.sdk.client.Client;
import org.fisco.bcos.sdk.client.protocol.request.Transaction;
import org.fisco.bcos.sdk.client.protocol.response.BcosBlock;
import org.fisco.bcos.sdk.client.protocol.response.BcosTransactionReceipt;
import org.fisco.bcos.sdk.client.protocol.response.BlockNumber;
import org.fisco.bcos.sdk.crypto.CryptoSuite;
import org.fisco.bcos.sdk.crypto.keypair.CryptoKeyPair;
import org.fisco.bcos.sdk.model.TransactionReceipt;
import org.fisco.bcos.sdk.transaction.codec.decode.TransactionDecoderInterface;
import org.fisco.bcos.sdk.transaction.codec.decode.TransactionDecoderService;
import org.fisco.bcos.sdk.transaction.manager.AssembleTransactionProcessor;
import org.fisco.bcos.sdk.transaction.manager.TransactionProcessorFactory;
import org.fisco.bcos.sdk.transaction.model.dto.CallResponse;
import org.fisco.bcos.sdk.transaction.model.dto.TransactionResponse;
import org.junit.jupiter.api.Test;
import java.io.FileNotFoundException;
import java.math.BigInteger;
import java.util.*;
public class BcosSDKTest {
// 获取配置文件路径
public final String configFile = BcosSDKTest.class.getClassLoader().getResource("config-example.toml").getPath();
@Test
public void testClient() throws Exception {
// 初始化BcosSDK
BcosSDK sdk = BcosSDK.build(configFile);
// 为群组1初始化client
Client client = sdk.getClient(Integer.valueOf(1));
// 向群组1部署AuditHashContract合约
CryptoKeyPair cryptoKeyPair = client.getCryptoSuite().getCryptoKeyPair();
AuditHashContract sample = AuditHashContract.deploy(client, cryptoKeyPair);
// 调用接口
TransactionReceipt receipt = sample.saveAuditHash("abcdefg", BigInteger.valueOf(1), BigInteger.valueOf(1));
// 获取群组1的块高
BlockNumber blockNumber = client.getBlockNumber();
BcosBlock block = client.getBlockByNumber(blockNumber.getBlockNumber(), false); //得到块的信息
Object o = block.getBlock().getTransactions().get(0).get(); //在块中得到交易哈希
BcosTransactionReceipt transactionReceipt = client.getTransactionReceipt((String) o); //通过交易哈希得到交易回执
// 获取当前群组对应的密码学接口
CryptoSuite cryptoSuite = client.getCryptoSuite();
// 构造TransactionDecoderService实例,传入是否密钥类型参数。
TransactionDecoderInterface decoder = new TransactionDecoderService(cryptoSuite);
//events = decoder.decodeEvents("main/abi/sm/HelloWorld.abi", transactionReceipt.getResult().getLogs());
String s = decoder.decodeReceiptMessage(transactionReceipt.getResult().getInput());
System.out.println(blockNumber.getBlockNumber());
System.out.println(s);
}
}
此时应该可以输出正确的审计信息,说明阶段性的配置已经完成(前面的操作没问题),可以通过java代码访问到群组下的节点了:
创建智能合约应用
开发业务逻辑
在路径/src/m
ain/java/org/com/fisco
目录下,创建AuditClient.java
类,通过调用AuditHa
shContract.java
实现对合约的部署与调用。
AuditClient.java
代码如下:
package org.com.fisco; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; import java.util.Objects; import java.util.Properties; import org.fisco.bcos.sdk.BcosSDK; import org.fisco.bcos.sdk.client.Client; import org.fisco.bcos.sdk.crypto.keypair.CryptoKeyPair; import org.fisco.bcos.sdk.model.TransactionReceipt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; public class AuditClient { static Logger logger = LoggerFactory.getLogger(AuditClient.class); private BcosSDK sdk; private Client client; private CryptoKeyPair cryptoKeyPair; public void initialize() throws Exception { String configFile = Objects.requireNonNull(BcosSDK.class.getClassLoader().getResource("config-example.toml")).getPath(); sdk = BcosSDK.build(configFile); // 初始化可向群组1发交易的Client client = sdk.getClient(1); cryptoKeyPair = client.getCryptoSuite().createKeyPair(); client.getCryptoSuite().setCryptoKeyPair(cryptoKeyPair); logger.debug("create client for group1, account address is " + cryptoKeyPair.getAddress()); } public void deployAssetAndRecordAddr() { try { AuditHashContract sample = AuditHashContract.deploy(client, cryptoKeyPair); System.out.println( " deploy Asset success, contract address is " + sample.getContractAddress()); recordAuditAddr(sample.getContractAddress()); } catch (Exception e) { // TODO Auto-generated catch block // e.printStackTrace(); System.out.println(" deploy Asset contract failed, error message is " + e.getMessage()); } } public void recordAuditAddr(String address) throws FileNotFoundException, IOException { Properties prop = new Properties(); prop.setProperty("address", address); final Resource contractResource = new ClassPathResource("contract.properties"); FileOutputStream fileOutputStream = new FileOutputStream(contractResource.getFile()); prop.store(fileOutputStream, "contract address"); } public String loadAuditAddr() throws Exception { // load Asset contact address from contract.properties Properties prop = new Properties(); final Resource contractResource = new ClassPathResource("contract.properties"); prop.load(contractResource.getInputStream()); String contractAddress = prop.getProperty("address"); if (contractAddress == null || contractAddress.trim().equals("")) { throw new Exception(" load Audit contract address failed, please deploy it first. "); } logger.info(" load Audit address from contract.properties, address is {}", contractAddress); return contractAddress; } public void the_saveAuditHash(String hash, BigInteger ctID, BigInteger flowStartSec) { try { String contractAddress = loadAuditAddr(); AuditHashContract save_event = AuditHashContract.load(contractAddress, client, cryptoKeyPair); TransactionReceipt receipt = save_event.saveAuditHash(hash, ctID, flowStartSec); } catch (Exception e) { logger.error("saveAuditHash exception, error message is {}", e.getMessage()); System.out.printf("saveAuditHash failed, error message is %s\n", e.getMessage()); } } public String the_getAuditHash(BigInteger ctID, BigInteger flowStartSec){ try{ String contractAddress = loadAuditAddr(); AuditHashContract get_event = AuditHashContract.load(contractAddress, client, cryptoKeyPair); return get_event.getAuditHash(ctID, flowStartSec); }catch (Exception e){ logger.error("getAuditHash exception, error message is {}", e.getMessage()); System.out.printf("getAuditHash failed, error message is %s\n", e.getMessage()); } return null; } public Boolean the_verifyAuditHash(String hash, BigInteger ctID, BigInteger flowStartSec){ try{ String contractAddress = loadAuditAddr(); AuditHashContract verify_event = AuditHashContract.load(contractAddress, client, cryptoKeyPair); return verify_event.verifyAuditHash(hash, ctID, flowStartSec); }catch(Exception e){ logger.error("VerifyAuditHash exception, error message is {}", e.getMessage()); System.out.printf("VerifyAuditHash failed, error message is %s\n", e.getMessage()); } return null; } public static void Usage() { System.out.println(" Usage:"); System.out.println( "\t java -cp conf/:lib/*:apps/* org.com.fisco.AuditClient deploy"); System.out.println( "\t java -cp conf/:lib/*:apps/* org.com.fisco.AuditClient save hash ctID flowStartSec"); System.out.println( "\t java -cp conf/:lib/*:apps/* org.com.fisco.AuditClient verify hash ctID flowStartSec"); System.out.println( "\t java -cp conf/:lib/*:apps/* org.com.fisco.AuditClient get ctID flowStartSec"); System.exit(0); } public static void main(String[] args) throws Exception { if (args.length < 1) { Usage(); } AuditClient client = new AuditClient(); client.initialize(); switch (args[0]) { case "deploy": client.deployAssetAndRecordAddr(); break; case "save": if (args.length < 4) { Usage(); } client.the_saveAuditHash(args[1], new BigInteger(args[2]), new BigInteger(args[3])); break; case "verify": if (args.length < 4) { Usage(); } if(client.the_verifyAuditHash(args[1], new BigInteger(args[2]), new BigInteger(args[3]))){ System.out.println("PASS"); } else{ System.out.println("FAIL"); } break; case "get": if (args.length < 3) { Usage(); } System.out.println(client.the_getAuditHash(new BigInteger(args[1]), new BigInteger(args[2]))); break; default: { Usage(); } } System.exit(0); } }
创建用户脚本
在fiscoAPP/tool目录下添加一个调用AuditClient的脚本run.sh。
脚本文件代码如下:
#!/bin/bash function usage() { echo " Usage : " echo " bash asset_run.sh deploy" echo " bash asset_run.sh save hash ctID flowStartSec" echo " bash asset_run.sh verify hash ctID flowStartSec " echo " bash asset_run.sh get ctID flowStartSec" exit 0 } case $1 in deploy) [ $# -lt 1 ] && { usage; } ;; save) [ $# -lt 4 ] && { usage; } ;; verify) [ $# -lt 4 ] && { usage; } ;; get) [ $# -lt 3 ] && { usage; } ;; *) usage ;; esac java -Djdk.tls.namedGroups="secp256k1" -cp 'apps/*:conf/:lib/*' org.com.fisco.AuditClient $@
这是一个Bash脚本,用于执行不同的命令并调用Java程序进行相应的操作。
脚本中定义了一个名为"usage"的函数,用于显示脚本的使用方法。根据传递给脚本的第一个参数,脚本会执行不同的操作。
以下是每个操作的解释:
- "deploy":执行部署操作。该操作不需要任何参数。
- "save":执行保存操作。该操作需要4个参数:hash、ctID、flowStartSec。
- "verify":执行验证操作。该操作需要4个参数:hash、ctID、flowStartSec。
- "get":执行获取操作。该操作需要3个参数:ctID、flowStartSec。
- 如果脚本传递了无效的参数或没有传递任何参数,则会调用"usage"函数显示脚本的使用方法。
最后,脚本会调用Java程序来执行相应的操作。它会将所有的命令行参数传递给Java程序。
配置gradle中的jar命令
接着,通过配置gradle中的Jar命令,指定复制和编译任务。
jar { destinationDirectory = file('dist/apps') archiveBaseName = project.name + '.jar'
//上面两行与原文档有改动,是gradle更新的原因 exclude '**/*.toml' exclude '**/*.properties' exclude '**/*.crt' exclude '**/*.key' doLast { copy { from configurations.runtimeClasspath
//上面一行与原手册有改动,是gradle更新的原因 into 'dist/lib' } copy { from file('src/test/resources/') into 'dist/conf' } copy { from file('tool/') into 'dist/' } copy { from file('src/test/resources/contract') into 'dist/contract' } } }
这是一个Gradle构建脚本中的jar块,用于配置项目的打包和发布。
1. destinationDirectory:设置打包生成的JAR文件的目标目录为"dist/apps"。
2. archiveBaseName:设置打包生成的JAR文件的基本名称为项目名称加上".jar"后缀。
3. exclude:指定需要在打包过程中排除的文件或目录。这里使用通配符来排除了一些文件,包括所有的.toml、.properties、.crt和.key文件。
4. doLast:这是一个闭包,在打包过程的最后执行。其中包含了一系列的复制操作,将需要的文件从源路径复制到目标路径。
- 第一个复制操作从配置项configurations.runtimeClasspath中获取运行时依赖,并将它们复制到目标路径'dist/lib'中。
- 第二个复制操作从文件路径'src/test/resources/'中获取资源文件,并将它们复制到目标路径'dist/conf'中。
- 第三个复制操作从文件路径'tool/'中获取文件,并将它们复制到目标路径'dist/'中。
- 第四个复制操作从文件路径'src/test/resources/contract'中获取合约文件,并将它们复制到目标路径'dist/contract'中。
这段代码的作用是将项目的源代码、依赖、配置文件和合约文件打包到指定的目录中,以便后续的发布或部署。
/src/test/resources
目录下,创建一个空的contract.properties
文件,用于应用在运行时存放合约地址。运行应用
总结: 至此,我们通过合约开发,合约编译,SDK配置与业务开发构建了一个基于FISCO BCOS联盟区块链的应用。