fabric能够跨链吗?

前言

今天公司让我整理一个基于fabric的跨链的方案,以前没怎么接触过跨链,在这里记录下本身的思路吧。git

首先,先明白几个概念。什么是跨链?个人理解是跨链是跨channel。下面详细说下个人理由:github

  1. 回顾下fabric的启动过程:建立证书,生成创世区块,通道配置交易块,建立通道,节点加入通道,安装链码,实例化链码,链码的调用。这个是完整的生命周期。
  2. 一个节点上能够安装多个chaincode,且每一个chaincode是一个帐本。
  3. 同一个通道中,全部的节点安装的是相同的chaincode,因此每一个节点都有完整的数据,不存在跨链之说。
  4. 综上,跨链是指跨channel,由于不一样的channel拥有不一样的帐本,跨链的本质是把一个链上的数据转移到另一条链上

跨链咱们既能够在上层来作,也能够在chaincode层来作。通过查找我发现了一个InvokeChaincode方法,看着不错,看上去是用来调用其余的chaincode的。docker

因此我设计以下的跨链方案:api

简单描述下:Org1中的peer1和ORG3中的peer3加入channel1,而且安装Chaincode1,Org2中的peer2 和ORG3中的peer3加入channel2,而且安装Chaincode2。
peer3这个节点是能够跨链的关键所在,由于该节点同时拥有两个通道的数据。网络

先整个简易版的跨链流程:app

  1. Chaincode1:UserA向UserPub转移10元钱,UserPub把这笔钱标记为已锁定:
  2. Chaincode2:经过invokeChaincode查询UserPub是否已经锁定该笔钱。未锁定,则终止该次跨链,并把资产转回UserA。不然执行3
  3. Chaincode2:UserPub向UserB转移10元钱,同时UserPub把这笔钱标记为已转移(注:该笔钱不可退回UserA。)
  4. 跨链完成

事情到这里,并无完,上面的操做不是一个原子操做,因此咱们必需要考虑事务性,若是中间步骤出错,咱们要将整个过程进行回滚,而且这是在分布式的环境下完成的,哎,真的让人头大。分布式


工欲善其事必先利其器,下面咱们来搭建跨链所需的环境ide

1. 搭建跨链环境

1.1 生成证书

在开始以前,咱们须要相应的搭建相应的开发环境,我是在fabric的源码基础上进行作的。基于 fabric v1.3.0
个人环境规划是:Org1有1个peer节点,Org2有1个peer节点,Org3有1个节点,其中Org1和Org3加入channel1,安装chaincode1,Org2和Org3加入channel2,安装chaincode2。函数

下面我所改动的文件的详细内容请参考:
https://github.com/Anapodoton/CrossChain

证书的生成咱们须要修改以下配置文件:
crypto-config.yaml
docker-compose-e2e-template.yaml
docker-compose-base.yam
generateArtifacts.sh

咱们须要添加第三个组织的相关信息,修改相应的端口号。

改动完成以后,咱们可使用cryptogen工具生成相应的证书文件,咱们使用tree命令进行查看。

1.2 生成创世区块,应用通道配置交易文件和锚节点配置更新交易文件

咱们须要修改configtx.yaml文件和generateArtifacts.sh文件。

咱们使用的主要工具是configtxgen工具。目的是生成系统通道的创世区块,两个应用通道channel1和channel2的配置交易文件,每一个channel的每一个组织都要生成锚节点配置更新交易文件。生成后的文件以下所示:

1.3 启动相应的容器

咱们首先可使用docker-comppose-e2e来测试下网络的联通是否正常。

docker-compose -f docker-compose-e2e.yaml 看看网络是不是正常的 ,不正常的要及时调整。

接下来,咱们修改docker-compose-cli.yaml,咱们使用fabric提供的fabric-tools镜像来建立cli容器来代替SDK。

1.4 建立网络

这里主要使用的是script.sh来建立网络,启动orderer节点和peer节点。

咱们建立channel1,channel2,把各个节点分别加入channel,更新锚节点,安装链码,实例化链码。

上面的操做所有没有错误后,咱们就搭建好了跨链的环境了,这里在逼逼一句,咱们建立了两个通道,每一个通道两个组织,其中Org3是其交集。下面能够正式的进行跨链了。

其实在前面的操做中,并非一路顺风的,你们能够看到,须要修改的文件其实仍是蛮多的,有一个地方出错,网络就启动不了,建议你们分步进行运行,一步一步的解决问题,好比说,我在configtx.yaml文件中,ORG3的MSPTYPE指定成了idemix类型的,致使后面不管如何也验证不过,通道没法建立成功。

简单说下idemix,这个玩意是fabric v1.3 引入的一个新的特性,是用来用户作隐私保护的,基于零知识证实的知识,这里不在详述,感兴趣的能够参考:
fabric关于idemix的描述

2. 跨链关键技术

2.1 API解读

找到fabric提供了这么一个函数的文档,咱们先来看看。

invokechaincode

// InvokeChaincode documentation can be found in 
interfaces.gofunc (stub *ChaincodeStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response {     
// Internally we handle chaincode name as a composite name     
    if channel != "" {          
    chaincodeName = chaincodeName + "/" + channel     
    }    
    return stub.handler.handleInvokeChaincode(chaincodeName, args, stub.ChannelId, stub.TxID)}

下面是官方的文档说明:

// InvokeChaincode locally calls the specified chaincode `Invoke` using the
// same transaction context; that is, chaincode calling chaincode doesn't
// create a new transaction message.
// If the called chaincode is on the same channel, it simply adds the called
// chaincode read set and write set to the calling transaction.
// If the called chaincode is on a different channel,
// only the Response is returned to the calling chaincode; any PutState calls
// from the called chaincode will not have any effect on the ledger; that is,
// the called chaincode on a different channel will not have its read set
// and write set applied to the transaction. Only the calling chaincode's
// read set and write set will be applied to the transaction. Effectively
// the called chaincode on a different channel is a `Query`, which does not
// participate in state validation checks in subsequent commit phase.
// If `channel` is empty, the caller's channel is assumed.
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response

上面的意思是说:
InvokeChaincode并不会建立一条新的交易,使用的是以前的transactionID。
若是调用的是相同通道的chaincode,返回的是调用者的chaincode的响应。仅仅会把被调用的chaincode的读写集添加到调用的transaction中。
若是被调用的chaincode在不一样的通道中,任何PutState的调用都不会影响被调用chaincode的帐本。

再次翻译下,相同的通道invokeChaincode能够读能够写,不一样的通道invokeChaincode能够读,不能够写。(可是能够读也是有前提的,两者必须有相同的共同的物理节点才能够)。下面咱们写个demo来验证下。

2.2 验证

下面我简单搭建一个测试网络来进行验证,仍是两个channel,channel2中的chaincode经过invokeChaincode方法尝试调用chaincode1中的方法,咱们来看看效果。

咱们采用方案的核心是不一样通道的Chaincode是否能够query? 须要在什么样的条件下才能够进行query?

其中chaincode1是fabric/examples/chaincode/go/example02,chaincode2是fabric/examples/chaincode/go/example05

直接贴出queryByInvoke核心代码:

f := "query"
    queryArgs := toChaincodeArgs(f, "a")
    // if chaincode being invoked is on the same channel,
    // then channel defaults to the current channel and args[2] can be "".
    // If the chaincode being called is on a different channel,
    // then you must specify the channel name in args[2]
    response := stub.InvokeChaincode(chaincodeName, queryArgs, channelName)

咱们分别执行以下两次查询:
第一次:
peer chaincode query -C "channel1" -n mycc1 -c '{"Args":["query","a"]}'

结果以下:能够查到正确的结果。

咱们再次查询,在channel2上经过chaincode2中的queryByInvoke方法调用channel1的chaincode1中的query方法:

peer chaincode query -C "channel2" -n mycc2 -c '{"Args":["queryByInvoke","a","mycc1"]}'

结果以下所示:

咱们成功的跨越通道查到了所需的数据。可是事情真的这么完美吗?若是两个通道没有公共的物理节点还能够吗?咱们再来测试下,此次咱们的网络是channel1中有peer1,channel2中有peer2,两者没有共同节点,咱们再次在channel2中InvokeChaincode Channel1中的代码,废话再也不多说,咱们直接来看调用的结果:

综上:结论是不一样的通道能够query,但前提必须是有共同的物理节点。

2.3 深刻了解

下面的内容不是必须看的,咱们来深刻进去看看invokeChaincode究竟是如何实现的。咱们发现上面的代码引用了fabric/core/chaincode/shim/interfaces.go中的ChaincodeStubInterface接口的InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response

该接口的实如今其同目录下的Chaincode.go文件中,咱们看其代码:

// InvokeChaincode documentation can be found in interfaces.go
func (stub *ChaincodeStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response {   
// Internally we handle chaincode name as a composite name    
if channel != "" {          
    chaincodeName = chaincodeName + "/" + channel    
}   
return stub.handler.handleInvokeChaincode(chaincodeName, args, stub.ChannelId, stub.TxID)}

该方法把chaincodeName和channel进行了拼接,同时传入了ChannelId和TxID,两者是Orderer节点发送来的。而后调用了handleInvokeChaincode,咱们在来看handleInvokeChaincode。在同目录下的handler.go文件中。

/ handleInvokeChaincode communicates with the peer to invoke another chaincode.
func (handler *Handler) handleInvokeChaincode(chaincodeName string, args [][]byte, channelId string, txid string) pb.Response {
    //we constructed a valid object. No need to check for error
    payloadBytes, _ := proto.Marshal(&pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: chaincodeName}, Input: &pb.ChaincodeInput{Args: args}})

    // Create the channel on which to communicate the response from validating peer
    var respChan chan pb.ChaincodeMessage
    var err error
    if respChan, err = handler.createChannel(channelId, txid); err != nil {
        return handler.createResponse(ERROR, []byte(err.Error()))
    }

    defer handler.deleteChannel(channelId, txid)

    // Send INVOKE_CHAINCODE message to peer chaincode support
    msg := &pb.ChaincodeMessage{Type: pb.ChaincodeMessage_INVOKE_CHAINCODE, Payload: payloadBytes, Txid: txid, ChannelId: channelId}
    chaincodeLogger.Debugf("[%s] Sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_INVOKE_CHAINCODE)

    var responseMsg pb.ChaincodeMessage

    if responseMsg, err = handler.sendReceive(msg, respChan); err != nil {
        errStr := fmt.Sprintf("[%s] error sending %s", shorttxid(msg.Txid), pb.ChaincodeMessage_INVOKE_CHAINCODE)
        chaincodeLogger.Error(errStr)
        return handler.createResponse(ERROR, []byte(errStr))
    }

    if responseMsg.Type.String() == pb.ChaincodeMessage_RESPONSE.String() {
        // Success response
        chaincodeLogger.Debugf("[%s] Received %s. Successfully invoked chaincode", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_RESPONSE)
        respMsg := &pb.ChaincodeMessage{}
        if err := proto.Unmarshal(responseMsg.Payload, respMsg); err != nil {
            chaincodeLogger.Errorf("[%s] Error unmarshaling called chaincode response: %s", shorttxid(responseMsg.Txid), err)
            return handler.createResponse(ERROR, []byte(err.Error()))
        }
        if respMsg.Type == pb.ChaincodeMessage_COMPLETED {
            // Success response
            chaincodeLogger.Debugf("[%s] Received %s. Successfully invoked chaincode", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_RESPONSE)
            res := &pb.Response{}
            if err = proto.Unmarshal(respMsg.Payload, res); err != nil {
                chaincodeLogger.Errorf("[%s] Error unmarshaling payload of response: %s", shorttxid(responseMsg.Txid), err)
                return handler.createResponse(ERROR, []byte(err.Error()))
            }
            return *res
        }
        chaincodeLogger.Errorf("[%s] Received %s. Error from chaincode", shorttxid(responseMsg.Txid), respMsg.Type)
        return handler.createResponse(ERROR, responseMsg.Payload)
    }
    if responseMsg.Type.String() == pb.ChaincodeMessage_ERROR.String() {
        // Error response
        chaincodeLogger.Errorf("[%s] Received %s.", shorttxid(responseMsg.Txid), pb.ChaincodeMessage_ERROR)
        return handler.createResponse(ERROR, responseMsg.Payload)
    }

    // Incorrect chaincode message received
    chaincodeLogger.Errorf("[%s] Incorrect chaincode message %s received. Expecting %s or %s", shorttxid(responseMsg.Txid), responseMsg.Type, pb.ChaincodeMessage_RESPONSE, pb.ChaincodeMessage_ERROR)
    return handler.createResponse(ERROR, []byte(fmt.Sprintf("[%s] Incorrect chaincode message %s received. Expecting %s or %s", shorttxid(responseMsg.Txid), responseMsg.Type, pb.ChaincodeMessage_RESPONSE, pb.ChaincodeMessage_ERROR)))
}

咱们来讲下上面的步骤:

  1. 序列化查询参数
  2. 使用channelId+ txid建立了一个txCtxID通道(这里的通道指的是go里的通道,用于消息的发送和接收,不是fabric里的,不要混淆。)
  3. 构造INVOKE_CHAINCODE类型的消息
  4. sendReceive(msg *pb.ChaincodeMessage, c chan pb.ChaincodeMessage) 经过grpc发送invokeChaincode(包括查询参数,channelID和交易ID)消息直到响应正确的消息。
    1. serialSendAsync(msg, errc)
    2. serialSend(msg *pb.ChaincodeMessage)
  5. 处理响应,若是接收到ChaincodeMessage_RESPONSE和ChaincodeMessage_COMPLETED类型的消息,说明InvokeChaincode成功,不然失败。
  6. 删除txCtxID

总结:InvokeChaincode本质上是构造了一个txCtxID,而后向orderer节点发送消息,最后把消息写入txCtxID,返回便可。

3. 跨链的实现

前面已经提到跨链的方案:

  1. Chaincode1:UserA向UserPub转移10元钱,UserPub把这笔钱标记为已锁定:
  2. Chaincode2:经过invokeChaincode查询UserPub是否已经锁定该笔钱。未锁定,则终止该次跨链,并把资产转回UserA。不然执行3
  3. Chaincode2:UserPub向UserB转移10元钱,同时UserPub把这笔钱标记为已转移(注:该笔钱不可退回UserA。)
  4. 跨链完成

其本质是经过一个公用帐户来作到的,经过invokeChaincode来保证金额确实被锁定的。这里面实际上是有很大的问题,咱们须要侵入别人的代码,这里就很烦,很不友好。

4. 可商用的跨链方案

3. 总结

在此次方案的研究中,仍是踩了不少的坑的,现总结以下:

  1. 对待一个陌生的东西,必定要先看官方文档,而后写个简单的demo进行验证。不要急着先干活。根据验证的结果在决定下面怎么办?
  2. 要学会思考,就好比说此次,其实很简单的道理,channel是为了保护数据的,不须要被调用 方作任何验证的状况下,怎么可能获取到数据呢?

跨链在实际的业务中仍是须要的,虽然没法经过chaincode来实现,可是仍是要想其余办法的。

相关文章
相关标签/搜索