Caliper 프로젝트에는 앞의 포스팅에서 언급한 바와 같이 fabric, sawtooth, iroha 등 Hyperledger 프로젝트로 구성된 Example이 있습니다. 여기서는 그 중에서 fabric을 기반으로 한 caliper 코드를 살펴보겠습니다.
디렉토리 구조(Directory Structure)
디렉토리 | 설명 |
---|---|
/benchmark | 블록체인 벤치마크 샘플 |
/docs | 문서 |
/network | 테스트중에 있는 미리 정의된 블록체인 네트워크를 배포하는 데 사용되는 부팅 구성 파일. |
/src | 프레임워크의 소스코드 |
/src/contract | 다른 블록체인 시스템을 위한 스마트 컨트렉 |
Flow
네트워크 구성(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 |