본문 바로가기

IT/Block Chain(Hyperledger)

Caliper 구조

Caliper 프로젝트에는 앞의 포스팅에서 언급한 바와 같이 fabric, sawtooth, iroha 등 Hyperledger 프로젝트로 구성된 Example이 있습니다. 여기서는 그 중에서 fabric을 기반으로 한 caliper 코드를 살펴보겠습니다.


디렉토리 구조(Directory Structure)

디렉토리설명
/benchmark블록체인 벤치마크 샘플
/docs문서
/network테스트중에 있는 미리 정의된 블록체인 네트워크를 배포하는 데 사용되는 부팅 구성 파일.
/src프레임워크의 소스코드
/src/contract다른 블록체인 시스템을 위한 스마트 컨트렉


Flow

180614 Caliper 구조도.png

네트워크 구성(fabric.json)

경로: caliper/benchmark/simple

네트워크: orderer 1개, org1의 peer1과 peer2, org2의 peer1과 peer2 로 구성

채널: mychannel(채널생성에 대한 테스트를 하지 않으려면 "deployed": true 항목을 추가

체인코드: contract/fabric/simple/simpletest.go

보증정책: OrgMSP1 - User, Admin / OrgMSP2 - User

context: open, query


{
  "fabric": {
    "cryptodir": "network/fabric/simplenetwork/crypto-config",
    "network": {
      "orderer": {
        "url": "grpcs://localhost:7050",
        "mspid": "OrdererMSP",
        "msp": "network/fabric/simplenetwork/crypto-config/ordererOrganizations/example.com/msp/",
        "server-hostname": "orderer.example.com",
        "tls_cacerts": "network/fabric/simplenetwork/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt"
      },
      "org1": {
        "name": "peerOrg1",
        "mspid": "Org1MSP",
        "msp": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org1.example.com/msp/",
        "ca": {
          "url": "https://localhost:7054",
          "name": "ca-org1"
        },
        "peer1": {
          "requests": "grpcs://localhost:7051",
          "events": "grpcs://localhost:7053",
          "server-hostname": "peer0.org1.example.com",
          "tls_cacerts": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"
        },
        "peer2": {
          "requests": "grpcs://localhost:7057",
          "events": "grpcs://localhost:7059",
          "server-hostname": "peer1.org1.example.com",
          "tls_cacerts": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/ca.crt"
        }
      },
      "org2": {
        "name": "peerOrg2",
        "mspid": "Org2MSP",
        "msp": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org2.example.com/msp/",
        "ca": {
          "url": "https://localhost:8054",
          "name": "ca-org2"
        },
        "peer1": {
          "requests": "grpcs://localhost:8051",
          "events": "grpcs://localhost:8053",
          "server-hostname": "peer0.org2.example.com",
          "tls_cacerts": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"
        },
        "peer2": {
          "requests": "grpcs://localhost:8057",
          "events": "grpcs://localhost:8059",
          "server-hostname": "peer1.org2.example.com",
          "tls_cacerts": "network/fabric/simplenetwork/crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt"
        }
      }
    },
    "channel": [
      {
        "name": "mychannel",
        "config": "network/fabric/simplenetwork/mychannel.tx",
        "organizations": ["org1", "org2"]
      }
    ],
    "chaincodes": [{"id": "simple", "path": "contract/fabric/simple", "language":"golang", "version": "v0", "channel": "mychannel"}],
    "endorsement-policy": {
      "identities": [
        {
          "role": {
            "name": "member",
            "mspId": "Org1MSP"
          }
        },
        {
          "role": {
            "name": "member",
            "mspId": "Org2MSP"
          }
        },
        {
          "role": {
            "name": "admin",
            "mspId": "Org1MSP"
          }
        }
      ],
      "policy": { "2-of": [{"signed-by": 0}, {"signed-by": 1}]}
    },
    "context": {
      "open": "mychannel",
      "query": "mychannel"
    }
  },
  "info" : {
    "Version": "1.0.5",
    "Size": "4 Peers",
    "Orderer": "Solo",
    "Distribution": "Single Host"
  }
}

Test 구성(config.json)

경로: caliper/benchmark/config.json

command: 도커 구성(docke-compose.yaml 이용) [원치않으면, 생략 가능]

test 구성:

  • clients 수
  • round(횟수) 지정
    • callback함수 2개(open.js, query.js)
    • open 3개, query 2개
    • 실행할 txNumber, tps 설정
    • cf) open의 경우, arguments를 money: 10000으로 설정


{
  "blockchain": {
    "type": "fabric",
    "config": "benchmark/simple/fabric.json"
  },
  "command" : {
    "start": "docker-compose -f network/fabric/simplenetwork/docker-compose.yaml up -d",
    "end" : "docker-compose -f network/fabric/simplenetwork/docker-compose.yaml down;docker rm $(docker ps -aq)"
  },
  "test": {
    "name": "simple",
    "description" : "This is an example benchmark for caliper, to test the backend DLT's performance with simple account opening & querying transactions",
    "clients": {
      "type": "local",
      "number": 5
    },
    "rounds": [{
        "label" : "open",
        "txNumber" : [5000, 5000, 5000],
        "rateControl" : [{"type": "fixed-rate", "opts": {"tps" : 100}}, {"type": "fixed-rate", "opts": {"tps" : 200}}, {"type": "fixed-rate", "opts": {"tps" : 300}}],
        "arguments": { "money": 10000 },
        "callback" : "benchmark/simple/open.js"
      },
      {
        "label" : "query",
        "txNumber" : [5000, 5000],
        "rateControl" : [{"type": "fixed-rate", "opts": {"tps" : 300}}, {"type": "fixed-rate", "opts": {"tps" : 400}}],
        "callback" : "benchmark/simple/query.js"
      }]
  },
  "monitor": {
    "type": ["docker", "process"],
    "docker":{
      "name": ["all"]
    },
    "process": [
      {
        "command" : "node",
        "arguments" : "local-client.js",
        "multiOutput" : "avg"
      }
    ],
    "interval": 1
  }
}


Test Callback(open.js)

경로: caliper/benchmark/open.js

계좌를 개설(open)하는 콜백 함수


/**
* Copyright 2017 HUAWEI. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
*/

'use strict';

module.exports.info  = 'opening accounts';

let accounts = [];
let initMoney;
let bc, contx;
module.exports.init = function(blockchain, context, args) {
    //config.json에서 선언한 인자값 존재여부 확인
    if(!args.hasOwnProperty('money')) {
        return Promise.reject(new Error('simple.open - "money" is missed in the arguments'));
    }

    //값 대입
    initMoney = args.money.toString();

    bc = blockchain;
    contx = context;
    return Promise.resolve();
};

const dic = 'abcdefghijklmnopqrstuvwxyz';
/**
 * Generate string by picking characters from dic variable
 * --==>> dic 변수에서 문자를 선택하여 문자열 생성
 * @param {*} number character to select
 * @returns {String} string generated based on @param number
 */
function get26Num(number){
    let result = '';
    while(number > 0) {
        result += dic.charAt(number % 26);
        number = parseInt(number/26);
    }
    return result;
}

let prefix;
/**
 * 거래에 고유한 계정 키 생성
 * Generate unique account key for the transaction
 * 리턴: account key
 * @returns {String} account key
 */
function generateAccount() {
    // should be [a-z]{1,9}
    if(typeof prefix === 'undefined') {
        prefix = get26Num(process.pid);
    }
    //console.log('$%#@!#---------- '+ prefix+' '+get26Num(accounts.length+1));
    //result: $%#@!#---------- cswg d
    return prefix + get26Num(accounts.length+1);
}

module.exports.run = function() {
    let newAcc = generateAccount();
    accounts.push(newAcc);
    return bc.invokeSmartContract(contx, 'simple', 'v0', {verb: 'open', account: newAcc, money: initMoney}, 30);
};

module.exports.end = function(results) {
    return Promise.resolve();
};

module.exports.accounts = accounts;


Test Callback(query.js)

경로: caliper/benchmark/query.js

개설된계좌를 조회(query)하는 콜백 함수


/**
* Copyright 2017 HUAWEI. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
*/

'use strict';

module.exports.info  = 'querying accounts';


let bc, contx;
let accounts;
module.exports.init = function(blockchain, context, args) {
    //open.js에서 선언한 변수 불러오기
    const open = require('./open.js');
    bc       = blockchain;
    contx    = context;
    accounts = open.accounts;
    return Promise.resolve();
};

module.exports.run = function() {
    //Math.Random()의 범위 -- 0.0~ 1.0사이의 실수
    //Math.floor 버림
    //accounts 배열 숫자를 랜덤으로 넣어줌
    const acc  = accounts[Math.floor(Math.random()*(accounts.length))];

    return bc.queryState(contx, 'simple', 'v0', acc);
};

module.exports.end = function(results) {
    // do nothing
    return Promise.resolve();
};



블록체인 내장함수(blockchain.js)

경로: caliper/src/comm

/**
* Copyright 2017 HUAWEI. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
*/

'use strict';

/**
 * BlockChain class, define operations to interact with the blockchain system under test
 */
class Blockchain {
    /**
     * Constructor
     * @param {String} configPath path of the blockchain configuration file
     */
    constructor(configPath) {
        let config = require(configPath);

        if(config.hasOwnProperty('fabric')) {
            let fabric = require('../fabric/fabric.js');
            this.bcType = 'fabric';
            this.bcObj = new fabric(configPath);
        }
        else if(config.hasOwnProperty('sawtooth')) {
            let sawtooth = require('../sawtooth/sawtooth.js');
            this.bcType = 'sawtooth';
            this.bcObj = new sawtooth(configPath);
        }
        else if(config.hasOwnProperty('iroha')) {
            let iroha = require('../iroha/iroha.js');
            this.bcType = 'iroha';
            this.bcObj = new iroha(configPath);
        }
        else if(config.hasOwnProperty('composer')) {
            let composer = require('../composer/composer.js');
            this.bcType = 'composer';
            this.bcObj = new composer(configPath);
        }
        else {
            this.bcType = 'unknown';
            throw new Error('Unknown blockchain config file ' + configPath);
        }
    }

    /**
     * return the blockchain's type
     * @return {string} type of the blockchain
     */
    gettype() {
        return this.bcType;
    }

    /**
    * Initialise test environment, e.g. create a fabric channel for the test
    * @return {Promise} promise object
    */
    init() {
        return this.bcObj.init();
    }

    /**
     * Perform required preparation for test clients, e.g. enroll clients and obtain key pairs
     * @param {Number} number count of test clients
     * @return {Promise} array of obtained material for test clients
     */
    prepareClients (number) {
        return this.bcObj.prepareClients(number);
    }

    /**
    * Install smart contract(s), detail informations are defined in the blockchain configuration file
    * @return {Promise} promise object
    */
    installSmartContract() {
        return this.bcObj.installSmartContract();
    }

    /**
     * Get a context for subsequent operations, e.g. invoke smart contract or query state
     * @param {String} name name of the context
     * @param {Object} args adapter specific arguments
     * @return {Promise} obtained context object
     */
    getContext(name, args) {
        return this.bcObj.getContext(name, args);
    }

    /**
     * Release a context as well as related resources
     * @param {Object} context adapter specific object
     * @return {Promise} promise object
     */
    releaseContext(context) {
        return this.bcObj.releaseContext(context);
    }

    /**
     * Invoke smart contract/submit transactions and return corresponding transactions' status
     * txStatus = {
     *     'id': transaction's id
     *     'status':  status of the transaction, should be:
     *                - 'created': successfully created, but not validated or committed yet
     *                - 'success': successfully validated and committed in the ledger
     *     'time_create': time(ms) that the transaction was created
     *     'time_final':  time(ms) that the transaction was known to be final and committed in ledger
     *     'result': response payloads of the transaction request
     *     ...... :  other adapter specific properties
     * }
     * @param {Object} context context object
     * @param {String} contractID identiy of the contract
     * @param {String} contractVer version of the contract
     * @param {Array} args array of JSON formatted arguments for multiple transactions
     * @param {Number} timeout request timeout, in second
     * @return {Promise} txStatus object or an array of txStatus objects
     */
    invokeSmartContract(context, contractID, contractVer, args, timeout) {
        let arg, time;    // compatible with old version
        if(Array.isArray(args)) {
            arg = args;
        }
        else if(typeof args === 'object') {
            arg = [args];
        }
        else {
            return Promise.reject(new Error('Invalid args for invokeSmartContract()'));
        }

        if(typeof timeout !== 'number' || timeout < 0) {
            time = 120;
        }
        else {
            time = timeout;
        }

        return this.bcObj.invokeSmartContract(context, contractID, contractVer, arg, time);
    }

    /**
     * Query state from the ledger
     * @param {Object} context context object from getContext
     * @param {String} contractID identiy of the contract
     * @param {String} contractVer version of the contract
     * @param {String} key lookup key
     * @return {Promise} as invokeSmateContract()
     */
    queryState(context, contractID, contractVer, key) {
        return this.bcObj.queryState(context, contractID, contractVer, key);
    }

    /**
    * Calculate the default transaction statistics
    * @param {Array} results array of txStatus
    * @param {Boolean} detail indicates whether to keep detailed information
    * @return {JSON} txStatistics JSON object
    */
    getDefaultTxStats(results, detail) {
        let succ = 0, fail = 0, delay = 0;
        let minFinal, maxFinal, minCreate, maxCreate;
        let minDelay = 100000, maxDelay = 0;
        let delays = [];
        for(let i = 0 ; i < results.length ; i++) {
            let stat   = results[i];
            let create = stat.time_create;

            if(typeof minCreate === 'undefined') {
                minCreate = create;
                maxCreate = create;
            }
            else {
                if(create < minCreate) {
                    minCreate = create;
                }
                if(create > maxCreate) {
                    maxCreate = create;
                }
            }

            if(stat.status === 'success') {
                succ++;
                let final = stat.time_final;
                let d     = (final - create) / 1000;
                if(typeof minFinal === 'undefined') {
                    minFinal = final;
                    maxFinal = final;
                }
                else {
                    if(final < minFinal) {
                        minFinal = final;
                    }
                    if(final > maxFinal) {
                        maxFinal = final;
                    }
                }

                delay += d;
                if(d < minDelay) {
                    minDelay = d;
                }
                if(d > maxDelay) {
                    maxDelay = d;
                }

                if(detail) {
                    delays.push(d);
                }
            }
            else {
                fail++;
            }
        }

        let stats = {
            'succ' : succ,
            'fail' : fail,
            'create' : {'min' : minCreate/1000, 'max' : maxCreate/1000},    // convert to second
            'final'  : {'min' : minFinal/1000,  'max' : maxFinal/1000 },
            'delay'  : {'min' : minDelay,  'max' : maxDelay, 'sum' : delay, 'detail': (detail?delays:[]) },
            'out' : []
        };
        return stats;
    }

    /**
     * merge an array of default 'txStatistics', the result is in first object of the array
     * Note even failed the first object of the array may still be changed
     * @param {Array} results txStatistics array
     * @return {Number} 0 if failed; otherwise 1
     */
    static mergeDefaultTxStats(results) {
        try{
            // skip invalid result
            let skip = 0;
            for(let i = 0 ; i < results.length ; i++) {
                let result = results[i];
                if(!result.hasOwnProperty('succ') || !result.hasOwnProperty('fail') || (result.succ + result.fail) === 0) {
                    skip++;
                }
                else {
                    break;
                }
            }
            if(skip > 0) {
                results.splice(0, skip);
            }

            if(results.length === 0) {
                return 0;
            }

            let r = results[0];
            for(let i = 1 ; i < results.length ; i++) {
                let v = results[i];
                if(!v.hasOwnProperty('succ') || !v.hasOwnProperty('fail') || (v.succ + v.fail) === 0) {
                    continue;
                }
                r.succ += v.succ;
                r.fail += v.fail;
                r.out.push.apply(r.out, v.out);
                if(v.create.min < r.create.min) {
                    r.create.min = v.create.min;
                }
                if(v.create.max > r.create.max) {
                    r.create.max = v.create.max;
                }
                if(v.final.min < r.final.min) {
                    r.final.min = v.final.min;
                }
                if(v.final.max > r.final.max) {
                    r.final.max = v.final.max;
                }
                if(v.delay.min < r.delay.min) {
                    r.delay.min = v.delay.min;
                }
                if(v.delay.max > r.delay.max) {
                    r.delay.max = v.delay.max;
                }
                r.delay.sum += v.delay.sum;
                for(let j = 0 ; j < v.delay.detail.length ; j++) {
                    r.delay.detail.push(v.delay.detail[j]);
                }
            }
            return 1;
        }
        catch(err) {
            return 0;
        }
    }

    /**
     * create a 'null' txStatistics object
     * @return {JSON} 'null' txStatistics object
     */
    static createNullDefaultTxStats() {
        return {succ: 0, fail: 0};
    }
}

module.exports = Blockchain;

chaincode(simpletest.go)

경로: caliper/src/contract/fabric/simple

계좌 개설(Open), 삭제(Delete), 조회(Query), 이체(Transfer)체인코드

/**
* Copyright 2017 HUAWEI. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
*/

package main

import (
	"fmt"
	"strconv"

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

const ERROR_SYSTEM = "{\"code\":300, \"reason\": \"system error: %s\"}"
const ERROR_WRONG_FORMAT = "{\"code\":301, \"reason\": \"command format is wrong\"}"
const ERROR_ACCOUNT_EXISTING = "{\"code\":302, \"reason\": \"account already exists\"}"
const ERROR_ACCOUT_ABNORMAL = "{\"code\":303, \"reason\": \"abnormal account\"}"
const ERROR_MONEY_NOT_ENOUGH = "{\"code\":304, \"reason\": \"account's money is not enough\"}"



type SimpleChaincode struct {

}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
	// nothing to do
	return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	function, args := stub.GetFunctionAndParameters()

	if function == "open" {
		return t.Open(stub, args)
	}
	if function == "delete" {
		return t.Delete(stub, args)
	}
	if function == "query" {
		return t.Query(stub, args)
	}
	if function == "transfer" {
		return t.Transfer(stub, args)
	}

	return shim.Error(ERROR_WRONG_FORMAT)
}

// open an account, should be [open account money]
func (t *SimpleChaincode) Open(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	if len(args) != 2 {
		return shim.Error(ERROR_WRONG_FORMAT)
	}

	account  := args[0]
	money,err := stub.GetState(account)
	if money != nil {
		return shim.Error(ERROR_ACCOUNT_EXISTING)
	}

	_,err = strconv.Atoi(args[1])
	if err != nil {
		return shim.Error(ERROR_WRONG_FORMAT)
	}

	err = stub.PutState(account, []byte(args[1]))
	if err != nil {
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}

	return shim.Success(nil)
}

// delete an account, should be [delete account]
func (t *SimpleChaincode) Delete(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	if len(args) != 1 {
		return shim.Error(ERROR_WRONG_FORMAT)
	}

	err := stub.DelState(args[0])
	if err != nil {
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}

	return shim.Success(nil)
}

// query current money of the account,should be [query accout]
func (t *SimpleChaincode) Query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	if len(args) != 1 {
		return shim.Error(ERROR_WRONG_FORMAT)
	}

	money, err := stub.GetState(args[0])
	if err != nil {
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}

	if money == nil {
		return shim.Error(ERROR_ACCOUT_ABNORMAL)
	}

	return shim.Success(money)
}

// transfer money from account1 to account2, should be [transfer accout1 accout2 money]
func (t *SimpleChaincode) Transfer(stub shim.ChaincodeStubInterface, args []string) pb.Response {
	if len(args) != 3 {
		return shim.Error(ERROR_WRONG_FORMAT)
	}
	money, err := strconv.Atoi(args[1])
	if err != nil {
		return shim.Error(ERROR_WRONG_FORMAT)
	}

	moneyBytes1, err1 := stub.GetState(args[0])
	moneyBytes2, err2 := stub.GetState(args[0])
	if err1 != nil || err2 != nil {
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}
	if moneyBytes1 == nil || moneyBytes2 == nil {
		return shim.Error(ERROR_ACCOUT_ABNORMAL)
	}

	money1, _ := strconv.Atoi(string(moneyBytes1))
	money2, _ := strconv.Atoi(string(moneyBytes1))
	if money1 < money {
		return shim.Error(ERROR_MONEY_NOT_ENOUGH)
	}

	money1 -= money
	money2 += money

	err = stub.PutState(args[0], []byte(strconv.Itoa(money1)))
	if err != nil {
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}

	err = stub.PutState(args[1], []byte(strconv.Itoa(money2)))
	if err != nil {
		stub.PutState(args[0], []byte(strconv.Itoa(money1+money)))
		s := fmt.Sprintf(ERROR_SYSTEM, err.Error())
		return shim.Error(s)
	}

	return shim.Success(nil)
}


func  main()  {
	err := shim.Start(new(SimpleChaincode))
	if err != nil {
		fmt.Printf("Error starting chaincode: %v \n", err)
	}

}




'IT > Block Chain(Hyperledger)' 카테고리의 다른 글

byfn 2.0 시작하기  (0) 2020.04.28
Minikube에 블록체인 네트워크 배포하기  (0) 2019.05.09
Hyperledger Caliper 개요  (0) 2018.08.28
Steem 그리고, Steemit 정의  (1) 2017.12.28
Hyperledger Composer  (0) 2017.12.28