fabric网络运行过程中动态追加新的组织是相当复杂的,网上的资料也十分匮乏,大多是基于first-network这样的简单示例,而且是使用启动cli容器的方法来增加组织,几乎没有针对实际应用的解决方案。本文介绍了如何在应用程序中调用SDK来进行组织的动态增加。

前言

首先需要介绍一个配置区块的概念,fabric中的配置信息是作为区块写在链上的,每个配置区块中只有一条配置交易,而且配置区块是全量更新的,最新的配置区块中应包含全部的配置信息。

回忆一下在创建通道时,会从本地读取通道配置交易(根据configtx.yaml生成),这个配置交易中指定了该通道中有哪些组织,以及设置了各组织的证书信息。如果想要在后续进行添加,就必须要让当前通道认可这个新组织,则需要提交一个包含新加组织的配置区块来对当前配置进行更新。

大致思路是首先从节点中获取到当前通道的最新配置区块,利用configtxlator工具将配置信息由protobuf格式转化为可读的json格式,手动在配置中添加上新组织的配置,然后再使用该工具计算修改前后的差值,将这个增量作为通道更新的请求发送出去。同时,这个通道更新的请求需要超过半数的当前组织签名才算有效。

调用SDK增加组织

因为是在fabric实际应用中增加组织,所以通过在app中编写代码调用SDK来完成所有操作是最优的方案。而且一旦实现,在之后的应用开发中可以很方便地复用,再配合上一些自动化脚本可以使繁杂的操作变得简单化,做到轻松的增加或删除网络内的组织。

值得一提的是,官方的node-sdk中提供了一段关于通道更新的例子configtxlator.js,不过里面实现的是删除某个组织,我们可以做一些改动来实现添加组织。

本文以balance-transfer v1.0为例,介绍如何通过调用Node SDK的方法,在已有两个组织的基础上增加新组织Org3,其中包含1个CA节点,2个Peer节点。

一、生成新组织证书目录

因为进入fabric网络是需要身份的,所以不论是加入新节点还是加入新组织,都要为新增的成员生成MSP目录。在artifacts/channel目录下创建新组织的配置文件org3-crypto.yaml

PeerOrgs:
  - Name:Org3
    Domain: org3.example.com
    CA:
      Hostname: ca 
    Template:
      Count: 2
      SANS:
        - "localhost"
    Users:
      Count: 1

接着利用cryptogen工具生成Org3的msp目录,并输出到crypto-config目录中:

./cryptogen generate --config=./crypto-org2.yaml --output ./crypto-config

二、编写Nodejs代码调用SDK

我在app目录下创建了一个单独的文件add-org.js来完成添加组织,下面只提供程序的主要思路,细节可参考详细代码

1.安装所需Node模块
由于要在Nodejs程序中发送REST请求给configtxlator工具,所以需要事先安装模块(类似于curl):superagentsuperagent-promiserequest,其中request建议使用v1.9.8版本。导入模块:

var requester = require('request');
var agent = require('superagent-promise')(require('superagent'), Promise);

2.获取当前配置区块
调用getChannelConfig接口获取到最新的配置信息,接收到的结果是ConfigEvelope类型的对象:

var config_envelope = await channel.getChannelConfig()

我们只需要用到其中的config部分,取出后将其转化为二进制,注意original_config_proto是原始的配置信息,会在后面计算差值时用到。

var original_config_proto = config_envelope.config.toBuffer();

3.利用工具转化为json格式
使用configtxlator工具进行protobuf和json之间的转换,利用superagent-promise发出请求:

var response = await 
  agent.post('http://127.0.0.1:7059/protolator/decode/common.Config',original_config_proto).buffer();

对响应结果进行处理:

var original_config_json = response.text.toString()     // json string
var updated_config = JSON.parse(updated_config_json)    // json object

4.手动增加新组织的信息
我们需要仿照已有的两个组织的配置结构添加上新组织的信息,首先复制Org1MSP部分的内容,注意这里通过先stringify再parse的方式完成一次深拷贝。

var new_config = JSON.parse(JSON.stringify(updated_config.channel_group.groups.Application.groups["Org1MSP"]));

接下来就是在new_config中做相关修改,主要包括两部分,一是所有跟组织名称有关的地方,都需要将Org1替换为Org3;二是将相关证书的值替换成Org3的MSP目录中的实际证书的内容(从文件中读取后还需要进行base64编码),三种证书的路径如下(当前位于app目录下,这里使用相对路径):

// 1.admins:组织管理员证书
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/Admin@org3.example.com-cert.pem'
// 2.root_certs:根CA证书
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/ca.org3.example.com-cert.pem'
// 3.tls_root_certs:tls根证书
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/tlsca.org3.example.com-cert.pem'

需要修改的具体位置这里就不方便一一展开了,细节还是参考下代码。在编写js代码的时候可以将配置信息的json对象打印出来,对比下已有组织的配置内容,就可以很直观的找到那些需要替换的地方了。

完成编辑之后,将新组织的配置new_org当前到原有配置update_config上:

updated_config.channel_group.groups.Application.groups["Org3MSP"] = new_config;

并转化为json字符串:

updated_config_json = JSON.stringify(updated_config);

5.利用工具将json格式转为pb格式

response = await 
  agent.post('http://127.0.0.1:7059/protolator/encode/common.Config', updated_config_json.toString()).buffer();
var updated_config_proto = response.body;     // 响应结果:pb格式

6.利用工具计算差值
通道更新请求需要的参数并不是新的配置信息,而是新配置与原始配置的一个差值,需要再次利用configtxlator工具计算这个增量。
首先构造向工具发送的请求体的结构,需要附带我们原始获取的配置original_config_proto以及修改过后的配置updated_config_proto,两者都是pb格式:

var formData = {
    channel: channel_name,
    original: {
        value: original_config_proto,
        options: {
            filename: 'original.proto',
            contentType: 'application/octet-stream'
        }
    },
    updated: {
        value: updated_config_proto,
        options: {
            filename: 'updated.proto',
            contentType: 'application/octet-stream'
        }
    }
};

通过request模块发送post请求:

requester.post({
        url:'http://127.0.0.1:7059/configtxlator/compute/update-from-configs',
        encoding: null,
        headers: {
            accept: '/',
            expect: '100-continue'
        },
        formData: formData
    }

计算的结果转化为二进制以后赋值给变量config_proto,这就是通道配置的更新增量,下面会作为通道更新请求的重要参数。

7.对配置更新增量进行签名
更新通道的请求需要超过半数的已有组织的管理员身份签名,现有两个组织,则需要两个签名。调用help.js里的getOrgAdmin()方法可以给client对象分配管理员用户对象,然后调用SDK中的signChannelConfig()对配置进行签名:

var signatures = []
for (let org of cur_orgs) {
    let client = helper.getClientForOrg(org)
    await helper.getOrgAdmin(org)     // 给client分配管理员对象
    let signature = client.signChannelConfig(config_proto);
    signatures.push(signature)
}

其中cur_orgs参数是除Orderer外所有组织名的集合,这里用了一个循环让所有组织管理员对配置签名。

8.发送更新通道的请求
首先构造请求体:

let tx_id = client.newTransactionID();
var request = {
    config: config_proto,        // 配置更新增量
    signatures: signatures,      // 组织管理员签名
    name: channel_name,
    orderer: channel.getOrderers()[0],
    txId: tx_id
};

调用SDK的updateChannel()接口对通道进行更新,该方法在内部会将新的配置交易发送到orderer节点,打包成配置区块后分发给当前所有peer节点,peer节点将新的配置区块存入链中,此时该通道就接受认可了新加入的组织。

var result = await client.updateChannel(request);

三、执行代码加入新组织

Nodejs代码编写完成后整个工作就成功了一大半,接下来需要执行该程序,将Org3加入到当前网络。

首先启动configtxlator服务,默认监听7059端口:

configtxlator start

然后运行我们的Nodejs程序:

node add_org.js

成功响应后说明新组织加入成功,此时链上会生成一个新的配置区块。

四、更新配置文件

1.创建CA服务器配置文件
新加的组织Org3也拥有一个属于自己的CA节点,在之前的修改组织名的文章中已经介绍了如何设置CA服务器配置文件fabric-ca-server-config.yaml(主要是affiliations部分需要修改),以及如何在docker-compose文件中将该文件映射到CA容器内部。我的Github中也保存了该配置文件的模板

2.编写容器配置文件启动新组织节点
现在启动Org3中的节点,首先需要编写docker-compose文件。这一步比较简单,只要模仿已有组织的docker-compose.yaml文件即可。

Org3包含一个CA节点,两个Peer节点。编写该配置文件需要注意:如果所有组织都在一个机器上,则要保证容器的端口不会冲突。而且CA容器中的CA_KEYFILETLS_KEYFILE两个参数要和实际新组织的msp目录中的私钥文件路径一致。最后不要忘记添加CA服务器配置文件的映射。

将已完成Org3的配置文件docker-compose-org3.yaml置于artifacts目录下,执行以下命令启动三个节点:

docker-compose -f docker-compose-org3.yaml up -d

3.修改网络配置文件network-config.json
该文件路径为app/network-config.json,主要设置了网络各组织节点的ip和port信息,用于应用程序与网络节点进行交互。

需要仿照已有的组织,添加上新加入组织的信息,Org3部分大致如下:

"Org3": {
    "name": "peerOrg3",
    "mspid": "Org3MSP",
    "ca": "https://localhost:7054",
    "peers": {
        "peer1": {
            "requests": "grpcs://localhost:9051",
            "events": "grpcs://localhost:9053",
            "server-hostname": "peer0.org3.example.com",
            "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt"
        },
        "peer2": {
            "requests": "grpcs://localhost:9056",
            "events": "grpcs://localhost:9058",
            "server-hostname": "peer1.org3.example.com",
            "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/peers/peer1.org3.example.com/tls/ca.crt"
        }
    },
    "admin": {
        "key": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp/keystore",
        "cert": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/users/Admin@org3.example.com/msp/signcerts"
    }
}

五、将新组织中的节点加入通道

新组织的节点容器已经启动,首先需要在Org3注册某个用户,拿到Org3的TOKEN,这里设为ORG3_TOKEN,然后发送请求把Org3中的两个节点加入到通道中:

curl -s -X POST \
  http://localhost:4000/channels/mychannel/peers \
  -H "authorization: Bearer $ORG3_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "peers": ["peer1","peer2"]
}'

六、升级链码

目前新组织节点没有安装链码,只能参与记账,无法指定其完成查询或交易操作。但是即使安装了旧版本的链码,会发现节点可以查询,但是进行的交易确是无效的。

这是因为在chaincode实例化的时候会指定背书策略,默认是channel其中一个组织的某一个成员进行背书,但是该背书策略中没有包含后续新加入的组织,所以在验证阶段会被标记成invalid,能一直产生区块,却不会写入状态数据库。

所以如果需要新加组织的节点来执行交易,则需要对链码进行升级,不改变链码内容,只改变版本和背书策略,为的就是在背书策略中加入新组织。

利用SDK来upgrade chaincode也并非易事,需要自行编写js代码来实现。升级链码和实例化链码很相似,都需要生成一个交易。SDK中提供了sendUpgradeProposal()方法来发送升级链码的提案,我们可以参考balance-transfer中的instantiateChaincode.js(链码实例化)代码来编写升级链码的代码,详细接口代码可见github

首先需要设置新的背书策略,该背书策略表示只要3个组织中的其中一个组织的任意一个节点对某个交易背书,该交易就满足策略。

var endorsement_policy = 
{
  identities: [
    { role: { name: "member", mspId: "Org1MSP" }},
    { role: { name: "member", mspId: "Org2MSP" }},
    { role: { name: "member", mspId: "Org3MSP" }}
  ],
  policy: {
    "1-of": [{ "signed-by": 0 }, { "signed-by": 1 }]
  }
}

接下来构造升级链码的请求:

var request = {
  "chaincodeId": "mycc",
  "chaincodeVersion": "v1",
  "args": [''],
  "txId": client.newTransactionID(),
  "endorsement-policy": endorsement_policy
};

然后发送提案到背书节点:

var results = channel.sendUpgradeProposal(request);

最后和交易流程一样,需要根据提案响应生成交易,发送到排序服务节点:

var sendPromise = channel.sendTransaction(txRequest);

成功后通道会接受新版本的链码,在Org3安装新链码后可以指定其节点进行有效的查询和交易操作。至此,添加新组织成功!

实际应用开发中的实现

应用开发中应该优先选择上述利用js脚本增加组织的方法。当然也可以使用cli容器的方法,最好要写一个脚本,自动启动cli容器,完成上述所有操作以后再删除cli容器,不过相比调用SDK还是有诸多不便。

我在实际开发中是将添加或删除组织升级链码这两个功能加入了应用程序代码中,写成了RESTful接口,客户端可以通过http请求来完成这两个操作。

并且还写了一个shell脚本,来自动化执行一些操作,包括生成证书,启动configtxlator工具,发送更改组织的请求,关闭工具等。如果进一步完善,甚至可以将后续修改配置文件等操作也加入脚本中,达到一键执行就能够完成增加或者删除组织的效果。

上述代码可以在我的Github中找到:https://github.com/zhayujie/fabric-tools

标签: fabric

已有 28 条评论

  1. 大赢家xue 大赢家xue

    大佬,现在用的fabric1.4版本,有相应的接口可以直接调用动态创建组织么?

    1. 大赢家xue 大赢家xue

      就是Fabric官方自带的一些接口?

      1. zyj zyj

        Fabric 1.4官方提供的动态增加组织方案还是基于cli容器来实现的,参考这篇文档
        如果你想在app层来完成组织变更,需要参考我这篇博文使用sdk来进行一些封装实现。

  2. zyt zyt

    生成的相关文件能不能通过sdk生成呢

    1. zyj zyj

      node-sdk中是没有提供生成相关文件的功能的,所以我通过脚本对这些生成命令做了整合,参考Github
      当然也可以在应用的js代码中封装所有的操作步骤,供外部调用,效果其实等同于sdk。

  3. abc abc

    order 报错:
    Rejecting broadcast of config message from 172.20.60.50:60272 because of error: initializing channelconfig failed: could not create channel Application sub-group config: setting up the MSP manager failed: sanitizeCert failed the supplied identity is not valid: x509: certificate signed by unknown authority
    目前是2个组织,添加组织3

    1. zyj zyj

      错误提示的应该是签名无效,请问请求中包含签名了吗,添加org3需要前2个组织的管理员分别签名。

      1. 哥哥 哥哥

        管理员都有签名,我把order 日志上传了,链接: https://pan.baidu.com/s/169WKRPFXhyGGzWQLnQnA4w 提取码: tjad
        可能的话,帮我看看

        1. zyj zyj

          日志上说需要Org2的签名,但是获取到的是Org1的: "the identity is a member of a different MSP (expected Org2MSP, got Org1MSP)" 方便把创建签名的代码给我看看嘛

  4. 哥哥 哥哥

    你好,按照这个流程,到最后一步,发送交易到order时,order有报错信息:error authorizing update: proto: can't skip unknown wire type 6,这个是什么原因?

    1. zyj zyj

      你发送post请求时是否有加上 "encoding: null" 和 "headers:{accept: '/', expect: '100-continue'}" 呢?

      1. 哥哥 哥哥

        感谢回复!
        有加,但是没效果

        1. zyj zyj

          那我不太清楚了,会不会是提交更新的结构体有问题,可以把updated_config打印出来分析一下。
          又或者是版本的问题,我的 node-sdk 版本是 v1.1.0, 用的configtxlator也是1.1.0版本,具体代码在 https://github.com/zhayujie/fabric-tools/blob/master/add-org.js 。 希望对你有帮助。

          1. 哥哥 哥哥

            这个问题搞定了,提交的解码方法错了。encode 写成decode

  5. ana ana

    您好,看了您的动态添加的fabric-tool,感觉如果添加org3的话还得需要更新一下config.json 不然再添加org4则会报错吧。

    1. zyj zyj

      是的,可能需要修改一下network-config.json的模板文件。不过后面还有个多机加入新组织,是自动在配置文件中添加组织的。

  6. dw dw

    问下大佬,有没有使用go语言动态添加组织的

    1. zyj zyj

      可以找一下go-sdk里有没有相关接口,我觉得思路应该是相似的。

  7. wzc wzc

    厉害 受教了

  8. fabric fabric

    最后一步升级合约能否再详细些,目前安装可以,升级不行

    1. zyj zyj

      升级合约的代码我已经更新到github中: https://github.com/zhayujie/fabric-tools/blob/master/upgrade-chaincode.js, 你需要把这个接口添加到自己的应用中,然后客户端发送请求来完成升级。

      1. fabric fabric

        ok好了,非常感谢,这里面坑蛮多的,我遇到的是docker容器之间的网络问题导致的,现在有个问题问下你,如果多机部署,域名还是假域名,之间是怎么联通呢,现在有个问题 新节点和order连接不通,比如order是order0.a.com,在同一台机器可以用加入之前容器网络解决,不通机器这个怎么弄呢,现在想到两种方法每台都有一个order,或者在节点中指定order.a.com 的hosts

        1. zyj zyj

          你可以看下后面一篇关于多机部署的博文,第一部分可能就是你需要的。 只要在新机器的docker-compose.yaml文件中指定orderer节点的域名和ip就可以了。

          1. fabric fabric

            嗯,是的用docker-compose extra_hosts可以解决,之前没想好order节点这块,用的solo排序服务,现在想改成kafka,这个能动态加吗?改完后面order扩展方便吗?

            1. zyj zyj

              排序节点动态增加我还没有尝试过。提供一个思路,可以先搭一个基于kafka的fabric网络,获取并查看其配置区块的结构,然后在已有的网络上给新加的排序节点生成证书, 最后仿照kafka网络的配置区块去修改当前链的配置区块。 感觉还是太复杂,最好还是重启网络来修改把。

  9. fabric fabric

    希望有用,点个赞!!!

    1. zyj zyj

      遇到问题可以留言哈

添加新评论