fabric-sdk-java爬坑历程

本文章使用的环境为1.4.0版本(都是一点一点磨出来的,有用请点个赞吧,呜呜呜)

主要实现的功能:

  1. 客户端初始化
  2. 通道初始化
  3. 链码安装
  4. 链码初始化
  5. 调用链码
  6. 通道创建
  7. 加入通道

1. 客户端初始化

HFClient client = HFClient.createNewInstance();
client.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
client.setUserContext(new SimpleUser());

2. 通道初始化

orderer1Prop中的pemFile对应的是orderer中的tls证书
peer1Prop中的pemFile对应的是peer节点中的tls证书
peer1Prop中的clientKeyFile对应的是peer节点中的tls文件下的keystore
peer1Prop中的clientCertFile对应的是peer节点中的tls文件下的signcerts

    // initialize Channel
    private Channel initializeChannel(HFClient client, String channelName) throws InvalidArgumentException, TransactionException {
        Properties orderer1Prop = new Properties();
        orderer1Prop.setProperty("pemFile", Config.ORDERER1_TLS_CACERT_FILE);
        orderer1Prop.setProperty("sslProvider", "openSSL");
        orderer1Prop.setProperty("negotiationType", "TLS");
        orderer1Prop.setProperty("hostnameOverride", "chainorder1.com");
        orderer1Prop.setProperty("trustServerCertificate", "true");
        orderer1Prop.put("grpc.NettyChannelBuilderOption.maxInboundMessageSize", 9000000);
        Orderer orderer = client.newOrderer("chainorder1.com", Config.ORDERER1_ADDRESS, orderer1Prop);

        Properties peer1Prop = new Properties();
        peer1Prop.setProperty("pemFile", Config.PEER1_TLS_CACERT_FILE);
        peer1Prop.setProperty("sslProvider", "openSSL");
        peer1Prop.setProperty("negotiationType", "TLS");
        peer1Prop.setProperty("hostnameOverride", "peer1.operation.com");
        peer1Prop.setProperty("trustServerCertificate", "true");
        peer1Prop.setProperty("clientKeyFile", Config.PEER1_TLS_CLIENTKEY);
        peer1Prop.setProperty("clientCertFile", Config.PEER1_TLS_CLIENTCERT);
        peer1Prop.put("grpc.NettyChannelBuilderOption.maxInboundMessageSize", 9000000);
        Peer peer = client.newPeer("peer1.operation.com", Config.PEER1_ADDRESS, peer1Prop);

        EventHub eventHub = client.newEventHub("peer1.operation.com",Config.EVENTHUB1_ADDRESS,peer1Prop);

        Channel channel = client.newChannel(channelName);
        channel.addOrderer(orderer);
        channel.addPeer(peer);
        channel.addEventHub(eventHub);
        channel.initialize();
        System.out.println("channel初始化");
        return channel;
    }

3. 链码安装

(链码默认会安装到peer节点的/var/hyperledger/production/chaincodes目录下,测试链码安装的时候可以去这个目录下查找,一定要注意是peer节点的目录下,我好多次都跑到cli里边找半天找不到。。。)
链码安装分为go和java两种方式,每种语言的链码安装也有两种方式

3.1
installProposalRequest.setChaincodeSourceLocation(sourceLocation);
go链码:
需要额外指定 installProposalRequest.setChaincodePath(chaincodePath);
使用这种方式要确保 sourceLocation子目录下为chaincodePath,chaincodePath子目录包含链码,即整体的目录为 sourceLocation/chaincodePath/链码.go
java链码:
sourceLocation要指定到项目的src目录下,否则初始化会失败。也就是说sourceLocation的路径为:项目路径/src

3.2
installProposalRequest.setChaincodeInputStream(inputStream);这个方法需要传的参数是经过tar压缩之后的流(使用官方的Util包下的generateTarGzInputStream工具进行压缩)

java链码、nodejs链码:Util.generateTarGzInputStream(Paths.get(sourcePath, projectName).toFile(), "src")
sourcePath/projectName为项目全路径,projectName为项目名称,即压缩的时候指定的两个路径为项目全路径和代码的"src"路径

go链码:Util.generateTarGzInputStream(Paths.get(sourcePath,"src", ccPath).toFile(), Paths.get("src",ccPath).toString())
sourcePath/src为gopath,ccPath为chaincode相对于gopath的路径

另外go链码需要额外指定 installProposalRequest.setChaincodePath(chaincodePath);

    public static Collection<ProposalResponse> InstallChainCode(HFClient hfClient, Channel channel, String chainCodePath, String projectName, String chainCodeName, TransactionRequest.Type language) throws InvalidArgumentException, ProposalException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, CryptoException, ClassNotFoundException, IOException {

        checkClientInit();

        checkChannelInit();

        InstallProposalRequest installProposalRequest = hfClient.newInstallProposalRequest();
        ChaincodeID chaincodeID = ChaincodeID.newBuilder().setName(chainCodeName).setVersion("1.0").build();
        installProposalRequest.setChaincodeID(chaincodeID);
        installProposalRequest.setChaincodeVersion("1.0");
        installProposalRequest.setChaincodeLanguage(language);
        if (language.equals(TransactionRequest.Type.GO_LANG)) {
//            installProposalRequest.setChaincodeSourceLocation(new File(chainCodePath));
            installProposalRequest.setChaincodeInputStream(Util.generateTarGzInputStream(Paths.get(chainCodePath, projectName).toFile(), ""));

            installProposalRequest.setChaincodePath(chainCodeName);
        } else {
            installProposalRequest.setChaincodeSourceLocation(new File(chainCodePath+"\\"+ projectName));
//            System.out.println("Paths>>>>>>>>>>>>>>>"+Paths.get(chainCodePath,projectName).toFile());
//            installProposalRequest.setChaincodeInputStream(Util.generateTarGzInputStream(Paths.get(chainCodePath,projectName).toFile(),"src"));
        }
        Collection<ProposalResponse> proposalResponses = hfClient.sendInstallProposal(installProposalRequest, channel.getPeers());
        return proposalResponses;
    }

4. 链码初始化

私有数据的链码需要额外调用instantiateProposalRequest.setChaincodeCollectionConfiguration方法

    public static Collection<ProposalResponse> InstantiateChainCode(HFClient hfClient, Channel channel, TransactionRequest.Type language, String chainCodeName, String[] args, String endorsementPolicy) throws ProposalException, InvalidArgumentException, InterruptedException, ExecutionException, TimeoutException, IOException, ChaincodeEndorsementPolicyParseException {

        checkClientInit();

        checkChannelInit();

        InstantiateProposalRequest instantiateProposalRequest = hfClient.newInstantiationProposalRequest();
        instantiateProposalRequest.setProposalWaitTime(120000);
        ChaincodeID chaincodeID = ChaincodeID.newBuilder().setName(chainCodeName).setVersion("1.0").setPath(chainCodeName).build();
        instantiateProposalRequest.setChaincodeID(chaincodeID);
        instantiateProposalRequest.setChaincodeLanguage(language);
        instantiateProposalRequest.setFcn("init");
        instantiateProposalRequest.setArgs(args);

        //region 如果需要用到私有数据,需要添加私有数据的配置
        //instantiateProposalRequest.setChaincodeCollectionConfiguration(ChaincodeCollectionConfiguration.fromYamlFile(new File("src/test/fixture/collectionProperties/PrivateDataIT.yaml")));
        //endregion

        Map<String, byte[]> tm = new HashMap<>();
        tm.put("HyperLedgerFabric", "InstantiateProposalRequest:JavaSDK".getBytes(UTF_8));
        tm.put("method", "InstantiateProposalRequest".getBytes(UTF_8));
        instantiateProposalRequest.setTransientMap(tm);
        if (!endorsementPolicy.equals("")){
            ChaincodeEndorsementPolicy chaincodeEndorsementPolicy = new ChaincodeEndorsementPolicy();
            chaincodeEndorsementPolicy.fromYamlFile(new File(endorsementPolicy));
            instantiateProposalRequest.setChaincodeEndorsementPolicy(chaincodeEndorsementPolicy);
        }
        Collection<ProposalResponse> responses = channel.sendInstantiationProposal(instantiateProposalRequest,channel.getPeers());

        List<ProposalResponse> list = responses.stream().filter(ProposalResponse::isVerified).collect(Collectors.toList());
        if (list.size() == 0) {
            return responses;
        }
        BlockEvent.TransactionEvent event = channel.sendTransaction(responses).get(60, TimeUnit.SECONDS);

        if (event.isValid()){
            logger.info("InstantiateChainCode success");
        }

        return responses;
    }

5. 链码调用

调用完channel.sendTransactionProposal()方法之后实际上只是完成了模拟交易的环节,接下来需要调用channel.sendTransaction()方法将成功的提案发送给Orderer节点进行真正的交易,并对该方法的返回回调进行事件监听获取数据落链情况,此时交易才完成。(私有数据链码需要额外调用transactionProposalRequest.setTransientMap方法)

  public Collection<ProposalResponse> invoke(HFClient client, Channel channel, String chaincodeName, String func, String args) throws InvalidArgumentException, ProposalException {
        TransactionProposalRequest request = client.newTransactionProposalRequest();
        String cc = chaincodeName;
        ChaincodeID ccid = ChaincodeID.newBuilder().setName(cc).build();
        request.setChaincodeID(ccid);
        request.setFcn(func);
        request.setArgs(args);
        request.setProposalWaitTime(3000);
        Collection<ProposalResponse> responses = channel.sendTransactionProposal(request);

        //region  如果使用私有数据,需要通过map传递临时数据
//        Map<String, byte[]> tm = new HashMap<>();
//        tm.put("A", "a".getBytes(UTF_8));
//        tm.put("AVal", "500".getBytes(UTF_8));
//        tm.put("B", "b".getBytes(UTF_8));
//        String arg3 = "" + (200 + 1);
//        tm.put("BVal", arg3.getBytes(UTF_8));
//        transactionProposalRequest.setTransientMap(tm);
        //endregion

        Collection<ProposalResponse> respsponse = channel.sendTransactionProposal(transactionProposalRequest);

        List<ProposalResponse> list = respsponse.stream().filter(ProposalResponse::isVerified).collect(Collectors.toList());
        if (list.size() == 0) {
            return respsponse;
        }
        BlockEvent.TransactionEvent event = channel.sendTransaction(respsponse).get(60, TimeUnit.SECONDS);

        if (event.isValid()){
            logger.info("Fabric 链码交互事件成功");
        }

        logger.info("Fabric 链码交互完成");

        return respsponse;
    }

6. 通道创建

在官方SDK里边只有创建完通道之后马上加入通道,而之后无法让其他节点通过SDK加入(可能是自己菜,没看到)。通过这种方式可以实现以后任意节点想加入通道就加入,姑且叫它channel. block(跟命令行生成的. block格式是不一样的,不能在命令行中使用这个文件让节点加入通道,如果需要通过cli的方式添加需要通过fetch命令获取通道配置创世区块:peer channel fetch oldest [outputfile] [flags])

public static Channel createChannel(HFClient hfClient,String channelName, byte[] txByte,Orderer orderer) throws InvalidArgumentException, TransactionException, IOException {
        logger.info("Fabric 创建通道开始");

        ChannelConfiguration configuration = new ChannelConfiguration(txByte);
        byte[] signData = hfClient.getChannelConfigurationSignature(configuration,hfClient.getUserContext());
        Channel channel = hfClient.newChannel(channelName,orderer,configuration,signData);
        //保存channel信息供以后加入通道使用。
        channel.serializeChannel(new File("/ channel.block"));
        channel.initialize();

        logger.info("Fabric 创建通道完成");
        return channel;
    }

7. 加入通道

通过之前序列化保存的channel对象文件channel. block创建channel从而使其他节点加入通道

public static Channel joinChannel(HFClient hfClient, byte[] channelByte, Peer peer, Channel.PeerOptions peerOptions) throws InvalidArgumentException, IOException, ClassNotFoundException, TransactionException, ProposalException {
        logger.info("Fabric 加入通道开始");
        //反序列化之前保存的通道信息文件
        Channel channel = hfClient.deSerializeChannel(channelByte);
        channel.initialize();
        channel.joinPeer(peer,peerOptions);

        logger.info("Fabric 加入通道完成");

        return channel;
    }

坑点:

1. TLS验证不通过

大概率是你的证书指定不对,这时候可以使用wireshark抓包看看SSL握手过程。在tls握手的时候使用的证书包括
· order的tls下的ca证书
· peer的tls的ca证书、server.key、server.crt证书
· admin的msp中的keystore和signcerts证书
如果实在绕不过去tls证书校验,可以先把SDK源码中的EndPoint类的262-266行改成下面的,等别的调通了再搞这个,万一哪天运气来了不是。

//                        try (InputStream myInputStream = new ByteArrayInputStream(pemBytes)) {
//                            sslContext = clientContextBuilder
//                                    .trustManager(myInputStream)
//                                    .build();
//                        }
//                        //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                        sslContext = clientContextBuilder
                                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                .build();
//                        //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2. 初始化go链码的时候出现的问题

· 2.1可能会报找不到类之类的错误,官网的解决办法是使用vendor来加载依赖包。


https://github.com/hyperledger/fabric-sdk-java/tree/v1.4.4#go-lang-chaincode

3. 链码实例化可能会炒鸡慢哦

4. 需要发送给orderer的都需要调用channel.sendTranscation方法将模拟交易的结果提案到排序节点

5. grpc通讯4M限制

因为SDK使用的grpc进行通讯,所以会有通讯报文大小的限制,默认是4M,那么怎么突破这种限制呢,官方是这么缩滴


https://github.com/hyperledger/fabric-sdk-java#grpc-message-frame-size-exceeds-maximum

然后我亲自去翻开End2endIT类


image.png

所以缩,只要在通道初始化的时候给peer和order添加这一行配置,我们就解决了这个4M限制。偶吼吼。

6. Android设备如何使用fabric-sdk-java

本人闲来无事在GitHub上闲逛,就跟女孩子内种只逛不买的一样。一不小心就看到了一个项目 android-fabric-sdk
人家是把grpc-netty换成了grpc-okhttp来支持Android设备,看起来很吊的样子

官方有个项目iroha是专供移动端的,安卓的:
https://github.com/hyperledger/iroha-android

7.nodejs链码实例化失败

Error: could not assemble transaction, err proposal response was not successful, error code 500, msg error starting container: error starting container: Failed to generate platform-specific docker build: Error returned from build: 1 "npm ERR! code EAI_AGAIN
npm ERR! errno EAI_AGAIN
npm ERR! request to https://registry.npmjs.org/fabric-shim failed, reason: getaddrinfo EAI_AGAIN registry.npmjs.org:443

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2019-09-16T06_57_58_381Z-debug.log
"

出现这个问题是因为docker内部DNS无法解析。解决方案:

$ sudo apt-get install bridge-utils -y
$ sudo service docker stop
$ sudo ip link set dev docker0 down
$ sudo brctl delbr docker0
$ sudo service docker start
$ docker network inspect bridge

之后我的node链码可以进行实例化了,不过依然很慢。或许可以修改源加快一点构建容器的速度。尝试从fabric源码里边查找关于创建Dockerfile的方法自己重新构建一个fabric-ccenv,发现挺复杂。。。后来 在这里卡了好几天。
最后尝试重新构建一个淘宝npm源的hyperledger/fabric-ccenv,速度快了很多。

FROM hyperledger/fabric-ccenv:1.4.1

ENV NPM_CONFIG_BUILD_FROM_SOURCE=true

ENV NPM_CONFIG_REGISTRY=https://registry.npm.taobao.org

然后执行docker build -t hyperledger/fabric-ccenv:latest .
也可以执行以下命令拉取我已经build好的镜像

docker push soullistener/fabric-ccenv:tagname

然后修改tag为hyperledger/fabric-ccenv:latest

8.指纹不匹配

response: status:500 message:"failed to execute transaction 51e5aae96105281fb96e26700ed9ed4fcb25c42fd1c702bbc5b3f2ffd0a58338: [channel mychannel] failed to get chaincode container info for chaincode:1.0.0: could not get chaincode code: chaincode fingerprint mismatch: data mismatch"

指纹不同是因为 链码名称、版本、读写权限,owner/group,时间戳、不同的fabric版本,不同的go语言版本(tar库版本)、不同的安装方式(SDK,peer命令行安装)等信息,任何不一致都会导致这个问题。
使用程序打包安装的时候要把generateTarGzInputStream生成的byte数组保存下来供以后其他的节点安装使用,否则会因为打包时间不同等问题安装之后的链码hash不一样导致链码实例化失败。

详情:https://www.jianshu.com/p/dca0546d85f6

9.java链码实例化慢或超时

当出现这种情况很大的可能是因为在把java链码打包为jar的时候会下载依赖的三方库,而下载三方库是个很漫长的过程,大概率会因为下载到一半就超时了。因此需要修改为国内的maven库加速下载。

首先需要修改settings文件,添加自己的仓库或者阿里云等库

  <mirror>
    <id>maven-public</id>
    <mirrorOf>*</mirrorOf>
    <url>http://192.168.7.2:8081/repository/maven-public/</url>
  </mirror>

其次重新构建javaenv(替换settings,从其他仓库下载三方库)

FROM hyperledger/fabric-javaenv:amd64-1.4.4
ADD ./settings.xml   /root/.sdkman/candidates/maven/current/conf
WORKDIR /root/chaincode-java

然后build后替换掉之前的javaenv,之后下载三方库会快很多。


可供参考的地方:

  1. 官方SDK地址:https://github.com/hyperledger/fabric-sdk-java/tree/v1.4.0(一定要多看test类,你要用的东西都能在这里找到)
  2. API :http://central.maven.org/maven2/org/hyperledger/fabric-sdk-java/fabric-sdk-java/
    (在整个SDK中最常用的类就是 HFClient、Channel、EventHub,还有请求对应的request类:InstallProposalRequest, InstantiateProposalRequest, QueryByChaincodeRequest, QuerySCCRequest, TransactionProposalRequest, UpgradeProposalRequest)
  3. IBM教程:https://developer.ibm.com/tutorials/hyperledger-fabric-java-sdk-for-tls-enabled-fabric-network/
  4. hyperledger中文文档 https://hyperledgercn.github.io/hyperledgerDocs/sdk_java_zh/
  5. IBM的官方例子 https://github.com/IBM/blockchain-application-using-fabric-java-sdk
  6. GitHub上好多可以借鉴的项目,搜索关键字 fabric-java就出来好多
  7. 可以加入fabric国际大家庭聊天群来问问题 https://chat.hyperledger.org/channel/fabric-sdk-java