Hyperledger-Fabric / 区块链 · 2018年4月18日 0

Hyperledger-Fabric安装开发部署(六)智能合约的安装与更新

简介

  • 之前虽然介绍了智能合约,但是并没有说明如何更新部署;在实际运用中我们的合约代码必然涉及到升级更新,甚至完全废弃更换新的合约代码;很有必要在这里讲述一下在NodeSDK下如何实现这些。
  • 如果接触过以太坊会知道在以太坊中这是一件非常烦的事情,但是在Fabric中变的非常简单,且容易理解。不过Fabric的更新合约的功能也是在1.0.0加入的。
  • 此篇所需要的开发环境与Hyperledger-Fabric安装开发部署(四)NodeJs-SDKHyperledger-Fabric安装开发部署(五)NodeJs-SDK详解相同。

官方英文版教程:
http://hyperledger-fabric.readthedocs.io/en/latest/index.html


1.撰写新的合约

按照e2e的模型我们写一个简单的合约如下:

package main
import (
    "fmt"
    "strconv"

    "github.com/hyperledger/fabric/core/chaincode/shim"
    pb "github.com/hyperledger/fabric/protos/peer"
)

var logger = shim.NewLogger("e2eTestcc_01")

// SimpleChaincode example simple Chaincode implementation
type SimpleChaincode struct {
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response  {
    logger.Info("########### examplee2eTestcc_01_cc0 Init ###########")
    _, args := stub.GetFunctionAndParameters()
    var A string    // Entities
    var Aval int // Asset holdings
    var err error
    A = args[0]
    Aval, err = strconv.Atoi(args[1])
    if err != nil {
        return shim.Error("Expecting integer value for asset holding")
    }

    err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
    if err != nil {
        return shim.Error(err.Error())
    }
    return shim.Success(nil)
}

// Transaction makes payment of X units from A to B
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    logger.Info("########### e2eTestcc_01 Invoke ###########")

    function, args := stub.GetFunctionAndParameters()

    if function == "create" {
        return t.create(stub, args)
    }

    if function == "query" {
        // queries an entity state
        return t.query(stub, args)
    }
    if function == "move" {
        // Deletes an entity from its state
        return t.move(stub, args)
    }

    logger.Errorf("Unknown action, check the first argument, must be one of 'delete', 'query', or 'move'. But got: %v", args[0])
    return shim.Error(fmt.Sprintf("Unknown action, check the first argument, must be one of 'delete', 'query', or 'move'. But got: %v", args[0]))
}

func (t *SimpleChaincode) move(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    // must be an invoke
    var A, B string    // Entities
    var Aval, Bval int // Asset holdings
    var X int          // Transaction value
    var err error

    if len(args) != 3 {
        return shim.Error("Incorrect number of arguments. Expecting 4, function followed by 2 names and 1 value")
    }

    A = args[0]
    B = args[1]

    // Get the state from the ledger
    // TODO: will be nice to have a GetAllState call to ledger
    Avalbytes, err := stub.GetState(A)
    if err != nil {
        return shim.Error("Failed to get state")
    }
    if Avalbytes == nil {
        return shim.Error("Entity not found")
    }
    Aval, _ = strconv.Atoi(string(Avalbytes))

    Bvalbytes, err := stub.GetState(B)
    if err != nil {
        return shim.Error("Failed to get state")
    }
    if Bvalbytes == nil {
        return shim.Error("Entity not found")
    }
    Bval, _ = strconv.Atoi(string(Bvalbytes))

    // Perform the execution
    X, err = strconv.Atoi(args[2])
    if err != nil {
        return shim.Error("Invalid transaction amount, expecting a integer value")
    }
    Aval = Aval - X
    Bval = Bval + X
    logger.Infof("Aval = %d, Bval = %d\n", Aval, Bval)

    // Write the state back to the ledger
    err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
    if err != nil {
        return shim.Error(err.Error())
    }

    err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
    if err != nil {
        return shim.Error(err.Error())
    }

        return shim.Success(nil);
}


func (t *SimpleChaincode) create(stub shim.ChaincodeStubInterface, args []string) pb.Response {

    var A string    // Entities
    var Aval int // Asset holdings
    var err error
    A = args[0]
    Aval, err = strconv.Atoi(args[1])
    if err != nil {
        return shim.Error("Expecting integer value for asset holding")
    }

    err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
    if err != nil {
        return shim.Error(err.Error())
    }
    return shim.Success(nil)
}

// Query callback representing the query of a chaincode
func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {

    var A string // Entities
    var err error

    if len(args) != 1 {
        return shim.Error("Incorrect number of arguments. Expecting name of the person to query")
    }

    A = args[0]

    // Get the state from the ledger
    Avalbytes, err := stub.GetState(A)
    if err != nil {
        jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"
        return shim.Error(jsonResp)
    }

    if Avalbytes == nil {
        jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}"
        return shim.Error(jsonResp)
    }

    jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}"
    logger.Infof("Query Response:%s\n", jsonResp)
    return shim.Success(Avalbytes)
}

func main() {
    err := shim.Start(new(SimpleChaincode))
    if err != nil {
        logger.Errorf("Error starting Simple chaincode: %s", err)
    }
}

将以上内容保存至D:\....\balance-transfer\artifacts\src\github.com\newmycc\e2eTestcc_01.go

在之前e2e的智能合约中只能再初始化的时候加入a,b并不能加入更多的key;上述合约加入了一个Create(),保留了Move()与Query();我们可以加入更多的c,d,e....

至于Go版的智能合约如何脱离Fabric进行调试、编译、测试等,后续会在新的文章中介绍。

2.安装智能合约

2.1 现有的智能合约

使用vscode运行balance-transfer,使用post工具(本文使用Restlet Client)调用查询已安装的智能合约

可以看到目前在peer1上已安装的合约只有一个"name: mycc, version: 1.0"
将上图中的type参数改为instantiated再次查询;
得到的结果是一样的,这个表示已经初始化的智能合约
以上查询在后续我们还要用到,需要保留对应post脚本。

注:本次操作使用org1上的用户,如无特别说明,均默认org1用户。

2.2 安装智能合约

调用如下接口:

给peer1、与peer2上同时安装新的合约,并提示操作成功。

先分析一下nodejs的代码:
对照/app/install-chaincode.js

...
...
var installChaincode = function(peers, chaincodeName, chaincodePath,
    chaincodeVersion, username, org) {
    logger.debug('\n============ Install chaincode on organizations ============\n');
    helper.setupChaincodeDeploy();
    var channel = helper.getChannelForOrg(org);
    var client = helper.getClientForOrg(org);
    return helper.getOrgAdmin(org).then((user) => {
        //这里有个可选参数:chaincodeType
        //用来表示链码的类型。 “golang”,“car”,“node”或“java”之一。 默认是'golang'。 请注意,从v1.0开始不支持'java'。
        //其他可参阅文档
        var request = {
            targets: helper.newPeers(peers, org),
            chaincodePath: chaincodePath,
            chaincodeId: chaincodeName,
            chaincodeVersion: chaincodeVersion
        };
        return client.installChaincode(request);
...
...

这里只是收集一些信息交给SDK中client.installChaincode()做处理,installChaincode()介绍: 官方SDK说明

操作完成后,我们再次调用刚才使用的查询已安装的智能合约接口,会得到如下的结果:

[
"name: mycc, version: 1.0, path: github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02",
"name: newmycc1, version: v1, path: github.com/newmycc"
]

可以看到我们的newmycc1已经成功安装,但是在Fabric中智能合约的安装成功并不代表这份合约可以被使用,必须要初始化之后才可以使用;可以使用刚才介绍的已经初始化的智能合约接口,newmycc1并不会出现在列表中。
安装仅仅是上传代码文件、创建链码的名称与版本信息,甚至不会再这一步编译Go文件。

2.3 初始化智能合约

使用如下接口初始化刚才安装的智能合约:

能看到我这里使用了52.21s,这个操作相对比较耗时,就会有超时的情况出现,需要在"/app/instantiate-chaincode.js"中修改默认超时时间:

...
...
        tx_id = client.newTransactionID();
        // send proposal to endorser
        // request参数中,是可以选择初始化某一个peer的,如不填写则是当前org下所有的peer
        var request = {
            chaincodeId: chaincodeName,
            chaincodeVersion: chaincodeVersion,
            args: args,
            txId: tx_id
        };
        if (functionName)
            request.fcn = functionName;
        //需要将下一行修改为:return channel.sendInstantiateProposal(request,999999);
        return channel.sendInstantiateProposal(request);
...
...

详细说明可以参阅sendInstantiateProposal()的官方SDK文档官方SDK文档

在初始化成功后,在服务器上会出现两个新的docker容器,对应了我们的两个节点,每个节点上的链码独立运行:

初始化的时候主要是将指定的智能合约进行如下操作:
1. 检测编译
2. 建立docker容器
3. docker内实例化链码
4. 执行初始化方法(可以理解为构造函数)


接下来进行验证;再次调用已初始化的智能合约接口来查询,结果如下:

[
"name: mycc, version: 1.0, path: github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02",
"name: newmycc1, version: v1, path: github.com/newmycc"
]

初始化的时候有一个参数是"args":["c","10"]
那么调用上一篇所使用的查询接口查询c的值试试:

然而没有查询到c对应的值,这是因为:
这次查询使用的智能合约是mycc,并不是我们新建的newmycc1;不同的链码之间数据也是隔离的。

了解这一点后,将mycc修改为newmycc1,再试一次:

至此,说明新的合约已经完全生效,再试试新合约中Create():

也是可以正常调通的,在使用上边的查询验证d的值,出现如下的结果说明新的合约以及新的方法均没有问题。
d now has 100 after the move

3.更新智能合约

有了代码的安装那么必然会有代码的升级,在我们安装和初始化的过程中都有一个版本号"chaincodeVersion":"v1",但是在使用的时候没有提交版本号的参数,就是说:更新链码的版本不会涉及到外部调用的修改,每次调用智能合约均会使用最新的版本。

首先对新的合约做少许修改,在e2eTestcc_01.go中做如下修改

...
...
// Transaction makes payment of X units from A to B
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
...
...
    // 加入以下代码
    if function == "delete" {
        // Deletes an entity from its state
        return t.delete(stub, args)
    }
...
...
}
...
...
// 加入以下代码
// Deletes an entity from state
func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    if len(args) != 1 {
        return shim.Error("Incorrect number of arguments. Expecting 1")
    }

    A := args[0]

    // Delete the key from the state in ledger
    err := stub.DelState(A)
    if err != nil {
        return shim.Error("Failed to delete state")
    }

    return shim.Success(nil)
}

3.1 更新智能合约-安装

这一步需要使用2.2 安装智能合约中的安装方法,区别只是将"v1"修改为"v2",合约文件已经修改完毕,目录也没有变化无需修改。

提示安装成功后,可以继续使用刚才提到的查询方法,验证是否已经安装成功。如下图可以看到多出来一个v2的版本。

[
"name: mycc, version: 1.0, path: github.com/hyperledger/fabric/examples/chaincode/go/chaincode_example02",
"name: newmycc1, version: v1, path: github.com/newmycc",
"name: newmycc1, version: v2, path: github.com/newmycc"
]

3.2 更新智能合约-更新

在balance-transfer项目中目前并没有关于更新的接口,但是在SDK中是存在的,只不过没有实现而已。

新建/app/upgrade-chaincode.js文件,将以下内容复制:

'use strict';
var path = require('path');
var fs = require('fs');
var util = require('util');
var hfc = require('fabric-client');
var Peer = require('fabric-client/lib/Peer.js');
var EventHub = require('fabric-client/lib/EventHub.js');
var helper = require('./helper.js');
var logger = helper.getLogger('upgrade-chaincode');
var ORGS = hfc.getConfigSetting('network-config');
var tx_id = null;
var eh = null;

var upgradeChaincode = function (peers,channelName, chaincodeName, chaincodeVersion, functionName, args, username, org) {
    logger.debug('\n============ upgrade chaincode on organization ' + org + ' ============\n');
    var channel = helper.getChannelForOrg(org);
    var client = helper.getClientForOrg(org);
    return helper.getOrgAdmin(org).then((user) => {
        // read the config block from the orderer for the channel
        // and initialize the verify MSPs based on the participating
        // organizations
        return channel.initialize();
    }, (err) => {
        logger.error('Failed to enroll user \'' + username + '\'. ' + err);
        throw new Error('Failed to enroll user \'' + username + '\'. ' + err);
    }).then((success) => {
        tx_id = client.newTransactionID();
        // send proposal to endorser
        var request = {
            targets: helper.newPeers(peers, org),
            chaincodeId: chaincodeName,
            chaincodeVersion: chaincodeVersion,
            args: args,
            txId: tx_id
        };
        if (functionName)
            request.fcn = functionName;
        return channel.sendUpgradeProposal(request,100000);
    }, (err) => {
        logger.error('Failed to upgrade the channel');
        throw new Error('Failed to upgrade the channel');
    }).then((results) => {
        var proposalResponses = results[0];
        var proposal = results[1];
        var all_good = true;
        for (var i in proposalResponses) {
            let one_good = false;
            if (proposalResponses && proposalResponses[i].response &&
                proposalResponses[i].response.status === 200) {
                one_good = true;
                logger.info('upgrade proposal was good');
            } else {
                logger.error('upgrade proposal was bad');
            }
            all_good = all_good & one_good;
        }
        if (all_good) {
            logger.info(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
            };
            // 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 deployId = tx_id.getTransactionID();

            eh = client.newEventHub();
            let data = fs.readFileSync(path.join(__dirname, ORGS[org].peers['peer1'][
                'tls_cacerts'
            ]));
            eh.setPeerAddr(ORGS[org].peers['peer1']['events'], {
                pem: Buffer.from(data).toString(),
                'ssl-target-name-override': ORGS[org].peers['peer1']['server-hostname']
            });
            eh.connect();

            let txPromise = new Promise((resolve, reject) => {
                let handle = setTimeout(() => {
                    eh.disconnect();
                    reject();
                }, 30000);

                eh.registerTxEvent(deployId, (tx, code) => {
                    logger.info('The chaincode upgrade transaction has been committed on peer ' + eh._ep._endpoint.addr);
                    clearTimeout(handle);
                    eh.unregisterTxEvent(deployId);
                    eh.disconnect();

                    if (code !== 'VALID') {
                        logger.error('The chaincode upgrade transaction was invalid, code = ' + code);
                        reject();
                    } else {
                        logger.info('The chaincode upgrade transaction was valid.');
                        resolve();
                    }
                });
            });

            var sendPromise = channel.sendTransaction(request);
            return Promise.all([sendPromise].concat([txPromise])).then((results) => {
                logger.debug('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) => {
                logger.error(util.format('Failed to send upgrade transaction and get notifications within the timeout period. %s', err));
                return 'Failed to send upgrade transaction and get notifications within the timeout period.';
            });
        } else {
            logger.error('Failed to send upgrade Proposal or receive valid response. Response null or status is not 200. exiting...');
            return 'Failed to send upgrade Proposal or receive valid response. Response null or status is not 200. exiting...';
        }
    }, (err) => {
        logger.error('Failed to send upgrade proposal due to error: ' + err.stack ?
            err.stack : err);
        return 'Failed to send upgrade proposal due to error: ' + err.stack ?
            err.stack : err;
    }).then((response) => {
        if (response.status === 'SUCCESS') {
            logger.info('Successfully sent transaction to the orderer.');
            return 'Chaincode upgrade is SUCCESS';
        } else {
            logger.error('Failed to order the transaction. Error code: ' + response.status);
            return 'Failed to order the transaction. Error code: ' + response.status;
        }
    }, (err) => {
        logger.error('Failed to send upgrade due to error: ' + err.stack ? err.stack : err);
        return 'Failed to send upgrade due to error: ' + err.stack ? err.stack : err;
    });
};
exports.upgradeChaincode = upgradeChaincode;

在app.js中加入如下代码:

//在头部的引用中加入/app/upgrade-chaincode.js
var upgrade = require('./app/upgrade-chaincode.js');
...
...
// 文件末尾加入如下方法
// upgrade chaincode on target peers
app.post('/channels/:channelName/upgrade', function (req, res) {
    logger.debug('==================== UPGRADE CHAINCODE ==================');
    var peers = req.body.peers;
    var chaincodeName = req.body.chaincodeName;
    var chaincodeVersion = req.body.chaincodeVersion;
    var channelName = req.params.channelName;
    var fcn = req.body.fcn;
    var args = req.body.args;
    logger.debug('channelName  : ' + channelName);
    logger.debug('chaincodeName : ' + chaincodeName);
    logger.debug('chaincodeVersion  : ' + chaincodeVersion);
    logger.debug('fcn  : ' + fcn);
    logger.debug('args  : ' + args);
    if (!chaincodeName) {
        res.json(getErrorMessage('\'chaincodeName\''));
        return;
    }
    if (!chaincodeVersion) {
        res.json(getErrorMessage('\'chaincodeVersion\''));
        return;
    }
    if (!channelName) {
        res.json(getErrorMessage('\'channelName\''));
        return;
    }
    // if (!args) {
    //  res.json(getErrorMessage('\'args\''));
    //  return;
    // }
    upgrade.upgradeChaincode(peers,channelName, chaincodeName, chaincodeVersion, fcn, args, req.username, req.orgname)
        .then(function (message) {
            res.send(message);
        });
});

调用新写的这个接口:

在这里只更新了peer1的链码,并没有更新peer2,只样做会导致peer2的对应链码无法使用,目前经过实测发现:
在peer1上安装合约A.V2,并且在peer1执行更新,这样操作对于peer1没有任何影响,正常调用新版本V2;但是当调用同一个org下的peer2的链码,会提示找不到A.V2,类似如下错误:
cannot retrieve package for chaincode newmycc1/v4, error open /var/hyperledger/production/chaincodes/newmycc1.v4: no such file or directory after the move
可是我们并没有在peer2上进行任何关于链码A.V2的操作;而且查询peer2上已经初始化的智能合约会发现,它的版本被A.V2覆盖了,不过在peer2上已安装的智能合约却不包含A.V2,这也就导致合约A在peer2下无法调用了。
尚不清楚这是Fabric就如此设计(同org下不同peer必须使用同版本的链码?)还是在node-SDK或者Fabric本身的BUG。


可以继续使用前文提到的各种查询验证此次更新合约是否成功;需要提醒的是再合约的更新中,也会执行Go中的构造函数,例如图中就将c值修改为88,可以利用查询验证c的值确实被修改为88。

4.总结

  1. 智能合约的安装:安装->初始化;
  2. 智能合约的更新:安装->更新;
  3. 链码不同的版本、安装不同的peer都会独立创建各自的docker images与docker container,均独立运行;
  4. 当服务器重启后,docker中智能合约的容器无需手动启动,会在调用的时候自行开启;
  5. 不同的智能合约数据独立存储

.
.
.
.
.
.
.
【本文章出自NM1024.com,转载请注明作者出处。】






>>转载请注明原文链接地址:Hyperledger-Fabric安装开发部署(六)智能合约的安装与更新