阿里云区块链BaaS平台调用链码

调用链码

部署后的链码作为Docker镜像或者是本地进程运行在Peer节点所在服务器。区块链应用客户端不能直接访问链码,所有链码操作通过请求Peer节点的背书服务实现。对于交易类链码操作,Peer节点只是模拟执行,并不会记录到账本,区块链应用客户端需要通过Orderer节点的广播服务提交交易。超级账本中交易提交过程上是异步的,Orderer节点的广播服务只是接受交易提交请求,只有在Orderer节点生成区块,传播到Peer节点,Peer节点记录区块后,才真正完成交易的处理。Peer节点提供事件服务,区块链应用客户端可以通过注册事件服务,从区块链事件中获取交易处理结果。

阿里云区块链平台选购地址 https://www.aliyun.com/product/baas
阿里云区块链平台官方帮助文档 https://help.aliyun.com/product/84950.html

超级账本是有许可的区块链,区块链应用客户端需要有身份证书才可以访问超级账本。超级账本可配置开启TLS通信协议,若开启TLS通信协议,区块链应用客户端需要进行TLS相关设置。

BaaS平台在启动超级账本网络时,自动生成身份许可相关的证书,并提供用户证书下载功能。此外,为了降低使用的难度,BaaS平台默认超级账本部署中未开启TLS。

获取连接参数

访问超级账本网络中的链码,需要建立与超级账本网络中相关服务的连接,连接参数包括Channel名称、Orderer服务地址、Peer背书服务地址、Peer事件服务地址等,参数列表及格式如表-超级账本连接参数表。

可以根据BaaS平台提供的信息属性获取超级账本网络连接参数,属性来源参见表-超级账本连接参数来源表,具体示例参见图-超级账本网络列表示例图、图-超级账本共识信息示例图、图-超级账本通道信息示例图、图-下载MSP证书示例图和图-MSP证书下载内容示例图。

参数名 参数格式 参数示例
Channel名   mychannel
Ordererer服务地址 gprc://%网络域名%/%Ordererer服务端口% grpc://k8s.3.cn/32123
Peer背书服务地址 gprc://%网络域名%/%Peer背书服务端口% grpc://k8s.3.cn/31093
Peer事件服务地址 gprc://%网络域名%/%Peer事件服务端口% grpc://k8s.3.cn/32511
MSP标识   jdMSP
用户名   User1
用户私钥文件   msp/keystore/key.pem
用户证书文件   msp/signcerts/User1@org0.peer.baas.jd.com-cert.pem

表-超级账本连接参数表


  属性名称
  

  属性来源
  

  属性示例
  

  超级账本界面
  

  网络域名
  

  网络列表中“网络域名”列
  

  k8s.3.cn
  

  网络详情界面.共识管理页

  Ordererer服务端口
  

  Ordererer列表中“Ports”列
  

  32123
  

  网络详情界面.通道管理页
  

  Channel名
  

  页面左上角下拉框
  

  mychannel
  

  MSP标识
  

  组织列表视图中“MSP标识”列
  

  jdMSP
  

  Peer背书服务端口
  

  组织列表视图中“Ports”列中第一个端口
  

  31093
  

  Peer事件服务端口
  

  组织列表视图中“Ports”列中第二个端口
  

  32511
  

  下载MSP证书界面
  

  用户名
  

  下载框中选中的用户名
  

  User1
  

  MSP证书下载内容
  

  用户私钥文件
  

  keystore目录下key.pem文件
  

  msp/keystore/key.pem
  

  用户证书文件
  

  signcerts目录下的pem文件
  

  msp/signcerts/User1@org0.peer.baas.jd.com-cert.pem

开发应用程序

超级账本提供Java、Node.js和Java版SDK,建议采用Java或Node.js开发超级账本区块链应用客户端。

Java版开发环境

使用Java版FabricSDK开发应用客户端,需要准备如下开发环境:

  • JDK,建议使用JDK1.8以上版本;
  • IDE,可使用熟悉的IDE,建议使用Eclipse;
  • FabricJavaSDK,建议使用与Fabric版本保持一致;
  • API说明

JavaFabricSDK中链码调用相关的主要API类关系下图所示。

 

HFClient提供客户端环境,通过该类阿里云初始化超级账本交互的对象。需要注意的是,HFClient中参数初始化有依赖关系,具体细节可参见示例代码。

CryptoSuite定义了PKI接口,PKI相关秘钥生成、签名和验证方法都在此接口中定义,所有PKI实现都要继承该接口。SDK中在CryptoPrimitives提供了基础实现,可通过CryptoSuite.Factory工厂类创建该实现阿里云。

链码调用功能由Chanel类提供,包括链码查询(queryByChaincode)、发送交易背书(sendTransactionProposal)和发送交易(sendTransaction)。Peer、Ordererer和Event服务分别用于定义Peer节点、Ordererer节点和事件通知服务地址,Channel通过这些定义的地址调用对应服务。

User和Enrollment接口用于提供操作账本时的身份认证信息,User定义了用户基本信息,Enrollment提供证书和对应签名私钥,SDK中未提供User实现,仅提供了FabricCA对应的Enrollment实现,应用中需要提供自己的实现。

示例代码

超级账本是有许可的区块链,只有授权用户才能操作,因此访问超级账本时,需要提供用户身份信息。在FabricJavaSDK中,用户身份认证信息通过User和Enrollment接口定义,应用中通过阿里云化接口对象提供身份认证信息。

在基于BaaS平台部署管理的链码开发中,通过用户证书下载功能可以获取阿里云化身份认证信息所需参数,主要包括用户名、MSP标识、签名私钥和CA证书。Enrollment阿里云化参见示例代码-Enrollment阿里云化,User阿里云化参见示例代码-User阿里云化。

通过FabricJavaSDK调用链码前,需要指定Channel、Peer背书服务地址、Ordererer广播服务地址、PeerEvent服务地址。除此之外,还需要初始化链码调用请求和响应数据的签名和验签等依赖的PKI组件,以及用户的身份标识信息。调用链码前的环境初始化代码参见示例代码-初始化Channel,初始化过程中需要注意各个模块的依赖关系,详情见示例中的NOTE。

超级账本中链码操作可分为两类,一类是执行交易,一类是查询状态。交易类操作,发送请求到一个或多个Peer背书,将Peer背书结果广播到Ordererer服务,通过PeerEvent服务监听执行结果,实现方式可参见示例代码-执行交易。查询状态实现比较简单,调用Peer背书服务即可,实现方式可参见示例代码-查询链码数据状态。
 

/**
 * load enrollment from local key and certificate files.
 * 
 * @param keyFile file path of private key
 * @param certFile file path of CA certificate
 * @return  enrollment loaded from local key and certificate files
 * @throws Exception  any exception occurred during  enrollment loading
 */
private static Enrollment loadEnrollment(String keyFile, String certFile) throws Exception {
    PemReader reader = null;
    PemObject pemObj = null;

    // read private key PEM file
    try {
        reader = new PemReader(new java.io.FileReader(keyFile));
        pemObj = reader.readPemObject();
    } finally {
        if(reader != null) {
            try {
                reader.close();
            } catch(Exception e) {}
        }
    }

    // generate private key from PKCS8 encoded data
    EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pemObj.getContent());;
    KeyFactory generator = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
    final PrivateKey privateKey = generator.generatePrivate(privateKeySpec);


    // read CA certificate content
    final String cert =  IOUtils.toString(new File(certFile).toURI());

    return new Enrollment() {
        public PrivateKey getKey() {
            return privateKey;
        }

        public String getCert() {
            return cert;
        }
    };

}

示例代码-Enrollment实例化

 
/**
 * create user with specified MSP, user name and enrollment.
 * 
 * @param user  ID, i.e., unique name of user
 * @param mspId ID of MSP which user belongs to
 * @param enrollment user's enrollment
 * @return  user instance with attributes provided by parameters, and others as default.
 */
private static User createUser(String user, String mspId,  final Enrollment enrollment) {
    return new User() {
        public String getName() {
            return user;
        }

        public Set<String> getRoles() {
            return null;
        }

        public String getAccount() {
            return null;
        }

        public String getAffiliation() {
            return null;
        }


        public Enrollment getEnrollment() {
            return enrollment;
        }

        public String getMspId() {
            return mspId;
        }
    };
}

示例代码-User实例化

 
HFClient hfClient = HFClient.createNewInstance();

// NOTE: CryptoSuite must be set first.
hfClient.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());

// NOTE: User must be set before Ordererer, Peer and EventHub.
hfClient.setUserContext(createUser("User1", "houseMSP",
        loadEnrollment("data/msp/keystore/key.pem", "data/msp/signcerts/User1@house.peer.k8s.3.cn-cert.pem")));

Channel channel = hfClient.newChannel("mychannel");

// NOTE: Ordererer is used to send transaction.
Properties OrderererProp = new Properties();
OrderererProp.setProperty("OrderererWaitTimeMilliSecs", "30000");
channel.addOrdererer(hfClient.newOrdererer("Ordererer", "grpc://k8s.3.cn:30094", OrderererProp));

// NOTE: Peer is used to interact with ChainCode, including transaction and query. 
channel.addPeer(hfClient.newPeer("peer0org0", "grpc://k8s.3.cn:31636"));

// NOTE: EventHub is used to listen block events.
// NOTE: EventHub must be added if transactions state is needed.
channel.addEventHub(hfClient.newEventHub("peer0org0", "grpc://k8s.3.cn:30512"));

// NOTE: channel must be initialized before any ledger operation.
channel.initialize();

初始化Channel客户端

 
TransactionProposalRequest txReq = hfClient.newTransactionProposalRequest();
ChaincodeID cid = ChaincodeID.newBuilder().setName("cdemo").build();
        txReq.setChaincodeID(cid);
        txReq.setFcn("invoke");
        txReq.setArgs(new String[ ] {"12"});
Collection<ProposalResponse> txRsps = channel.sendTransactionProposal(txReq);
CompletableFuture<TransactionEvent> eFuture = channel.sendTransaction(txRsps);
// NOTE: if no EventHub, this function will be blocked. 
TransactionEvent txEvent = eFuture.get();

示例代码-执行交易

 
QueryByChaincodeRequest qryReq = hfClient.newQueryProposalRequest();
        qryReq.setChaincodeID(cid);
        qryReq.setFcn("query");
        qryReq.setArgs(new String[ ] {});
        Collection<ProposalResponse> 
Collection<ProposalResponse> qryRsps =channel.queryByChaincode(qryReq);

示例代码-查询链码数据状态

Nodejs版开发环境

  1. Node, Node需要6.9.x 或者更高版本, 8.4.0 或者更高版本,不支持Node v7+;  NPM版本支持3.10.x 或者更高版本;
  2. IDE, 推荐使用vscode,其他如Sublime Text甚至记事本都可以;
  3. Fabric Nodejs SDK,建议使用与Fabric版本保持一致;

API说明

SDK由三个模块组成:

  1. api: 为应用程序开发人员提供可插入的api,以提供SDK使用的关键接口的替代实现。对于每个接口都有内置的默认实现;
  2. fabric-client: 这个模块提供api来与基于Hypreledger构建的区块链网络的核心组件交互,即对等体、定序器和事件流;
  3. fabric-ca-client: 该模块提供api与可选组件fabric-ca交互,该组件包含用于成员管理的服务;

示例代码

Query:

 
var Fabric_Client = require('fabric-client');
var path = require('path');
var util = require('util');
var os = require('os');
var console = require("console")
var fabric_client = new Fabric_Client();
// setup the fabric network
var channel = fabric_client.newChannel('mychannel');
var peer = fabric_client.newPeer('grpc://192.168.*.*:32683');
channel.addPeer(peer);
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.error('Store path:'+store_path);
var tx_id = null;
// create the key value store as defined in the fabric-client/config/default.json 'key-value-store' setting
Fabric_Client.newDefaultKeyValueStore({ path: store_path
}).then((state_store) => {
    // assign the store to the fabric client
    fabric_client.setStateStore(state_store);
    var crypto_suite = Fabric_Client.newCryptoSuite();
    var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path});
    crypto_suite.setCryptoKeyStore(crypto_store);
    fabric_client.setCryptoSuite(crypto_suite);
    // get the enrolled user from persistence, this user will sign all requests
    var userOpt = {
            username: 'User1',
            mspid: 'org0MSP',
            //  这里是从网页上下载的证书跟私钥
            cryptoContent: {
                privateKey: './key.pem',
                signedCert: './cert.pem'
            }
        }
        return fabric_client.createUser(userOpt)
}).then((user_from_store) => {
    if (user_from_store && user_from_store.isEnrolled()) {
        console.log('Successfully loaded user1 from persistence');
        member_user = user_from_store;
    } else {
        throw new Error('Failed to get user1.... run registerUser.js');
    }
    fabric_client.setUserContext(user_from_store, true)
    const request = {
        chaincodeId: 'cdemo',
        fcn: 'query',
        args: ['']
    };
    console.log("pass")
    // send the query proposal to the peer
    return channel.queryByChaincode(request);
}).then((query_responses) => {
    console.log("Query has completed, checking results");
    // query_responses could have more than one  results if there multiple peers were used as targets
    if (query_responses && query_responses.length == 1) {
        if (query_responses[0] instanceof Error) {
            console.error("error from query = ", query_responses[0]);
        } else {
            console.log("Response is ", query_responses[0].toString());
        }
    } else {
        console.log("No payloads were returned from query");
    }
}).catch((err) => {
    console.error('Failed to query successfully :: ' + err);
});

Invoke:

 
'use strict';
var hfc = require('fabric-client');
var path = require('path');
var util = require('util');
var sdkUtils = require('fabric-client/lib/utils')
const fs = require('fs');
var options = {
    user_id: 'User1',
    msp_id:'Org0MSP',
    channel_id: 'mychannel',
    chaincode_id: 'cdemo',
    peer_url: 'grpc://192.168.*.*:32683',
    event_url: 'grpc://192.168.*.*:32134',
    Ordererer_url: 'grpc://192.168.*.*:31048',
    privateKeyFolder:'./keystore',
    signedCert:'./cert.pem',
};
var channel = {};
var client = null;
var targets = [ ];
var tx_id = null;
Promise.resolve().then(() => {
    console.log("Load privateKey and signedCert");
    client = new hfc();
    var createUserOpt = {
        username: 'Admin',
        mspid: 'org0MSP',
        cryptoContent: {
            privateKey: './key.pem',
            signedCert: './cert.pem'
        }
    }
//以上代码指定了当前用户的私钥,证书等基本信息
return sdkUtils.newKeyValueStore({
                        path: "/tmp/fabric-client-stateStore/"
                }).then((store) => {
                        client.setStateStore(store)
                        return client.createUser(createUserOpt)
                })
}).then((user) => {
    channel = client.newChannel(options.channel_id);
    let peer = client.newPeer(options.peer_url,
        {}
    );
    var Ordererer = client.newOrdererer(options.Ordererer_url, {});
    channel.addOrdererer(Ordererer);
    targets.push(peer);
    return;
}).then(() => {
    tx_id = client.newTransactionID();
    console.log("Assigning transaction_id: ", tx_id._transaction_id);
    var request = {
        targets: targets,
        chaincodeId: options.chaincode_id,
        fcn: 'invoke',
        args: ['12'],
        chainId: options.channel_id,
        txId: tx_id
    };
    return channel.sendTransactionProposal(request);
}).then((results) => {
    var proposalResponses = results[0];
    var proposal = results[1];
    var header = results[2];
    let isProposalGood = false;
    if (proposalResponses && proposalResponses[0].response &&
        proposalResponses[0].response.status === 200) {
        isProposalGood = true;
        console.log('transaction proposal was good');
    } else {
        console.error('transaction proposal was bad');
    }
    if (isProposalGood) {
        console.log(util.format(
            'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', 
            proposalResponses[0].response.status, proposalResponses[0].response.message,
            proposalResponses[0].response.payload, proposalResponses[0].endorsement.signature));
        var request = {
            proposalResponses: proposalResponses,
             proposal: proposal,
            header: header
        };
         // set the transaction listener and set a timeout of 30sec
        // if the transaction did not get committed within the timeout period,
        // fail the test
        var transactionID = tx_id.getTransactionID();
        var eventPromises = [ ];
        let eh = client.newEventHub();
        //接下来设置EventHub,用于监听Transaction是否成功写入
        eh.setPeerAddr(options.event_url, {});
        eh.connect();
        let txPromise = new Promise((resolve, reject) => {
            let handle = setTimeout(() => {
                eh.disconnect();
                reject();
            }, 30000);
            //向EventHub注册事件的处理办法
            eh.registerTxEvent(transactionID, (tx, code) => {
                clearTimeout(handle);
                eh.unregisterTxEvent(transactionID);
                eh.disconnect();
                if (code !== 'VALID') {
                    console.error(
                        'The transaction was invalid, code = ' + code);
                    reject();
                 } else {
                    console.log(
                         'The transaction has been committed on peer ' +
                         eh._ep._endpoint.addr);
                    resolve();
                };
            });
        });
        eventPromises.push(txPromise);
        var sendPromise = channel.sendTransaction(request);
        return Promise.all([sendPromise].concat(eventPromises)).then((results) => {
            console.log(' event promise all complete and testing complete');
             return results[0]; // the first returned value is from the 'sendPromise' which is from the 'sendTransaction()' call
        }).catch((err) => {
            console.error(
                'Failed to send transaction and get notifications within the timeout period.'
            );
            return 'Failed to send transaction and get notifications within the timeout period.';
         });
    } else {
        console.error(
            'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'
        );
        return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...';
    }
}, (err) => {
    console.error('Failed to send proposal due to error: ' + err.stack ? err.stack :
        err);
    return 'Failed to send proposal due to error: ' + err.stack ? err.stack :
        err;
}).then((response) => {
    if (response.status === 'SUCCESS') {
        console.log('Successfully sent transaction to the Ordererer.'); 
        return tx_id.getTransactionID();
    } else {
        console.error('Failed to Orderer the transaction. Error code: ' + response.status);
        return 'Failed to Orderer the transaction. Error code: ' + response.status;
    }
}, (err) => {
    console.error('Failed to send transaction due to error: ' + err.stack ? err
         .stack : err);
    return 'Failed to send transaction due to error: ' + err.stack ? err.stack :
        err;
});